iqm-exa-common 25.34__py3-none-any.whl → 26.0__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.
- exa/common/api/proto_serialization/_parameter.py +4 -3
- exa/common/api/proto_serialization/nd_sweep.py +3 -8
- exa/common/api/proto_serialization/sequence.py +5 -5
- exa/common/control/sweep/exponential_sweep.py +15 -47
- exa/common/control/sweep/fixed_sweep.py +10 -14
- exa/common/control/sweep/linear_sweep.py +15 -40
- exa/common/control/sweep/option/__init__.py +1 -1
- exa/common/control/sweep/option/center_span_base_options.py +14 -7
- exa/common/control/sweep/option/center_span_options.py +13 -6
- exa/common/control/sweep/option/constants.py +2 -2
- exa/common/control/sweep/option/fixed_options.py +8 -2
- exa/common/control/sweep/option/option_converter.py +4 -8
- exa/common/control/sweep/option/start_stop_base_options.py +20 -6
- exa/common/control/sweep/option/start_stop_options.py +20 -5
- exa/common/control/sweep/option/sweep_options.py +9 -0
- exa/common/control/sweep/sweep.py +52 -16
- exa/common/control/sweep/sweep_values.py +58 -0
- exa/common/data/base_model.py +40 -0
- exa/common/data/parameter.py +123 -68
- exa/common/data/setting_node.py +481 -135
- exa/common/data/settingnode_v2.html.jinja2 +6 -6
- exa/common/data/value.py +49 -0
- exa/common/logger/logger.py +1 -1
- exa/common/qcm_data/file_adapter.py +2 -6
- exa/common/qcm_data/qcm_data_client.py +1 -37
- exa/common/sweep/database_serialization.py +30 -98
- exa/common/sweep/util.py +4 -5
- {iqm_exa_common-25.34.dist-info → iqm_exa_common-26.0.dist-info}/METADATA +2 -2
- iqm_exa_common-26.0.dist-info/RECORD +54 -0
- exa/common/api/model/__init__.py +0 -15
- exa/common/api/model/parameter_model.py +0 -111
- exa/common/api/model/setting_model.py +0 -63
- exa/common/api/model/setting_node_model.py +0 -72
- exa/common/api/model/sweep_model.py +0 -63
- exa/common/control/sweep/function_sweep.py +0 -35
- exa/common/control/sweep/option/function_options.py +0 -26
- exa/common/control/sweep/utils.py +0 -43
- iqm_exa_common-25.34.dist-info/RECORD +0 -59
- {iqm_exa_common-25.34.dist-info → iqm_exa_common-26.0.dist-info}/LICENSE.txt +0 -0
- {iqm_exa_common-25.34.dist-info → iqm_exa_common-26.0.dist-info}/WHEEL +0 -0
- {iqm_exa_common-25.34.dist-info → iqm_exa_common-26.0.dist-info}/top_level.txt +0 -0
exa/common/data/setting_node.py
CHANGED
|
@@ -101,6 +101,14 @@ The values can be changed with a simple ``=`` syntax:
|
|
|
101
101
|
|
|
102
102
|
``node.setting`` refers to the Setting object. ``node.setting.value`` syntax refers to the data stored inside.
|
|
103
103
|
|
|
104
|
+
``SettingNode`` also supports "the path notation" by default (but not if ``align_name`` is set to ``False``,
|
|
105
|
+
since it cannot be made to work consistently if nodes are allowed to be named differently from their paths):
|
|
106
|
+
|
|
107
|
+
.. doctest:: pulse
|
|
108
|
+
|
|
109
|
+
>>> node['flux.voltage']
|
|
110
|
+
|
|
111
|
+
is the same as ``node['flux']['voltage']``.
|
|
104
112
|
|
|
105
113
|
Basic manipulation
|
|
106
114
|
------------------
|
|
@@ -116,6 +124,27 @@ Adding and deleting new Settings and nodes is simple:
|
|
|
116
124
|
|
|
117
125
|
It is usually a good idea to make a copy of the original node, so that it won't be modified accidentally.
|
|
118
126
|
|
|
127
|
+
The path notation of ``SettingNode``also works when inserting:
|
|
128
|
+
|
|
129
|
+
.. doctest:: pulse
|
|
130
|
+
|
|
131
|
+
>>> node['flux.my.new.path.foo'] = Setting(Parameter('foo'), 1.0)
|
|
132
|
+
|
|
133
|
+
Any nodes that did not already exist under ``node`` will be inserted (in this case ``flux`` already existed, but
|
|
134
|
+
the rest not, so under ``flux`` the nodes ``my``, ``new``, and ``path`` would be added), and then finally the
|
|
135
|
+
value is added as child to the final node. Note: ``SettingNode`` always alings the path and name of any nodes under it,
|
|
136
|
+
so this would result in the new setting being renamed as "flux.my.new.path.foo":
|
|
137
|
+
|
|
138
|
+
.. doctest:: pulse
|
|
139
|
+
|
|
140
|
+
>>> node['flux.my.new.path.foo'] = Setting(Parameter('bar'), 1.0)
|
|
141
|
+
|
|
142
|
+
If ``align_name`` is set to ``False", the name and path of nodes are not automatically aligned, but otherwise the above
|
|
143
|
+
path notation will still work. The added nodes will be named by just their path fragments ("my", "new", "path", and
|
|
144
|
+
so on), and the Setting will be added under the key "foo", but it will still retain its name "bar". Note: the root node
|
|
145
|
+
name will always be excluded from the paths (and names when they are aligned with the path), so that the path of
|
|
146
|
+
``root.foo.bar`` is ``"foo.bar"``.
|
|
147
|
+
|
|
119
148
|
To merge values of two SettingNodes, there are helpers :meth:`.SettingNode.merge` and
|
|
120
149
|
:meth:`.SettingNode.merge_values`.
|
|
121
150
|
|
|
@@ -177,20 +206,45 @@ flag is used.
|
|
|
177
206
|
from __future__ import annotations
|
|
178
207
|
|
|
179
208
|
from collections.abc import Generator, ItemsView, Iterator
|
|
180
|
-
from copy import copy
|
|
209
|
+
from copy import copy
|
|
210
|
+
from itertools import permutations
|
|
181
211
|
import logging
|
|
182
212
|
import numbers
|
|
183
213
|
import pathlib
|
|
184
|
-
from typing import Any
|
|
214
|
+
from typing import Any, Iterable
|
|
185
215
|
|
|
186
216
|
import jinja2
|
|
187
217
|
import numpy as np
|
|
188
218
|
|
|
219
|
+
from exa.common.data.base_model import BaseModel
|
|
189
220
|
from exa.common.data.parameter import CollectionType, Parameter, Setting
|
|
190
221
|
from exa.common.errors.exa_error import UnknownSettingError
|
|
222
|
+
from exa.common.qcm_data.chip_topology import sort_components
|
|
191
223
|
|
|
224
|
+
logger = logging.getLogger(__name__)
|
|
192
225
|
|
|
193
|
-
|
|
226
|
+
|
|
227
|
+
def _fix_path_recursive(node: SettingNode | dict, path: str) -> SettingNode | dict:
|
|
228
|
+
"""Recursively travel the settings tree and fix the ``path``attribute (also aligns ``name``,
|
|
229
|
+
based on the node type). Deep copies all the child nodes.
|
|
230
|
+
"""
|
|
231
|
+
settings = {}
|
|
232
|
+
subtrees = {}
|
|
233
|
+
for key, setting in node.settings.items():
|
|
234
|
+
child_path = f"{path}.{key}"
|
|
235
|
+
update_dict = {"path": child_path}
|
|
236
|
+
if node.align_name:
|
|
237
|
+
update_dict["parameter"] = setting.parameter.model_copy(update={"name": child_path})
|
|
238
|
+
settings[key] = setting.model_copy(update=update_dict)
|
|
239
|
+
for key, subnode in node.subtrees.items():
|
|
240
|
+
subtrees[key] = _fix_path_recursive(subnode, f"{path}.{key}")
|
|
241
|
+
node_update_dict = {"path": path, "settings": settings, "subtrees": subtrees}
|
|
242
|
+
if node.align_name:
|
|
243
|
+
node_update_dict["name"] = path
|
|
244
|
+
return node.model_copy(update=node_update_dict, deep=False)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class SettingNode(BaseModel):
|
|
194
248
|
"""A tree-structured :class:`.Setting` container.
|
|
195
249
|
|
|
196
250
|
Each child of the node is a :class:`.Setting`, or another :class:`SettingNode`.
|
|
@@ -202,90 +256,176 @@ class SettingNode:
|
|
|
202
256
|
>>> from exa.common.data.parameter import Parameter
|
|
203
257
|
>>> from exa.common.data.setting_node import SettingNode
|
|
204
258
|
>>> p1 = Parameter("voltage", "Voltage")
|
|
205
|
-
>>>
|
|
206
|
-
>>>
|
|
259
|
+
>>> f1 = Parameter("frequency", "Frequency")
|
|
260
|
+
>>> sub = SettingNode("sub", frequency=f1)
|
|
261
|
+
>>> settings = SettingNode('name', voltage=p1)
|
|
262
|
+
>>> settings.voltage.parameter is p1
|
|
207
263
|
True
|
|
208
|
-
>>> settings['
|
|
264
|
+
>>> settings['voltage'].parameter is p1
|
|
209
265
|
True
|
|
210
|
-
>>> settings.
|
|
266
|
+
>>> settings.voltage.value is None
|
|
211
267
|
True
|
|
212
|
-
>>> settings.
|
|
213
|
-
>>> settings.
|
|
268
|
+
>>> settings.voltage = 7 # updates to Setting(p1, 7)
|
|
269
|
+
>>> settings.voltage.value
|
|
214
270
|
7
|
|
271
|
+
>>> settings["sub.frequency"] = 8
|
|
272
|
+
>>> settings["sub.frequency"].value
|
|
273
|
+
8
|
|
215
274
|
|
|
216
275
|
Args:
|
|
217
276
|
name: Name of the node.
|
|
218
|
-
|
|
277
|
+
settings: Dict of setting path fraqment names (usually the same as the setting name) to the settings. Mostly
|
|
278
|
+
used when deserialising and otherwise left empty.
|
|
279
|
+
subtrees: Dict of child node path fraqment names (usually the same as the child node name) to the settings.
|
|
280
|
+
Mostly used when deserialising and otherwise left empty.
|
|
281
|
+
path: Optionally give a path for the node, by default empty.
|
|
282
|
+
generate_paths: If set ``True``, all subnodes will get their paths autogenerated correctly. Only set to
|
|
283
|
+
``False`` if the subnodes already have correct paths set (e.g. when deserialising).
|
|
284
|
+
kwargs: The children given as keyword arguments. Each argument must be a :class:`.Setting`,
|
|
219
285
|
:class:`.Parameter`, or a :class:`SettingNode`. The keywords are used as the names of the nodes.
|
|
220
286
|
Parameters will be cast into Settings with the value ``None``.
|
|
221
287
|
|
|
222
288
|
"""
|
|
223
289
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
290
|
+
name: str
|
|
291
|
+
settings: dict[str, Setting] = {}
|
|
292
|
+
subtrees: dict[str, SettingNode] = {}
|
|
293
|
+
path: str = ""
|
|
294
|
+
|
|
295
|
+
align_name: bool = True
|
|
296
|
+
|
|
297
|
+
def __init__(
|
|
298
|
+
self,
|
|
299
|
+
name: str,
|
|
300
|
+
settings: dict[str, Any] | None = None,
|
|
301
|
+
subtrees: dict[str, Any] | None = None,
|
|
302
|
+
*,
|
|
303
|
+
path: str = "",
|
|
304
|
+
align_name: bool = True,
|
|
305
|
+
generate_paths: bool = True,
|
|
306
|
+
**kwargs,
|
|
307
|
+
):
|
|
308
|
+
settings: dict[str, Any] = settings or {}
|
|
309
|
+
subtrees: dict[str, Any] = subtrees or {}
|
|
310
|
+
|
|
311
|
+
for key, child in kwargs.items():
|
|
230
312
|
if isinstance(child, Setting):
|
|
231
|
-
|
|
313
|
+
settings[key] = child
|
|
232
314
|
elif isinstance(child, Parameter):
|
|
233
|
-
|
|
315
|
+
settings[key] = Setting(parameter=child, value=None, path=key)
|
|
234
316
|
elif isinstance(child, SettingNode):
|
|
235
|
-
|
|
317
|
+
subtrees[key] = child
|
|
236
318
|
else:
|
|
237
319
|
raise ValueError(f"{key} should be a Parameter, Setting or a SettingNode, not {type(child)}.")
|
|
320
|
+
super().__init__(
|
|
321
|
+
name=name,
|
|
322
|
+
settings=settings,
|
|
323
|
+
subtrees=subtrees,
|
|
324
|
+
path=path,
|
|
325
|
+
align_name=align_name,
|
|
326
|
+
**kwargs,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
if generate_paths:
|
|
330
|
+
self._generate_paths_and_names()
|
|
331
|
+
|
|
332
|
+
def _generate_paths_and_names(self) -> None:
|
|
333
|
+
"""This method generates the paths and aligns the names when required."""
|
|
334
|
+
for key, child in self.subtrees.items():
|
|
335
|
+
update_path = f"{self.path}.{key}" if self.path else key
|
|
336
|
+
self.subtrees[key] = _fix_path_recursive(child, update_path)
|
|
337
|
+
for key, child in self.settings.items():
|
|
338
|
+
update_path = f"{self.path}.{key}" if self.path else key
|
|
339
|
+
if isinstance(child, Setting):
|
|
340
|
+
update_dict = {"path": update_path}
|
|
341
|
+
if self.align_name:
|
|
342
|
+
update_dict["parameter"] = child.parameter.copy(name=update_path)
|
|
343
|
+
self[key] = child.model_copy(update=update_dict)
|
|
344
|
+
elif self.align_name:
|
|
345
|
+
self[key] = Setting(
|
|
346
|
+
parameter=child.model_copy(update={"name": update_path}), value=None, path=update_path
|
|
347
|
+
)
|
|
348
|
+
if self.path and self.align_name:
|
|
349
|
+
self.name = self.path
|
|
238
350
|
|
|
239
351
|
def __getattr__(self, key):
|
|
240
|
-
if key == "
|
|
241
|
-
# Prevent infinite recursion. If
|
|
352
|
+
if key == "settings":
|
|
353
|
+
# Prevent infinite recursion. If settings actually exists, this method is not called anyway
|
|
242
354
|
raise AttributeError
|
|
243
|
-
if key in self.
|
|
244
|
-
return self.
|
|
245
|
-
if key in self.
|
|
246
|
-
return self.
|
|
247
|
-
raise UnknownSettingError(
|
|
355
|
+
if key in self.settings:
|
|
356
|
+
return self.settings[key]
|
|
357
|
+
if key in self.subtrees:
|
|
358
|
+
return self.subtrees[key]
|
|
359
|
+
raise UnknownSettingError(
|
|
360
|
+
f'{self.__class__.__name__} "{self.name}" has no attribute {key}. Children: {self.children.keys()}.'
|
|
361
|
+
)
|
|
248
362
|
|
|
249
363
|
def __dir__(self):
|
|
250
364
|
"""List settings and subtree names, so they occur in IPython autocomplete after ``node.<TAB>``."""
|
|
251
|
-
return [name for name in list(self.
|
|
365
|
+
return [name for name in list(self.settings) + list(self.subtrees) if name.isidentifier()] + super().__dir__()
|
|
252
366
|
|
|
253
367
|
def _ipython_key_completions_(self):
|
|
254
368
|
"""List items and subtree names, so they occur in IPython autocomplete after ``node[<TAB>``"""
|
|
255
|
-
return [*self.
|
|
369
|
+
return [*self.settings, *self.subtrees]
|
|
256
370
|
|
|
257
371
|
def __setattr__(self, key, value):
|
|
258
372
|
"""Overrides default attribute assignment to allow the following syntax: ``self.foo = 3`` which is
|
|
259
373
|
equivalent to ``self.foo.value.update(3)`` (if ``foo`` is a :class:`.Setting`).
|
|
260
374
|
"""
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
375
|
+
path = self._get_path(key)
|
|
376
|
+
if isinstance(value, (Setting, Parameter)):
|
|
377
|
+
if isinstance(value, Parameter):
|
|
378
|
+
if self.align_name:
|
|
379
|
+
value = value.model_copy(update={"name": path})
|
|
380
|
+
value = Setting(parameter=value, value=None, path=path)
|
|
381
|
+
else:
|
|
382
|
+
update_dict = {"path": path}
|
|
383
|
+
if self.align_name:
|
|
384
|
+
update_dict["parameter"] = value.parameter.model_copy(update={"name": path})
|
|
385
|
+
value = value.model_copy(update=update_dict)
|
|
386
|
+
self.settings[key] = value
|
|
387
|
+
self.subtrees.pop(key, None)
|
|
266
388
|
elif isinstance(value, SettingNode):
|
|
267
|
-
self.
|
|
268
|
-
self.
|
|
269
|
-
elif key != "
|
|
270
|
-
self.
|
|
389
|
+
self.subtrees[key] = _fix_path_recursive(value, path)
|
|
390
|
+
self.settings.pop(key, None)
|
|
391
|
+
elif key != "settings" and key in self.settings: # != prevents infinite recursion
|
|
392
|
+
self.settings[key] = self.settings[key].update(value)
|
|
271
393
|
else:
|
|
272
394
|
self.__dict__[key] = value
|
|
273
395
|
|
|
274
396
|
def __delattr__(self, key):
|
|
275
|
-
if key in self.
|
|
276
|
-
del self.
|
|
277
|
-
elif key in self.
|
|
278
|
-
del self.
|
|
397
|
+
if key in self.settings:
|
|
398
|
+
del self.settings[key]
|
|
399
|
+
elif key in self.subtrees:
|
|
400
|
+
del self.subtrees[key]
|
|
279
401
|
else:
|
|
280
402
|
del self.__dict__[key]
|
|
281
403
|
|
|
282
404
|
def __getitem__(self, item: str) -> Setting | SettingNode:
|
|
283
405
|
"""Allows dictionary syntax."""
|
|
284
|
-
|
|
406
|
+
if len(item.split(".")) == 1:
|
|
407
|
+
return self.__getattr__(item)
|
|
408
|
+
return self.get_node_for_path(item)
|
|
285
409
|
|
|
286
410
|
def __setitem__(self, key: str, value: Any) -> None:
|
|
287
411
|
"""Allows dictionary syntax."""
|
|
288
|
-
|
|
412
|
+
path_fragments = key.split(".")
|
|
413
|
+
node_key = path_fragments[0]
|
|
414
|
+
remaining_path = ".".join(path_fragments[1:])
|
|
415
|
+
|
|
416
|
+
if not remaining_path:
|
|
417
|
+
self.__setattr__(key, value)
|
|
418
|
+
elif node_key in self.children:
|
|
419
|
+
try:
|
|
420
|
+
self[node_key][remaining_path] = value
|
|
421
|
+
except TypeError:
|
|
422
|
+
raise ValueError(f"Path '{key}' is invalid: '{node_key}' is a setting, not a node.")
|
|
423
|
+
else:
|
|
424
|
+
if not isinstance(value, (SettingNode, Setting, Parameter)):
|
|
425
|
+
raise ValueError(
|
|
426
|
+
f"Assigning value {value} to path {key} cannot be done when this path is not found in self."
|
|
427
|
+
)
|
|
428
|
+
self.add_for_path({path_fragments[-1]: value}, path=".".join(path_fragments[:-1]))
|
|
289
429
|
|
|
290
430
|
def __delitem__(self, key):
|
|
291
431
|
"""Allows dictionary syntax."""
|
|
@@ -294,8 +434,8 @@ class SettingNode:
|
|
|
294
434
|
def __iter__(self):
|
|
295
435
|
"""Allows breadth-first iteration through the tree."""
|
|
296
436
|
yield self
|
|
297
|
-
yield from iter(self.
|
|
298
|
-
for subtree in self.
|
|
437
|
+
yield from iter(self.settings.values())
|
|
438
|
+
for subtree in self.subtrees.values():
|
|
299
439
|
yield from iter(subtree)
|
|
300
440
|
|
|
301
441
|
def nodes_by_type(
|
|
@@ -363,21 +503,17 @@ class SettingNode:
|
|
|
363
503
|
@property
|
|
364
504
|
def children(self) -> dict[str, Setting | SettingNode]:
|
|
365
505
|
"""Dictionary of immediate child nodes of this node."""
|
|
366
|
-
return {**self.
|
|
506
|
+
return {**self.settings, **self.subtrees}
|
|
367
507
|
|
|
368
508
|
@property
|
|
369
509
|
def child_settings(self) -> ItemsView[str, Setting]:
|
|
370
510
|
"""ItemsView of settings of this node."""
|
|
371
|
-
return self.
|
|
511
|
+
return self.settings.items()
|
|
372
512
|
|
|
373
513
|
@property
|
|
374
514
|
def child_nodes(self) -> ItemsView[str, SettingNode]:
|
|
375
515
|
"""ItemsView of immediate child nodes of this node."""
|
|
376
|
-
return self.
|
|
377
|
-
|
|
378
|
-
def copy(self) -> SettingNode:
|
|
379
|
-
"""Return a deepcopy of this SettingNode."""
|
|
380
|
-
return deepcopy(self)
|
|
516
|
+
return self.subtrees.items()
|
|
381
517
|
|
|
382
518
|
def get_parent_of(self, name: str) -> SettingNode:
|
|
383
519
|
"""Get the first SettingNode that has a Setting named `name`.
|
|
@@ -408,32 +544,49 @@ class SettingNode:
|
|
|
408
544
|
return next((item for item in self if item.name == name), None)
|
|
409
545
|
|
|
410
546
|
@staticmethod
|
|
411
|
-
def merge(
|
|
547
|
+
def merge(
|
|
548
|
+
first: SettingNode,
|
|
549
|
+
second: SettingNode,
|
|
550
|
+
merge_nones: bool = False,
|
|
551
|
+
align_name: bool = True,
|
|
552
|
+
deep_copy: bool = True,
|
|
553
|
+
) -> SettingNode:
|
|
412
554
|
"""Recursively combine the tree structures and values of two SettingNodes.
|
|
413
555
|
|
|
414
556
|
In case of conflicting nodes,values in `first` take priority regardless of the replaced content in `second`.
|
|
415
|
-
`None` values are
|
|
557
|
+
`None` values are not prioritized unless ``merge_nones`` is set to ``True``.
|
|
416
558
|
|
|
417
559
|
Args:
|
|
418
560
|
first: SettingNode to merge, whose values and structure take priority
|
|
419
561
|
second: SettingNode to merge.
|
|
562
|
+
merge_nones: Whether to merge also ``None`` values from ``first`` to ``second``.
|
|
563
|
+
align_name: Whether to align the paths (and also names if ``second`` does not use ``align_name==False``)
|
|
564
|
+
when merging the nodes. Should never be set ``False`` unless the paths in ``first`` already align with
|
|
565
|
+
what they should be in ``second`` (setting it ``False`` in such cases can improve performance).
|
|
566
|
+
deep_copy: Whether to deepcopy or just shallow copy all the sub-nodes. Set to ``False`` with high caution
|
|
567
|
+
and understand the consequences.
|
|
420
568
|
|
|
421
569
|
Returns:
|
|
422
570
|
A new SettingNode constructed from arguments.
|
|
423
571
|
|
|
424
572
|
"""
|
|
425
|
-
new = second.
|
|
426
|
-
for key, item in first.
|
|
427
|
-
if item.value is not None:
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
573
|
+
new = second.model_copy(deep=deep_copy)
|
|
574
|
+
for key, item in first.settings.items():
|
|
575
|
+
if merge_nones or item.value is not None:
|
|
576
|
+
if align_name:
|
|
577
|
+
new[key] = item.copy(name=item.name.replace(f"{first.name}.", ""))
|
|
578
|
+
else:
|
|
579
|
+
new.settings[key] = item.copy()
|
|
580
|
+
for key, item in first.subtrees.items():
|
|
581
|
+
item_copy = item.model_copy(update={"name": item.name.replace(f"{first.name}.", "")}, deep=deep_copy)
|
|
582
|
+
subs = new if align_name else new.subtrees
|
|
583
|
+
if key in new.subtrees:
|
|
584
|
+
subs[key] = SettingNode.merge(item_copy, new[key])
|
|
432
585
|
else:
|
|
433
|
-
|
|
586
|
+
subs[key] = item_copy
|
|
434
587
|
|
|
435
588
|
for key, item in first.__dict__.items():
|
|
436
|
-
if key not in ["
|
|
589
|
+
if key not in ["settings", "subtrees"]:
|
|
437
590
|
new[key] = copy(item)
|
|
438
591
|
return new
|
|
439
592
|
|
|
@@ -448,17 +601,17 @@ class SettingNode:
|
|
|
448
601
|
will be replaced.
|
|
449
602
|
|
|
450
603
|
"""
|
|
451
|
-
for key, item in other.
|
|
452
|
-
if key in self.
|
|
453
|
-
self[key] = item.value
|
|
454
|
-
for key, item in other.
|
|
455
|
-
if key in self.
|
|
456
|
-
self[key].merge_values(copy(item), prioritize_other)
|
|
604
|
+
for key, item in other.settings.items():
|
|
605
|
+
if key in self.settings and (prioritize_other or (self[key].value is None)):
|
|
606
|
+
self.settings[key] = Setting(self.settings[key].parameter, item.value)
|
|
607
|
+
for key, item in other.subtrees.items():
|
|
608
|
+
if key in self.subtrees:
|
|
609
|
+
self.subtrees[key].merge_values(copy(item), prioritize_other)
|
|
457
610
|
|
|
458
611
|
def prune(self, other: SettingNode) -> None:
|
|
459
612
|
"""Recursively delete all branches from this SettingNode that are not found in ``other``."""
|
|
460
|
-
for key, node in self.
|
|
461
|
-
if key not in other.
|
|
613
|
+
for key, node in self.subtrees.copy().items():
|
|
614
|
+
if key not in other.subtrees:
|
|
462
615
|
del self[key]
|
|
463
616
|
else:
|
|
464
617
|
self[key].prune(other[key])
|
|
@@ -471,10 +624,10 @@ class SettingNode:
|
|
|
471
624
|
|
|
472
625
|
"""
|
|
473
626
|
|
|
474
|
-
def append_lines(
|
|
627
|
+
def append_lines(node: SettingNode, lines: list[str], indents: list[bool]):
|
|
475
628
|
indent = "".join([" ║ " if i else " " for i in indents])
|
|
476
629
|
if len(indents) < levels:
|
|
477
|
-
for key, setting in node.
|
|
630
|
+
for key, setting in node.settings.items():
|
|
478
631
|
if setting.value is None:
|
|
479
632
|
value = "None (automatic/unspecified)"
|
|
480
633
|
elif setting.parameter.collection_type == CollectionType.NDARRAY:
|
|
@@ -482,23 +635,25 @@ class SettingNode:
|
|
|
482
635
|
else:
|
|
483
636
|
value = f"{setting.value} {setting.unit}"
|
|
484
637
|
lines.append(indent + f" ╠─ {key}: {setting.label} = {value}")
|
|
485
|
-
if node.
|
|
638
|
+
if node.subtrees:
|
|
486
639
|
lines.append(indent + " ║ ")
|
|
487
640
|
subtrees_written = 0
|
|
488
|
-
for key, subtree in node.
|
|
489
|
-
lines.append(indent + f' ╠═ {key}: "{subtree.name}"')
|
|
490
|
-
if subtrees_written == len(node.
|
|
641
|
+
for key, subtree in node.subtrees.items():
|
|
642
|
+
lines.append(indent + f' ╠═ {key}: "{subtree.name}: align_name={subtree.align_name}"')
|
|
643
|
+
if subtrees_written == len(node.subtrees) - 1:
|
|
491
644
|
lines[-1] = lines[-1].replace("╠", "╚")
|
|
492
|
-
append_lines(
|
|
645
|
+
append_lines(subtree, lines, indents + [subtrees_written < len(node.subtrees) - 1])
|
|
493
646
|
subtrees_written += 1
|
|
494
647
|
lines[-1] = lines[-1].replace("╠", "╚")
|
|
495
648
|
|
|
496
|
-
lines = [f'"{self.name}"']
|
|
497
|
-
append_lines(
|
|
649
|
+
lines = [f'"{self.name}: align_name={self.align_name}"']
|
|
650
|
+
append_lines(self, lines, [])
|
|
498
651
|
print("\n", "\n".join(lines))
|
|
499
652
|
|
|
500
653
|
def __eq__(self, other: SettingNode):
|
|
501
|
-
return isinstance(other, SettingNode) and
|
|
654
|
+
return isinstance(other, SettingNode) and (
|
|
655
|
+
(self.name, self.settings, self.subtrees) == (other.name, other.settings, other.subtrees)
|
|
656
|
+
)
|
|
502
657
|
|
|
503
658
|
def __repr__(self):
|
|
504
659
|
return f"{self.__class__.__name__}{self.children}"
|
|
@@ -518,9 +673,9 @@ class SettingNode:
|
|
|
518
673
|
A new SettingNode with the same structure as the original, but where node instances are of type `cls`.
|
|
519
674
|
|
|
520
675
|
"""
|
|
521
|
-
new = cls(name=node.name, **node.
|
|
522
|
-
for key, subnode in node.
|
|
523
|
-
new[key] = cls.transform_node_types(subnode)
|
|
676
|
+
new = cls(name=node.name, **node.settings, align_name=node.align_name, generate_paths=False)
|
|
677
|
+
for key, subnode in node.subtrees.items():
|
|
678
|
+
new.subtrees[key] = cls.transform_node_types(subnode)
|
|
524
679
|
return new
|
|
525
680
|
|
|
526
681
|
def set_from_dict(self, dct: dict[str, Any], strict: bool = False) -> None:
|
|
@@ -538,57 +693,24 @@ class SettingNode:
|
|
|
538
693
|
for key, value in dct.items():
|
|
539
694
|
if key not in self.children:
|
|
540
695
|
error = UnknownSettingError(f"Tried to set {key} to {value}, but no such node exists in {self.name}.")
|
|
541
|
-
|
|
696
|
+
logger.debug(error.message)
|
|
542
697
|
if strict:
|
|
543
698
|
raise error
|
|
544
699
|
continue
|
|
545
700
|
if isinstance(value, dict) and isinstance(self[key], SettingNode):
|
|
546
701
|
self[key].set_from_dict(value)
|
|
702
|
+
elif type(value) is str:
|
|
703
|
+
self.settings[key] = Setting(
|
|
704
|
+
self.settings[key].name,
|
|
705
|
+
self.settings[key].parameter.data_type.cast(value),
|
|
706
|
+
path=self.settings[key].path,
|
|
707
|
+
)
|
|
547
708
|
else:
|
|
548
|
-
self[key] = self[key].
|
|
709
|
+
self.settings[key] = self.settings[key].update(value)
|
|
549
710
|
|
|
550
711
|
def setting_with_path_name(self, setting: Setting) -> Setting:
|
|
551
|
-
"""Get
|
|
552
|
-
|
|
553
|
-
The path is defined as the sequence of attribute keys leading to the node with ``setting`` in it.
|
|
554
|
-
The method is used for conveniently saving settings tree observations with their settings tree path
|
|
555
|
-
as the ``dut_field``.
|
|
556
|
-
|
|
557
|
-
Args:
|
|
558
|
-
setting: Setting to search for.
|
|
559
|
-
|
|
560
|
-
Returns:
|
|
561
|
-
Copy of Setting ``setting`` where the parameter name is replaced with its path in ``self``.
|
|
562
|
-
|
|
563
|
-
Raises:
|
|
564
|
-
UnknownSettingError: If ``name`` is not found within ``self``.
|
|
565
|
-
|
|
566
|
-
"""
|
|
567
|
-
|
|
568
|
-
def _search(settings: SettingNode, name: str, prefix: str) -> tuple[str, Setting] | tuple[None, None]:
|
|
569
|
-
for key, value in settings.children.items():
|
|
570
|
-
path_prefix = f"{prefix}.{key}" if prefix else key
|
|
571
|
-
if isinstance(value, Setting) and value.name == setting.name:
|
|
572
|
-
return path_prefix, value
|
|
573
|
-
if isinstance(value, SettingNode) and value.find_by_name(setting.name):
|
|
574
|
-
return _search(value, setting, path_prefix)
|
|
575
|
-
return None, None
|
|
576
|
-
|
|
577
|
-
path, found_setting = _search(self, setting, "")
|
|
578
|
-
if path is None:
|
|
579
|
-
raise UnknownSettingError(f'{name} not found inside {self.__class__.__name__} "{self.name}".') # noqa: F821
|
|
580
|
-
param = found_setting.parameter
|
|
581
|
-
return Setting(
|
|
582
|
-
Parameter(
|
|
583
|
-
name=path,
|
|
584
|
-
label=param.label,
|
|
585
|
-
unit=param.unit,
|
|
586
|
-
data_type=param.data_type,
|
|
587
|
-
collection_type=param.collection_type,
|
|
588
|
-
element_indices=param.element_indices,
|
|
589
|
-
),
|
|
590
|
-
found_setting.value,
|
|
591
|
-
)
|
|
712
|
+
"""Get a copy of a setting with its name replaced with the path name."""
|
|
713
|
+
return self.find_by_name(setting.name).with_path_name()
|
|
592
714
|
|
|
593
715
|
def diff(self, other: SettingNode, *, path: str = "") -> list[str]:
|
|
594
716
|
"""Recursive diff between two SettingNodes.
|
|
@@ -637,9 +759,9 @@ class SettingNode:
|
|
|
637
759
|
node_diff.append(f"node name: {self.name}/{other.name}")
|
|
638
760
|
|
|
639
761
|
# compare settings
|
|
640
|
-
b_keys = set(other.
|
|
641
|
-
for key, a_setting in self.
|
|
642
|
-
b_setting = other.
|
|
762
|
+
b_keys = set(other.settings)
|
|
763
|
+
for key, a_setting in self.settings.items():
|
|
764
|
+
b_setting = other.settings.get(key)
|
|
643
765
|
if b_setting is None:
|
|
644
766
|
node_diff.append(f"-setting: {key}")
|
|
645
767
|
continue
|
|
@@ -647,21 +769,21 @@ class SettingNode:
|
|
|
647
769
|
if a_setting != b_setting:
|
|
648
770
|
node_diff.append(diff_settings(a_setting, b_setting, key))
|
|
649
771
|
for key in b_keys:
|
|
650
|
-
if key not in self.
|
|
772
|
+
if key not in self.settings:
|
|
651
773
|
node_diff.append(f"+setting: {key}")
|
|
652
774
|
|
|
653
775
|
# compare subnodes
|
|
654
776
|
diff_subnodes: list[tuple[SettingNode, SettingNode, str]] = []
|
|
655
|
-
b_keys = set(other.
|
|
656
|
-
for key, a_sub in self.
|
|
657
|
-
b_sub = other.
|
|
777
|
+
b_keys = set(other.subtrees)
|
|
778
|
+
for key, a_sub in self.subtrees.items():
|
|
779
|
+
b_sub = other.subtrees.get(key)
|
|
658
780
|
if b_sub is None:
|
|
659
781
|
node_diff.append(f"-subnode: {key}")
|
|
660
782
|
else:
|
|
661
783
|
b_keys.remove(key)
|
|
662
784
|
diff_subnodes.append((a_sub, b_sub, key))
|
|
663
785
|
for key in b_keys:
|
|
664
|
-
if key not in self.
|
|
786
|
+
if key not in self.subtrees:
|
|
665
787
|
node_diff.append(f"+subnode: {key}")
|
|
666
788
|
|
|
667
789
|
# add path prefixes
|
|
@@ -704,3 +826,227 @@ class SettingNode:
|
|
|
704
826
|
jenv = jinja2.Environment(loader=jinja2.FileSystemLoader(tmpl_path), auto_reload=True)
|
|
705
827
|
|
|
706
828
|
return jenv.get_template("settingnode_v2.html.jinja2").render(s=self, withsi=self._withsiprefix, startopen=0)
|
|
829
|
+
|
|
830
|
+
def get_node_for_path(self, path: str) -> Setting | SettingNode:
|
|
831
|
+
"""Return the node corresponding to the given path.
|
|
832
|
+
|
|
833
|
+
Args:
|
|
834
|
+
path: The path.
|
|
835
|
+
|
|
836
|
+
Returns:
|
|
837
|
+
The node at ``path`` in self.
|
|
838
|
+
|
|
839
|
+
Raises:
|
|
840
|
+
ValueError: If the given path cannot be found in self.
|
|
841
|
+
|
|
842
|
+
"""
|
|
843
|
+
keys = path.split(".")
|
|
844
|
+
node = self
|
|
845
|
+
for index, key in enumerate(keys, start=1):
|
|
846
|
+
if index < len(keys) and key in node.settings:
|
|
847
|
+
raise ValueError(f"Path '{path}' is invalid: '{key}' is a setting, not a node.")
|
|
848
|
+
if key not in node.children:
|
|
849
|
+
raise KeyError(f"Path '{path}' is invalid: key '{key}' is not found in the preceding node {node}.")
|
|
850
|
+
node = node[key]
|
|
851
|
+
return node
|
|
852
|
+
|
|
853
|
+
def add_for_path(
|
|
854
|
+
self,
|
|
855
|
+
nodes: Iterable[Setting | Parameter | SettingNode] | dict[str, Setting | Parameter | SettingNode],
|
|
856
|
+
path: str,
|
|
857
|
+
override_values: dict[str, Any] | None = None,
|
|
858
|
+
) -> None:
|
|
859
|
+
"""Add nodes to ``self`` while creating the missing nodes in-between.
|
|
860
|
+
|
|
861
|
+
Whether the names and paths are aligned is determined by the attribute ``align_name`` of the current node
|
|
862
|
+
(``self``). All the created missing nodes will use this same ``align_name`` value,
|
|
863
|
+
which determines whether their names will align with their paths.
|
|
864
|
+
|
|
865
|
+
Args:
|
|
866
|
+
nodes: Nodes to add as new leaves/branches of ``path``. If of type ``dict``, maps the keys used in
|
|
867
|
+
``self.settings`` or ``self.subtrees`` to the nodes themselves. If ``align_name=False``, the key and
|
|
868
|
+
the node name can differ, but otherwise the names will be replaced by the path anyways).
|
|
869
|
+
path: Path in ``self`` to which ``nodes`` will be added. If the path or any part (suffix) of it is not
|
|
870
|
+
found in self, the associated nodes will be created automatically.
|
|
871
|
+
override_values: Optionally override the values for the `Settings` corresponding to ``nodes``. This dict
|
|
872
|
+
should have the same structure as ``nodes``, including matching names.
|
|
873
|
+
|
|
874
|
+
"""
|
|
875
|
+
override_values = override_values or {}
|
|
876
|
+
path_split = path.split(".")
|
|
877
|
+
# find the depth already found in self
|
|
878
|
+
latest_node = self
|
|
879
|
+
levels_to_add = []
|
|
880
|
+
for idx, fragment in enumerate(path_split):
|
|
881
|
+
if fragment in latest_node.children:
|
|
882
|
+
latest_node = latest_node[fragment]
|
|
883
|
+
else:
|
|
884
|
+
levels_to_add = path_split[idx:]
|
|
885
|
+
break
|
|
886
|
+
# add the missing levels
|
|
887
|
+
for fragment in levels_to_add:
|
|
888
|
+
latest_node[fragment] = SettingNode(name=fragment, align_name=latest_node.align_name)
|
|
889
|
+
latest_node = latest_node[fragment]
|
|
890
|
+
# finally add the nodes
|
|
891
|
+
nodes_to_add = nodes.values() if isinstance(nodes, dict) else nodes
|
|
892
|
+
nodes_keys = list(nodes.keys()) if isinstance(nodes, dict) else []
|
|
893
|
+
for idx, node in enumerate(nodes_to_add):
|
|
894
|
+
key = nodes_keys[idx] if isinstance(nodes, dict) else node.name.split(".")[-1]
|
|
895
|
+
if isinstance(node, SettingNode):
|
|
896
|
+
latest_node[key] = node
|
|
897
|
+
else:
|
|
898
|
+
default_value = node.value if isinstance(node, Setting) else None
|
|
899
|
+
parameter = node.parameter if isinstance(node, Setting) else node
|
|
900
|
+
value = override_values.get(node.name) if override_values.get(node.name) is not None else default_value
|
|
901
|
+
latest_node[key] = Setting(parameter, value)
|
|
902
|
+
|
|
903
|
+
def get_default_implementation_name(self, gate: str, locus: str | Iterable[str]) -> str:
|
|
904
|
+
"""Get the default implementation name for a given gate and locus.
|
|
905
|
+
|
|
906
|
+
Takes into account the global default implementation and a possible locus specific implementation and also
|
|
907
|
+
the symmetry properties of the gate.
|
|
908
|
+
|
|
909
|
+
NOTE: using this method requires the standard EXA settings tree structure.
|
|
910
|
+
|
|
911
|
+
Args:
|
|
912
|
+
gate: The name of the gate.
|
|
913
|
+
locus: Individual qubits, couplers, or combinations.
|
|
914
|
+
|
|
915
|
+
Returns:
|
|
916
|
+
The default implementation name.
|
|
917
|
+
|
|
918
|
+
"""
|
|
919
|
+
gate_settings = self.gate_definitions[gate]
|
|
920
|
+
for impl in gate_settings.children:
|
|
921
|
+
if (
|
|
922
|
+
isinstance(gate_settings[impl], SettingNode)
|
|
923
|
+
and impl not in ("symmetric", "default_implementation") # sanity check
|
|
924
|
+
and "override_default_for_loci" in gate_settings[impl].children # backwards compatibility
|
|
925
|
+
and gate_settings[impl].override_default_for_loci.value
|
|
926
|
+
):
|
|
927
|
+
if isinstance(locus, str):
|
|
928
|
+
locus = locus.split("__")
|
|
929
|
+
if gate_settings.symmetric.value:
|
|
930
|
+
loci = list(permutations(locus))
|
|
931
|
+
else:
|
|
932
|
+
loci = [tuple(locus)]
|
|
933
|
+
for permuted_locus in loci:
|
|
934
|
+
locus_str = "__".join(permuted_locus)
|
|
935
|
+
if locus_str in gate_settings[impl].override_default_for_loci.value:
|
|
936
|
+
return impl
|
|
937
|
+
return gate_settings.default_implementation.value
|
|
938
|
+
|
|
939
|
+
def get_gate_node_for_locus(
|
|
940
|
+
self,
|
|
941
|
+
gate: str,
|
|
942
|
+
locus: str | Iterable[str],
|
|
943
|
+
implementation: str | None = None,
|
|
944
|
+
) -> SettingNode:
|
|
945
|
+
"""Get the gate calibration sub-node for the locus given as a parameter if it exists in the settings tree.
|
|
946
|
+
|
|
947
|
+
NOTE: using this method requires the standard EXA settings tree structure.
|
|
948
|
+
|
|
949
|
+
Args:
|
|
950
|
+
gate: The gate to retrieve the settings for.
|
|
951
|
+
locus: Individual qubits, couplers, or combinations.
|
|
952
|
+
implementation: Using a custom rather than the default gate implementation.
|
|
953
|
+
|
|
954
|
+
Returns:
|
|
955
|
+
The settings of the specified locus and gate.
|
|
956
|
+
|
|
957
|
+
"""
|
|
958
|
+
pulse_settings = self["gates"] if "gates" in self.children else self
|
|
959
|
+
|
|
960
|
+
if gate not in pulse_settings.children:
|
|
961
|
+
raise ValueError(f"Gate {gate} cannot be found in the pulse settings.")
|
|
962
|
+
|
|
963
|
+
if not implementation:
|
|
964
|
+
implementation = self.get_default_implementation_name(gate, locus)
|
|
965
|
+
|
|
966
|
+
if implementation not in pulse_settings[gate].children:
|
|
967
|
+
raise ValueError(f"Gate implementation {implementation} cannot be found in the pulse settings.")
|
|
968
|
+
|
|
969
|
+
str_loci = self._get_symmetric_loci(gate, implementation, locus)
|
|
970
|
+
|
|
971
|
+
for str_locus in str_loci:
|
|
972
|
+
if str_locus in pulse_settings[gate][implementation].children:
|
|
973
|
+
return pulse_settings[gate][implementation][str_locus]
|
|
974
|
+
|
|
975
|
+
raise ValueError(f"Locus {locus} cannot be found in the pulse settings.")
|
|
976
|
+
|
|
977
|
+
def get_locus_node_paths_for(self, gate: str, implementations: list[str] | None = None) -> list[str]:
|
|
978
|
+
"""Get all the gate locus node paths for a given ``gate``.
|
|
979
|
+
|
|
980
|
+
NOTE: using this method requires the standard EXA settings tree structure.
|
|
981
|
+
|
|
982
|
+
Args:
|
|
983
|
+
gate: Gate name.
|
|
984
|
+
implementations: optionally limit the paths by these gate implementations.
|
|
985
|
+
|
|
986
|
+
Returns:
|
|
987
|
+
The locus node (string) paths corresponding to this gate.
|
|
988
|
+
|
|
989
|
+
"""
|
|
990
|
+
node_paths = []
|
|
991
|
+
if "gates" not in self.children or gate not in self.gates.children:
|
|
992
|
+
return node_paths
|
|
993
|
+
if implementations is not None:
|
|
994
|
+
impls = [i for i in self.gates[gate].children if i in implementations]
|
|
995
|
+
else:
|
|
996
|
+
impls = self.gates[gate].children
|
|
997
|
+
for impl_name in impls:
|
|
998
|
+
if isinstance(self.gates[gate][impl_name], SettingNode):
|
|
999
|
+
for locus in self.gates[gate][impl_name].children:
|
|
1000
|
+
if isinstance(self.gates[gate][impl_name][locus], SettingNode):
|
|
1001
|
+
node_paths.append(f"{gate}.{impl_name}.{locus}")
|
|
1002
|
+
return node_paths
|
|
1003
|
+
|
|
1004
|
+
def get_gate_properties_for_locus(
|
|
1005
|
+
self, gate: str, locus: str | Iterable[str], implementation: str | None = None
|
|
1006
|
+
) -> SettingNode:
|
|
1007
|
+
"""Get the gate characterization sub-node for the locus given as a parameter if it exists in the settings tree.
|
|
1008
|
+
|
|
1009
|
+
NOTE: using this method requires the standard EXA settings tree structure.
|
|
1010
|
+
|
|
1011
|
+
Args:
|
|
1012
|
+
gate: The gate to retrieve the settings for.
|
|
1013
|
+
locus: Individual qubits, couplers, or combinations.
|
|
1014
|
+
implementation: Using a custom rather than the default gate implementation.
|
|
1015
|
+
|
|
1016
|
+
Returns:
|
|
1017
|
+
The settings of the specified locus and gate.
|
|
1018
|
+
|
|
1019
|
+
"""
|
|
1020
|
+
if not implementation:
|
|
1021
|
+
implementation = self.get_default_implementation_name(gate, locus)
|
|
1022
|
+
gate_properties = self.characterization.gate_properties
|
|
1023
|
+
if gate not in gate_properties.children:
|
|
1024
|
+
raise ValueError(f"Gate {gate} cannot be found in the characterization settings.")
|
|
1025
|
+
if implementation not in gate_properties[gate].children:
|
|
1026
|
+
raise ValueError(
|
|
1027
|
+
f"Implementation {implementation} of Gate {gate} cannot be found in the characterization settings."
|
|
1028
|
+
)
|
|
1029
|
+
|
|
1030
|
+
str_loci = self._get_symmetric_loci(gate, implementation, locus)
|
|
1031
|
+
for str_locus in str_loci:
|
|
1032
|
+
if str_locus in gate_properties[gate][implementation].children:
|
|
1033
|
+
return gate_properties[gate][implementation][str_locus]
|
|
1034
|
+
|
|
1035
|
+
raise ValueError(f"Locus {locus} cannot be found in the gate properties characterization settings.")
|
|
1036
|
+
|
|
1037
|
+
def _get_symmetric_loci(self, gate: str, implementation: str, locus: str) -> list[str]:
|
|
1038
|
+
if not isinstance(locus, str):
|
|
1039
|
+
if self.gate_definitions[gate][implementation].symmetric.value:
|
|
1040
|
+
str_loci = ["__".join(sort_components(locus))]
|
|
1041
|
+
elif self.gate_definitions[gate].symmetric.value:
|
|
1042
|
+
str_loci = ["__".join(item) for item in list(permutations(locus))]
|
|
1043
|
+
else:
|
|
1044
|
+
str_loci = ["__".join(locus)]
|
|
1045
|
+
else:
|
|
1046
|
+
str_loci = [locus]
|
|
1047
|
+
return str_loci
|
|
1048
|
+
|
|
1049
|
+
def _get_path(self, key) -> str:
|
|
1050
|
+
if not self.path:
|
|
1051
|
+
return key
|
|
1052
|
+
return f"{self.path}.{key}"
|