iqm-exa-common 26.25.0__tar.gz → 26.27.0__tar.gz

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.
Files changed (81) hide show
  1. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/CHANGELOG.rst +16 -0
  2. {iqm_exa_common-26.25.0/src/iqm_exa_common.egg-info → iqm_exa_common-26.27.0}/PKG-INFO +1 -1
  3. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/control/sweep/option/center_span_options.py +1 -1
  4. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/control/sweep/option/start_stop_base_options.py +5 -9
  5. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/control/sweep/option/start_stop_options.py +1 -1
  6. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/control/sweep/sweep_values.py +3 -4
  7. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/data/base_model.py +2 -1
  8. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/data/parameter.py +49 -27
  9. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/data/setting_node.py +48 -23
  10. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/helpers/data_helper.py +4 -2
  11. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/helpers/software_version_helper.py +1 -1
  12. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/qcm_data/chad_model.py +3 -2
  13. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/qcm_data/chip_topology.py +7 -7
  14. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/qcm_data/qcm_data_client.py +1 -1
  15. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0/src/iqm_exa_common.egg-info}/PKG-INFO +1 -1
  16. iqm_exa_common-26.27.0/version.txt +1 -0
  17. iqm_exa_common-26.25.0/version.txt +0 -1
  18. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/LICENSE.txt +0 -0
  19. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/MANIFEST.in +0 -0
  20. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/README.rst +0 -0
  21. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/docs/API.rst +0 -0
  22. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/docs/Makefile +0 -0
  23. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/docs/_static/.gitignore +0 -0
  24. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/docs/_static/css/custom.css +0 -0
  25. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/docs/_static/images/favicon.ico +0 -0
  26. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/docs/_static/images/logo.png +0 -0
  27. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/docs/_templates/autosummary-class-template.rst +0 -0
  28. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/docs/_templates/autosummary-module-template.rst +0 -0
  29. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/docs/changelog.rst +0 -0
  30. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/docs/conf.py +0 -0
  31. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/docs/index.rst +0 -0
  32. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/docs/license.rst +0 -0
  33. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/pyproject.toml +0 -0
  34. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/requirements/base.in +0 -0
  35. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/requirements/base.txt +0 -0
  36. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/setup.cfg +0 -0
  37. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/setup.py +0 -0
  38. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/__init__.py +0 -0
  39. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/api/__init__.py +0 -0
  40. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/api/proto_serialization/__init__.py +0 -0
  41. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/api/proto_serialization/_parameter.py +0 -0
  42. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/api/proto_serialization/array.py +0 -0
  43. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/api/proto_serialization/datum.py +0 -0
  44. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/api/proto_serialization/nd_sweep.py +0 -0
  45. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/api/proto_serialization/sequence.py +0 -0
  46. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/api/proto_serialization/setting_node.py +0 -0
  47. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/control/__init__.py +0 -0
  48. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/control/sweep/__init__.py +0 -0
  49. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/control/sweep/exponential_sweep.py +0 -0
  50. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/control/sweep/fixed_sweep.py +0 -0
  51. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/control/sweep/linear_sweep.py +0 -0
  52. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/control/sweep/option/__init__.py +0 -0
  53. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/control/sweep/option/center_span_base_options.py +0 -0
  54. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/control/sweep/option/constants.py +0 -0
  55. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/control/sweep/option/fixed_options.py +0 -0
  56. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/control/sweep/option/option_converter.py +0 -0
  57. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/control/sweep/option/sweep_options.py +0 -0
  58. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/control/sweep/sweep.py +0 -0
  59. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/data/__init__.py +0 -0
  60. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/data/settingnode_v2.html.jinja2 +0 -0
  61. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/data/value.py +0 -0
  62. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/errors/__init__.py +0 -0
  63. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/errors/exa_error.py +0 -0
  64. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/errors/station_control_errors.py +0 -0
  65. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/helpers/__init__.py +0 -0
  66. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/helpers/deprecation.py +0 -0
  67. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/helpers/json_helper.py +0 -0
  68. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/helpers/numpy_helper.py +0 -0
  69. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/helpers/yaml_helper.py +0 -0
  70. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/logger/__init__.py +0 -0
  71. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/logger/logger.py +0 -0
  72. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/qcm_data/__init__.py +0 -0
  73. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/qcm_data/file_adapter.py +0 -0
  74. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/qcm_data/immutable_base_model.py +0 -0
  75. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/sweep/__init__.py +0 -0
  76. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/sweep/database_serialization.py +0 -0
  77. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/exa/common/sweep/util.py +0 -0
  78. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/iqm_exa_common.egg-info/SOURCES.txt +0 -0
  79. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/iqm_exa_common.egg-info/dependency_links.txt +0 -0
  80. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/iqm_exa_common.egg-info/requires.txt +0 -0
  81. {iqm_exa_common-26.25.0 → iqm_exa_common-26.27.0}/src/iqm_exa_common.egg-info/top_level.txt +0 -0
@@ -2,6 +2,22 @@
2
2
  Changelog
3
3
  =========
4
4
 
5
+ Version 26.27.0 (2025-07-04)
6
+ ============================
7
+
8
+ Features
9
+ --------
10
+
11
+ - Setting now has a private field _source and an associated property which can be used to store e.g. the observation that is the source of the Setting's value.
12
+
13
+ Version 26.26.0 (2025-07-02)
14
+ ============================
15
+
16
+ Bug fixes
17
+ ---------
18
+
19
+ - Fix type errors raised by mypy.
20
+
5
21
  Version 26.25.0 (2025-06-17)
6
22
  ============================
7
23
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: iqm-exa-common
3
- Version: 26.25.0
3
+ Version: 26.27.0
4
4
  Summary: Framework for control and measurement of superconducting qubits: common library
5
5
  Author-email: IQM Finland Oy <info@meetiqm.com>
6
6
  License: Apache License
@@ -40,7 +40,7 @@ class CenterSpanOptions(SweepOptions):
40
40
  #: :const:`exa.common.control.sweep.option.constants.DEFAULT_COUNT`.
41
41
  count: int | None = None
42
42
  #: Size of spacing between values.
43
- step: int | float | complex = None
43
+ step: int | float | complex | None = None
44
44
  #: Order of generated values. Default to ascending
45
45
  asc: bool | None = None
46
46
 
@@ -35,26 +35,22 @@ class StartStopBaseOptions(SweepOptions):
35
35
  """
36
36
 
37
37
  #: The power for the start of the interval.
38
- start: int | float | complex
38
+ start: int | float
39
39
  #: The power for the end of the interval.
40
- stop: int | float | complex
40
+ stop: int | float
41
41
  #: Number of values to generate. Default to
42
42
  #: :const:`exa.common.control.sweep.option.constants.DEFAULT_COUNT`.
43
- count: int | None = None
43
+ count: int = DEFAULT_COUNT
44
44
  #: Number, that is raised to the power `start` or `stop`. Default to
45
45
  #: :const:`exa.common.control.sweep.option.constants.DEFAULT_BASE`.
46
- base: int | float | None = None
46
+ base: int = DEFAULT_BASE
47
47
 
48
48
  def __post_init__(self):
49
- if self.count is None:
50
- object.__setattr__(self, "count", DEFAULT_COUNT)
51
- if self.base is None:
52
- object.__setattr__(self, "base", DEFAULT_BASE)
53
49
  if self.start == 0 or self.stop == 0:
54
50
  raise ValueError("Exponential range sweep start and stop values must not be zero.")
55
51
 
56
52
  @property
57
- def data(self) -> list[int | float | complex]:
53
+ def data(self) -> list[int | float]:
58
54
  logger.debug(f"EXPONENTS: ({self.start}, {self.stop}) with base {self.base}")
59
55
  start = math.pow(self.base, self.start)
60
56
  stop = math.pow(self.base, self.stop)
@@ -65,7 +65,7 @@ class StartStopOptions(SweepOptions):
65
65
  count = 1 + math.ceil(abs(self.stop - self.start) / float(np.abs(self.step)))
66
66
  data = self._generate_by_count(count)
67
67
  else:
68
- data = self._generate_by_count(self.count)
68
+ data = self._generate_by_count(self.count if self.count is not None else DEFAULT_COUNT)
69
69
  return data
70
70
 
71
71
  def _generate_by_count(self, count: int) -> SweepValues:
@@ -28,9 +28,8 @@ def validate_sweep_values(sweep_values: Any) -> Any:
28
28
  if isinstance(sweep_values, np.ndarray):
29
29
  sweep_values = sweep_values.tolist()
30
30
  for index, value in enumerate(sweep_values):
31
- if isinstance(value, dict):
32
- if "__complex__" in value:
33
- sweep_values[index] = complex(value["real"], value["imag"])
31
+ if isinstance(value, dict) and "__complex__" in value:
32
+ sweep_values[index] = complex(value["real"], value["imag"])
34
33
  return sweep_values
35
34
 
36
35
 
@@ -51,7 +50,7 @@ def serialize_sweep_values(sweep_values: Any) -> Any:
51
50
 
52
51
 
53
52
  SweepValues = Annotated[
54
- list[Any] | np.ndarray[Any],
53
+ list[Any] | np.ndarray,
55
54
  PlainValidator(validate_sweep_values),
56
55
  PlainSerializer(serialize_sweep_values),
57
56
  WithJsonSchema(core_schema.any_schema()),
@@ -1,3 +1,4 @@
1
+ from collections.abc import Mapping
1
2
  from typing import Any, Self
2
3
 
3
4
  import pydantic
@@ -24,7 +25,7 @@ class BaseModel(pydantic.BaseModel):
24
25
  frozen=True, # This makes instances of the model potentially hashable if all the attributes are hashable
25
26
  )
26
27
 
27
- def model_copy(self, *, update: dict[str, Any] | None = None, deep: bool = True) -> Self:
28
+ def model_copy(self, *, update: Mapping[str, Any] | None = None, deep: bool = True) -> Self:
28
29
  """Returns a copy of the model.
29
30
 
30
31
  Overrides the Pydantic default 'model_copy' to set 'deep=True' by default.
@@ -61,7 +61,7 @@ import ast
61
61
  from collections.abc import Hashable
62
62
  import copy
63
63
  from enum import IntEnum
64
- from typing import Any, Self
64
+ from typing import Any, Self, TypeAlias
65
65
  import warnings
66
66
 
67
67
  import numpy as np
@@ -73,7 +73,9 @@ from exa.common.data.base_model import BaseModel
73
73
  from exa.common.data.value import ObservationValue
74
74
  from exa.common.errors.station_control_errors import ValidationError
75
75
 
76
- CastType = str | list["CastType"] | None
76
+ CastType: TypeAlias = str | list["CastType"] | None
77
+ SourceType: TypeAlias = None | BaseModel | dict[str, Any]
78
+ """Type for Setting sources."""
77
79
 
78
80
 
79
81
  class DataType(IntEnum):
@@ -116,7 +118,7 @@ class DataType(IntEnum):
116
118
  else:
117
119
  return False
118
120
 
119
- def _cast(self, value: str) -> Any: # noqa: PLR0911
121
+ def _cast(self, value: str | None) -> Any: # noqa: PLR0911
120
122
  if value is None:
121
123
  return None
122
124
  elif self in [DataType.FLOAT, DataType.NUMBER]:
@@ -135,7 +137,7 @@ class DataType(IntEnum):
135
137
  raise TypeError("Boolean data types can only be 'false', 'true, '0' or '1' (case-insensitive)")
136
138
  elif self is DataType.STRING:
137
139
  return value
138
- else:
140
+ else: # TODO: can this be removed?
139
141
  try:
140
142
  return ast.literal_eval(value)
141
143
  except (SyntaxError, ValueError): # if the value can not be evaluated, return the original value
@@ -238,13 +240,12 @@ class Parameter(BaseModel):
238
240
  raise ValidationError("Parameter 'element_indices' must be one or more ints.")
239
241
  object.__setattr__(self, "element_indices", idxs)
240
242
  # there may be len(idxs) num of "__" separated indices at the end, remove those to get the parent name
241
- parent_name = self.name.replace("__" + "__".join([str(idx) for idx in idxs]), "")
243
+ seperated_indices = "__".join([str(idx) for idx in idxs])
244
+ parent_name = self.name.replace("__" + seperated_indices, "")
242
245
  object.__setattr__(self, "_parent_name", parent_name)
243
246
  object.__setattr__(self, "_parent_label", self.label.replace(f" {idxs}", ""))
244
247
  object.__setattr__(self, "label", f"{self._parent_label} {idxs}")
245
- name = self._parent_name
246
- for index in idxs:
247
- name += f"__{index}"
248
+ name = parent_name + "__" + seperated_indices
248
249
  object.__setattr__(self, "name", name)
249
250
 
250
251
  @property
@@ -272,7 +273,7 @@ class Parameter(BaseModel):
272
273
  def build_data_set(
273
274
  variables: list[tuple[Parameter, list[Any]]],
274
275
  data: tuple[Parameter, SweepValues],
275
- attributes: dict[str, Any] = None,
276
+ attributes: dict[str, Any] | None = None,
276
277
  extra_variables: list[tuple[str, int]] | None = None,
277
278
  ):
278
279
  """Build an xarray Dataset, where the only DataArray is given by `results` and coordinates are given by
@@ -289,9 +290,9 @@ class Parameter(BaseModel):
289
290
  extra_variables: Valueless dimensions and their sizes.
290
291
 
291
292
  """
292
- variable_names = []
293
- variable_sizes = []
294
- variable_data_arrays = {}
293
+ variable_names: list[str] = []
294
+ variable_sizes: list[int] = []
295
+ variable_data_arrays: dict[str, xr.DataArray] = {}
295
296
  for variable in variables:
296
297
  variable_names.append(variable[0].name)
297
298
  variable_sizes.append(len(variable[1]))
@@ -326,9 +327,9 @@ class Parameter(BaseModel):
326
327
  def build_data_array(
327
328
  self,
328
329
  data: np.ndarray,
329
- dimensions: list[Hashable] = None,
330
- coords: dict[Hashable, Any] = None,
331
- metadata: dict[str, Any] = None,
330
+ dimensions: list[str] | list[Hashable] | None = None,
331
+ coords: dict[Hashable, Any] | None = None,
332
+ metadata: dict[str, Any] | None = None,
332
333
  ) -> xr.DataArray:
333
334
  """Attach Parameter information to a numerical array.
334
335
 
@@ -365,7 +366,7 @@ class Parameter(BaseModel):
365
366
  da = xr.DataArray(name=self.name, data=data, attrs=attrs, dims=dimensions, coords=coords)
366
367
  # copying the coordinate metadata, if present, to the new DataArray coordinates
367
368
  if coords:
368
- for key in [k for k in coords.keys() if isinstance(coords[k], xr.DataArray)]:
369
+ for key in [k for k in coords if isinstance(coords[k], xr.DataArray)]:
369
370
  da[key].attrs = coords[key].attrs
370
371
  return da
371
372
 
@@ -408,12 +409,19 @@ class Setting(BaseModel):
408
409
  path: str = ""
409
410
  """Path in the settings tree (starting from the root ``SettingNode``) for this setting."""
410
411
 
412
+ _source: SourceType = None
413
+ """The source for this Setting value. May contain an observation (ObservationDefinition or ObservationData)
414
+ or a source-dict (e.g. ``{"type": "configuration_source", "configurator": "defaults_from_yml"}``). By default,
415
+ ``None``, which denotes the source not being specified (e.g. hardcoded defaults). The source is stored in a private
416
+ attribute and thus is never serialized (the source field can contain non-serializable data such as Callables)."""
417
+
411
418
  def __init__(
412
419
  self,
413
420
  parameter: Parameter | None = None,
414
421
  value: ObservationValue | None = None,
415
422
  read_only: bool = False,
416
423
  path: str = "",
424
+ source: SourceType = None,
417
425
  **kwargs,
418
426
  ) -> None:
419
427
  super().__init__(
@@ -423,6 +431,7 @@ class Setting(BaseModel):
423
431
  path=path,
424
432
  **kwargs,
425
433
  )
434
+ self._source = source
426
435
 
427
436
  @model_validator(mode="after")
428
437
  def validate_parameter_value_after(self) -> Self:
@@ -436,8 +445,17 @@ class Setting(BaseModel):
436
445
  raise ValidationError(f"Invalid value '{self.value}' for parameter '{self.parameter}'.")
437
446
  return self
438
447
 
439
- def update(self, value: ObservationValue) -> Setting:
440
- """Create a new setting object with updated `value`."""
448
+ def update(self, value: ObservationValue, source: SourceType = None) -> Setting:
449
+ """Create a new setting object with updated value and source.
450
+
451
+ Args:
452
+ value: New value for the setting.
453
+ source: New source for the setting.
454
+
455
+ Returns:
456
+ Copy of ``self`` with modified properties.
457
+
458
+ """
441
459
  if self.read_only:
442
460
  raise ValueError(
443
461
  f"Can't update the value of {self.parameter.name} to {value} since the setting is read-only."
@@ -446,7 +464,7 @@ class Setting(BaseModel):
446
464
  value = np.array(value)
447
465
  # Need to create a new Setting here instead of using Pydantic model_copy().
448
466
  # model_copy() can't handle backdoor settings without errors, i.e. values with a list of 2 elements.
449
- return Setting(self.parameter, value, self.read_only, self.path)
467
+ return Setting(self.parameter, value, self.read_only, self.path, source=source)
450
468
 
451
469
  @property
452
470
  def name(self):
@@ -474,24 +492,28 @@ class Setting(BaseModel):
474
492
  return self.parameter.unit
475
493
 
476
494
  @property
477
- def element_indices(self) -> tuple[int, ...] | None:
495
+ def element_indices(self) -> int | list[int] | None:
478
496
  """Element-wise indices of the parameter in ``self``."""
479
497
  return self.parameter.element_indices
480
498
 
499
+ @property
500
+ def source(self) -> SourceType:
501
+ """Return the source for this Setting's value."""
502
+ return self._source
503
+
481
504
  @staticmethod
482
505
  def get_by_name(name: str, values: set[Setting]) -> Setting | None:
483
506
  return next((setting for setting in values if setting.parameter.name == name), None)
484
507
 
485
508
  @staticmethod
486
- def remove_by_name(name: str, values: set[Setting] = None) -> set[Setting]:
487
- if values is None:
488
- values = set()
509
+ def remove_by_name(name: str, values: set[Setting]) -> set[Setting]:
489
510
  removed = copy.deepcopy(values)
490
- removed.discard(Setting.get_by_name(name, values))
511
+ if setting := Setting.get_by_name(name, values):
512
+ removed.discard(setting)
491
513
  return removed
492
514
 
493
515
  @staticmethod
494
- def replace(settings: Setting | list[Setting], values: set[Setting] = None) -> set[Setting]:
516
+ def replace(settings: Setting | list[Setting], values: set[Setting] | None = None) -> set[Setting]:
495
517
  if values is None:
496
518
  values = set()
497
519
  if not isinstance(settings, list):
@@ -518,7 +540,7 @@ class Setting(BaseModel):
518
540
  diff = first.difference(second)
519
541
  for s in first.intersection(second):
520
542
  a, b = [Setting.get_by_name(s.parameter.name, group) for group in [first, second]]
521
- if a.value != b.value:
543
+ if a is not None and b is not None and a.value != b.value:
522
544
  diff.add(a)
523
545
  return diff
524
546
 
@@ -555,7 +577,7 @@ class Setting(BaseModel):
555
577
  def __hash__(self):
556
578
  return hash(self.parameter)
557
579
 
558
- def __eq__(self, other: Setting):
580
+ def __eq__(self, other: Any) -> bool:
559
581
  if not (isinstance(other, Setting) and self.parameter == other.parameter):
560
582
  return False
561
583
  if isinstance(self.value, np.ndarray):
@@ -217,19 +217,19 @@ import jinja2
217
217
  import numpy as np
218
218
 
219
219
  from exa.common.data.base_model import BaseModel
220
- from exa.common.data.parameter import CollectionType, Parameter, Setting
220
+ from exa.common.data.parameter import CollectionType, Parameter, Setting, SourceType
221
221
  from exa.common.errors.exa_error import UnknownSettingError
222
222
  from exa.common.qcm_data.chip_topology import sort_components
223
223
 
224
224
  logger = logging.getLogger(__name__)
225
225
 
226
226
 
227
- def _fix_path_recursive(node: SettingNode | dict, path: str) -> SettingNode | dict:
227
+ def _fix_path_recursive(node: SettingNode, path: str) -> SettingNode:
228
228
  """Recursively travel the settings tree and fix the ``path``attribute (also aligns ``name``,
229
229
  based on the node type). Deep copies all the child nodes.
230
230
  """
231
- settings = {}
232
- subtrees = {}
231
+ settings: dict[str, Setting] = {}
232
+ subtrees: dict[str, SettingNode] = {}
233
233
  for key, setting in node.settings.items():
234
234
  child_path = f"{path}.{key}"
235
235
  update_dict = {"path": child_path}
@@ -305,8 +305,8 @@ class SettingNode(BaseModel):
305
305
  generate_paths: bool = True,
306
306
  **kwargs,
307
307
  ):
308
- settings: dict[str, Any] = settings or {}
309
- subtrees: dict[str, Any] = subtrees or {}
308
+ settings = settings or {}
309
+ subtrees = subtrees or {}
310
310
 
311
311
  for key, child in kwargs.items():
312
312
  if isinstance(child, Setting):
@@ -603,7 +603,7 @@ class SettingNode(BaseModel):
603
603
  """
604
604
  for key, item in other.settings.items():
605
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)
606
+ self.settings[key] = Setting(self.settings[key].parameter, item.value, source=self.settings[key].source)
607
607
  for key, item in other.subtrees.items():
608
608
  if key in self.subtrees:
609
609
  self.subtrees[key].merge_values(copy(item), prioritize_other)
@@ -650,7 +650,7 @@ class SettingNode(BaseModel):
650
650
  append_lines(self, lines, [])
651
651
  print("\n", "\n".join(lines))
652
652
 
653
- def __eq__(self, other: SettingNode):
653
+ def __eq__(self, other: Any) -> bool:
654
654
  return isinstance(other, SettingNode) and (
655
655
  (self.name, self.settings, self.subtrees) == (other.name, other.settings, other.subtrees)
656
656
  )
@@ -678,16 +678,22 @@ class SettingNode(BaseModel):
678
678
  new.subtrees[key] = cls.transform_node_types(subnode)
679
679
  return new
680
680
 
681
- def set_from_dict(self, dct: dict[str, Any], strict: bool = False) -> None:
681
+ def set_from_dict(
682
+ self,
683
+ dct: dict[str, Any],
684
+ strict: bool = False,
685
+ source: SourceType = None,
686
+ ) -> None:
682
687
  """Recursively set values to Settings, taking values from a dictionary that has similar tree structure.
683
- Keys that are not found in self are ignored, unless `strict` is True.
688
+ Keys that are not found in self are ignored, unless ``strict`` is True.
684
689
 
685
690
  Args:
686
691
  dct: Dictionary containing the new values to use.
687
- strict: If True, will raise error if `dct` contains a setting that is not found in `self`.
692
+ strict: If True, will raise error if ``dct`` contains a setting that is not found in ``self``.
693
+ source: Source for the settings (this same source is applied to all settings from the dict).
688
694
 
689
695
  Raises:
690
- UnknownSettingError: If the condition of `strict` happens.
696
+ UnknownSettingError: If the condition of ``strict`` happens.
691
697
 
692
698
  """
693
699
  for key, value in dct.items():
@@ -704,13 +710,17 @@ class SettingNode(BaseModel):
704
710
  self.settings[key].name,
705
711
  self.settings[key].parameter.data_type.cast(value),
706
712
  path=self.settings[key].path,
713
+ source=source,
707
714
  )
708
715
  else:
709
- self.settings[key] = self.settings[key].update(value)
716
+ self.settings[key] = self.settings[key].update(value, source=source)
710
717
 
711
- def setting_with_path_name(self, setting: Setting) -> Setting:
718
+ def setting_with_path_name(self, setting: Setting) -> Setting | None:
712
719
  """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()
720
+ first_item = self.find_by_name(setting.name)
721
+ if isinstance(first_item, Setting):
722
+ return first_item.with_path_name()
723
+ return None
714
724
 
715
725
  def diff(self, other: SettingNode, *, path: str = "") -> list[str]:
716
726
  """Recursive diff between two SettingNodes.
@@ -855,6 +865,7 @@ class SettingNode(BaseModel):
855
865
  nodes: Iterable[Setting | Parameter | SettingNode] | dict[str, Setting | Parameter | SettingNode],
856
866
  path: str,
857
867
  override_values: dict[str, Any] | None = None,
868
+ override_source: SourceType = None,
858
869
  ) -> None:
859
870
  """Add nodes to ``self`` while creating the missing nodes in-between.
860
871
 
@@ -870,6 +881,8 @@ class SettingNode(BaseModel):
870
881
  found in self, the associated nodes will be created automatically.
871
882
  override_values: Optionally override the values for the `Settings` corresponding to ``nodes``. This dict
872
883
  should have the same structure as ``nodes``, including matching names.
884
+ override_source: Optionally override the source for the ``Settings`` corresponding to ``nodes``. All the
885
+ settings will then have this same source.
873
886
 
874
887
  """
875
888
  override_values = override_values or {}
@@ -888,7 +901,7 @@ class SettingNode(BaseModel):
888
901
  latest_node[fragment] = SettingNode(name=fragment, align_name=latest_node.align_name)
889
902
  latest_node = latest_node[fragment]
890
903
  # finally add the nodes
891
- nodes_to_add = nodes.values() if isinstance(nodes, dict) else nodes
904
+ nodes_to_add: Iterable[Setting | Parameter | SettingNode] = nodes.values() if isinstance(nodes, dict) else nodes
892
905
  nodes_keys = list(nodes.keys()) if isinstance(nodes, dict) else []
893
906
  for idx, node in enumerate(nodes_to_add):
894
907
  key = nodes_keys[idx] if isinstance(nodes, dict) else node.name.split(".")[-1]
@@ -896,9 +909,10 @@ class SettingNode(BaseModel):
896
909
  latest_node[key] = node
897
910
  else:
898
911
  default_value = node.value if isinstance(node, Setting) else None
912
+ source = override_source or (node.source if isinstance(node, Setting) else None)
899
913
  parameter = node.parameter if isinstance(node, Setting) else node
900
914
  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)
915
+ latest_node[key] = Setting(parameter, value, source=source)
902
916
 
903
917
  def get_default_implementation_name(self, gate: str, locus: str | Iterable[str]) -> str:
904
918
  """Get the default implementation name for a given gate and locus.
@@ -926,10 +940,7 @@ class SettingNode(BaseModel):
926
940
  ):
927
941
  if isinstance(locus, str):
928
942
  locus = locus.split("__")
929
- if gate_settings.symmetric.value:
930
- loci = list(permutations(locus))
931
- else:
932
- loci = [tuple(locus)]
943
+ loci = list(permutations(locus)) if gate_settings.symmetric.value else [tuple(locus)]
933
944
  for permuted_locus in loci:
934
945
  locus_str = "__".join(permuted_locus)
935
946
  if locus_str in gate_settings[impl].override_default_for_loci.value:
@@ -987,7 +998,7 @@ class SettingNode(BaseModel):
987
998
  The locus node (string) paths corresponding to this gate.
988
999
 
989
1000
  """
990
- node_paths = []
1001
+ node_paths: list[str] = []
991
1002
  if "gates" not in self.children or gate not in self.gates.children:
992
1003
  return node_paths
993
1004
  if implementations is not None:
@@ -1034,7 +1045,21 @@ class SettingNode(BaseModel):
1034
1045
 
1035
1046
  raise ValueError(f"Locus {locus} cannot be found in the gate properties characterization settings.")
1036
1047
 
1037
- def _get_symmetric_loci(self, gate: str, implementation: str, locus: str) -> list[str]:
1048
+ def set_source(self, source: SourceType, ignore_nones: bool = True) -> None:
1049
+ """Set source recursively to all Settings in ``self``.
1050
+
1051
+ Args:
1052
+ source: The source to set.
1053
+ ignore_nones: If ``True``, the source will not be set for Settings with ``None`` value.
1054
+
1055
+ """
1056
+ for setting in self.settings.values():
1057
+ if not ignore_nones or setting.value is not None:
1058
+ setting._source = source
1059
+ for subtree in self.subtrees.values():
1060
+ subtree.set_source(source, ignore_nones=ignore_nones)
1061
+
1062
+ def _get_symmetric_loci(self, gate: str, implementation: str, locus: str | Iterable[str]) -> list[str]:
1038
1063
  if not isinstance(locus, str):
1039
1064
  if self.gate_definitions[gate][implementation].symmetric.value:
1040
1065
  str_loci = ["__".join(sort_components(locus))]
@@ -13,13 +13,15 @@
13
13
  # limitations under the License.
14
14
 
15
15
 
16
+ from collections.abc import Hashable
17
+
16
18
  import xarray as xr
17
19
 
18
20
  """Helper methods for data manipulation.
19
21
  """
20
22
 
21
23
 
22
- def add_data_array(ds: xr.Dataset, da: xr.DataArray, name: str | None = None) -> xr.Dataset:
24
+ def add_data_array(ds: xr.Dataset, da: xr.DataArray, name: Hashable | None = None) -> xr.Dataset:
23
25
  """Add data array `da` to dataset `ds`.
24
26
 
25
27
  Unlike the default xarray command, preserves metadata of the dataset.
@@ -50,7 +52,7 @@ def add_data_array(ds: xr.Dataset, da: xr.DataArray, name: str | None = None) ->
50
52
  ds[name] = da
51
53
  for key in ds.coords:
52
54
  if attributes.get(key):
53
- ds.coords[key].attrs = attributes.get(key)
55
+ ds.coords[key].attrs = attributes.get(key) # type:ignore[assignment]
54
56
  for key in ds.data_vars:
55
57
  if attributes.get(key):
56
58
  ds.data_vars[key].attrs = attributes[key]
@@ -30,7 +30,7 @@ def _is_editable(pkg_name: str) -> bool:
30
30
  ``importlib.metadata``, so it might break anytime.
31
31
  """
32
32
  dist = distribution(pkg_name)
33
- return dist.files and dist.files[0].name.startswith("__editable__.")
33
+ return dist.files is not None and dist.files[0].name.startswith("__editable__.")
34
34
 
35
35
 
36
36
  def get_all_software_versions(reload_module: bool = False) -> dict[str, str]:
@@ -17,13 +17,14 @@
17
17
  from collections.abc import Collection
18
18
  from functools import cached_property
19
19
  import re
20
+ from typing import Any
20
21
 
21
22
  from pydantic import Field, field_validator
22
23
 
23
24
  from exa.common.qcm_data.immutable_base_model import ImmutableBaseModel
24
25
 
25
26
 
26
- def _natural_sort_key(name: str) -> tuple[int, ...]:
27
+ def _natural_sort_key(name: str) -> tuple[int | str | Any, ...]:
27
28
  return tuple(int(item) if item.isdigit() else item.lower() for item in re.split(r"(\d+)", name))
28
29
 
29
30
 
@@ -76,7 +77,7 @@ class Components(ImmutableBaseModel):
76
77
 
77
78
  @cached_property
78
79
  def all(self) -> dict[str, Component]:
79
- components: tuple[Qubit, Coupler, ProbeLine, Launcher, ComputationalResonator] = (
80
+ components: tuple[Qubit | Coupler | ProbeLine | Launcher | ComputationalResonator, ...] = (
80
81
  self.qubits + self.couplers + self.probe_lines + self.launchers + self.computational_resonators
81
82
  )
82
83
  return {component.name: component for component in components}
@@ -109,7 +109,7 @@ class ChipTopology:
109
109
  coupler: tuple(sort_components(components)) for coupler, components in couplers.items()
110
110
  }
111
111
  """Map from each coupler to all other components it connects to. The values are sorted."""
112
- component_to_couplers = {}
112
+ component_to_couplers: dict = {}
113
113
  for coupler, components in couplers.items():
114
114
  for c in components:
115
115
  component_to_couplers.setdefault(c, set()).add(coupler)
@@ -128,7 +128,7 @@ class ChipTopology:
128
128
  Components without connection to a probe line don't appear.
129
129
  """
130
130
 
131
- self._locus_mappings: dict[Locus, dict[tuple[str, ...]]] = {
131
+ self._locus_mappings: dict[str, dict[Locus, tuple[str, ...]]] = {
132
132
  DEFAULT_1QB_MAPPING: {(qubit,): (qubit,) for qubit in self.qubits_sorted},
133
133
  DEFAULT_2QB_MAPPING: {
134
134
  frozenset(comps): (coupler,)
@@ -244,7 +244,7 @@ class ChipTopology:
244
244
  """Get probelines that are connected to any of the given components."""
245
245
  return {self.component_to_probe_line[c] for c in components if c in self.component_to_probe_line}
246
246
 
247
- def get_connected_coupler_map(self, components: Collection[str]) -> ComponentMap:
247
+ def get_connected_coupler_map(self, components: Collection[str]) -> dict[str, tuple[str, ...]]:
248
248
  """Returns a `ComponentMap`, including only the couplers between components that both are in the given subset.
249
249
 
250
250
  Args:
@@ -261,7 +261,7 @@ class ChipTopology:
261
261
  }
262
262
 
263
263
  @staticmethod
264
- def limit_values(dct: ComponentMap, limit_to: Collection[str]) -> ComponentMap:
264
+ def limit_values(dct: ComponentMap, limit_to: Collection[str]) -> dict[str, Collection[str]]:
265
265
  """Prunes the given dictionary (e.g. a coupler-to-qubits map) to a subset of values.
266
266
 
267
267
  Used to prune e.g. :attr:`coupler_to_components` to a subset of relevant elements.
@@ -313,7 +313,7 @@ class ChipTopology:
313
313
  self._validate_locus_mapping(mapping)
314
314
  self._locus_mappings[name] = mapping
315
315
 
316
- def _validate_locus_mapping(self, mapping: dict[str | tuple[str], Locus] | None = None) -> None:
316
+ def _validate_locus_mapping(self, mapping: dict[Locus, tuple[str, ...]]) -> None:
317
317
  """Validate that the components given in mapping are found in self and the mapping is correctly formed."""
318
318
  for locus, mapped in mapping.items():
319
319
  if not isinstance(locus, tuple) and not isinstance(locus, frozenset):
@@ -325,7 +325,7 @@ class ChipTopology:
325
325
  if locus_component not in self.all_components:
326
326
  raise ValueError(f"Locus component {locus_component} is not found in this ChipTopology.")
327
327
 
328
- def map_locus(self, locus: Locus, name: str | None = None) -> str | tuple[str] | None:
328
+ def map_locus(self, locus: Locus, name: str | None = None) -> str | tuple[str, ...] | None:
329
329
  """Returns the mapped components for the given locus and the given gate.
330
330
 
331
331
  If the locus or the gate is not found from the locus mappings of self, returns None.
@@ -388,7 +388,7 @@ class ChipTopology:
388
388
  name = DEFAULT_1QB_MAPPING
389
389
  elif default_mapping_dimension == 2:
390
390
  name = DEFAULT_2QB_MAPPING
391
- return [locus for locus in self._locus_mappings.get(name, {})]
391
+ return list(self._locus_mappings.get(name, {}))
392
392
 
393
393
  def get_common_computational_resonator(self, first_qubit: str, second_qubit: str) -> str:
394
394
  """Convenience method for getting the name of a computational resonator which is connected to both specified
@@ -62,7 +62,7 @@ class QCMDataClient:
62
62
  # Make the cache containers local to the instances so that the reference from cache to the instance
63
63
  # gets scraped off with the instance
64
64
  # https://rednafi.github.io/reflections/dont-wrap-instance-methods-with-functoolslru_cache-decorator-in-python.html
65
- self._send_request = cache(self._send_request)
65
+ self._send_request = cache(self._send_request) # type:ignore[method-assign]
66
66
 
67
67
  @property
68
68
  def root_url(self) -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: iqm-exa-common
3
- Version: 26.25.0
3
+ Version: 26.27.0
4
4
  Summary: Framework for control and measurement of superconducting qubits: common library
5
5
  Author-email: IQM Finland Oy <info@meetiqm.com>
6
6
  License: Apache License
@@ -0,0 +1 @@
1
+ 26.27.0
@@ -1 +0,0 @@
1
- 26.25.0