qilisdk 0.1.4__py3-none-any.whl → 0.1.6__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.
Files changed (86) hide show
  1. qilisdk/__init__.py +11 -2
  2. qilisdk/__init__.pyi +2 -3
  3. qilisdk/_logging.py +135 -0
  4. qilisdk/_optionals.py +5 -7
  5. qilisdk/analog/__init__.py +3 -18
  6. qilisdk/analog/exceptions.py +2 -4
  7. qilisdk/analog/hamiltonian.py +455 -110
  8. qilisdk/analog/linear_schedule.py +121 -0
  9. qilisdk/analog/schedule.py +275 -79
  10. qilisdk/{extras → backends}/__init__.py +9 -4
  11. qilisdk/{common/model.py → backends/__init__.pyi} +3 -1
  12. qilisdk/backends/backend.py +117 -0
  13. qilisdk/{extras/cuda → backends}/cuda_backend.py +152 -159
  14. qilisdk/backends/qutip_backend.py +473 -0
  15. qilisdk/core/__init__.py +63 -0
  16. qilisdk/{common → core}/algorithm.py +2 -1
  17. qilisdk/{extras/qaas/qaas_settings.py → core/exceptions.py} +12 -6
  18. qilisdk/core/model.py +1034 -0
  19. qilisdk/core/parameterizable.py +75 -0
  20. qilisdk/core/qtensor.py +666 -0
  21. qilisdk/{common → core}/result.py +2 -1
  22. qilisdk/core/variables.py +1969 -0
  23. qilisdk/cost_functions/__init__.py +18 -0
  24. qilisdk/cost_functions/cost_function.py +77 -0
  25. qilisdk/cost_functions/model_cost_function.py +145 -0
  26. qilisdk/cost_functions/observable_cost_function.py +109 -0
  27. qilisdk/digital/__init__.py +3 -22
  28. qilisdk/digital/ansatz.py +200 -160
  29. qilisdk/digital/circuit.py +81 -9
  30. qilisdk/digital/exceptions.py +12 -6
  31. qilisdk/digital/gates.py +229 -86
  32. qilisdk/{extras/qaas/qaas_analog_result.py → functionals/__init__.py} +14 -5
  33. qilisdk/functionals/functional.py +39 -0
  34. qilisdk/{common/backend.py → functionals/functional_result.py} +3 -1
  35. qilisdk/functionals/sampling.py +81 -0
  36. qilisdk/functionals/sampling_result.py +92 -0
  37. qilisdk/functionals/time_evolution.py +98 -0
  38. qilisdk/functionals/time_evolution_result.py +84 -0
  39. qilisdk/functionals/variational_program.py +80 -0
  40. qilisdk/functionals/variational_program_result.py +69 -0
  41. qilisdk/logging_config.yaml +16 -0
  42. qilisdk/{common → optimizers}/__init__.py +1 -1
  43. qilisdk/optimizers/optimizer.py +39 -0
  44. qilisdk/{common → optimizers}/optimizer_result.py +3 -12
  45. qilisdk/{common/optimizer.py → optimizers/scipy_optimizer.py} +10 -28
  46. qilisdk/settings.py +78 -0
  47. qilisdk/speqtrum/__init__.py +41 -0
  48. qilisdk/{extras → speqtrum}/__init__.pyi +3 -3
  49. qilisdk/speqtrum/experiments/__init__.py +25 -0
  50. qilisdk/speqtrum/experiments/experiment_functional.py +124 -0
  51. qilisdk/speqtrum/experiments/experiment_result.py +231 -0
  52. qilisdk/{extras/qaas → speqtrum}/keyring.py +8 -4
  53. qilisdk/speqtrum/speqtrum.py +587 -0
  54. qilisdk/speqtrum/speqtrum_models.py +467 -0
  55. qilisdk/utils/__init__.py +0 -14
  56. qilisdk/utils/openqasm2.py +1 -1
  57. qilisdk/utils/serialization.py +1 -1
  58. qilisdk/utils/visualization/PlusJakartaSans-SemiBold.ttf +0 -0
  59. qilisdk/utils/visualization/__init__.py +24 -0
  60. qilisdk/utils/visualization/circuit_renderers.py +781 -0
  61. qilisdk/utils/visualization/schedule_renderers.py +166 -0
  62. qilisdk/utils/visualization/style.py +154 -0
  63. qilisdk/utils/visualization/themes.py +76 -0
  64. qilisdk/yaml.py +126 -0
  65. {qilisdk-0.1.4.dist-info → qilisdk-0.1.6.dist-info}/METADATA +186 -140
  66. qilisdk-0.1.6.dist-info/RECORD +69 -0
  67. qilisdk/analog/algorithms.py +0 -111
  68. qilisdk/analog/analog_backend.py +0 -43
  69. qilisdk/analog/analog_result.py +0 -114
  70. qilisdk/analog/quantum_objects.py +0 -596
  71. qilisdk/digital/digital_algorithm.py +0 -20
  72. qilisdk/digital/digital_backend.py +0 -90
  73. qilisdk/digital/digital_result.py +0 -145
  74. qilisdk/digital/vqe.py +0 -166
  75. qilisdk/extras/cuda/__init__.py +0 -13
  76. qilisdk/extras/cuda/cuda_analog_result.py +0 -19
  77. qilisdk/extras/cuda/cuda_digital_result.py +0 -19
  78. qilisdk/extras/qaas/__init__.py +0 -13
  79. qilisdk/extras/qaas/models.py +0 -132
  80. qilisdk/extras/qaas/qaas_backend.py +0 -255
  81. qilisdk/extras/qaas/qaas_digital_result.py +0 -20
  82. qilisdk/extras/qaas/qaas_time_evolution_result.py +0 -20
  83. qilisdk/extras/qaas/qaas_vqe_result.py +0 -20
  84. qilisdk-0.1.4.dist-info/RECORD +0 -51
  85. {qilisdk-0.1.4.dist-info → qilisdk-0.1.6.dist-info}/WHEEL +0 -0
  86. {qilisdk-0.1.4.dist-info → qilisdk-0.1.6.dist-info}/licenses/LICENCE +0 -0
@@ -13,17 +13,14 @@
13
13
  # limitations under the License.
14
14
  from __future__ import annotations
15
15
 
16
+ from qilisdk.core.result import Result
16
17
  from qilisdk.yaml import yaml
17
18
 
18
19
 
19
20
  @yaml.register_class
20
- class OptimizerIntermediateResult:
21
+ class OptimizerIntermediateResult(Result):
21
22
  """
22
23
  Represents an intermediate result.
23
-
24
- Attributes:
25
- cost (float): The optimal cost value (e.g., minimum energy) found.
26
- parameters (List[float]): The parameters that yield the optimal cost.
27
24
  """
28
25
 
29
26
  def __init__(
@@ -50,15 +47,9 @@ class OptimizerIntermediateResult:
50
47
 
51
48
 
52
49
  @yaml.register_class
53
- class OptimizerResult:
50
+ class OptimizerResult(Result):
54
51
  """
55
52
  Represents the result of an optimization run.
56
-
57
- Attributes:
58
- optimal_cost (float): The optimal cost value (e.g., minimum energy) found.
59
- optimal_parameters (List[float]): The parameters that yield the optimal cost.
60
- intermediate_results (List[OptimizerResult]): A list of intermediate optimization results.
61
- Each intermediate result is an instance of OptimizerResult containing the current cost and parameters.
62
53
  """
63
54
 
64
55
  def __init__(
@@ -11,39 +11,19 @@
11
11
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
+ from __future__ import annotations
14
15
 
15
- from abc import ABC, abstractmethod
16
- from typing import Any, Callable
16
+ from typing import TYPE_CHECKING, Any, Callable
17
17
 
18
18
  from scipy import optimize as scipy_optimize
19
- from scipy.optimize import OptimizeResult
20
19
 
21
20
  from qilisdk.yaml import yaml
22
21
 
22
+ from .optimizer import Optimizer
23
23
  from .optimizer_result import OptimizerIntermediateResult, OptimizerResult
24
24
 
25
-
26
- class Optimizer(ABC):
27
- @abstractmethod
28
- def optimize(
29
- self,
30
- cost_function: Callable[[list[float]], float],
31
- init_parameters: list[float],
32
- store_intermediate_results: bool = False,
33
- ) -> OptimizerResult:
34
- """
35
- Optimize the cost function and return an OptimizerResult.
36
-
37
- Args:
38
- cost_function (Callable[[List[float]], float]): A function that takes a list of parameters and returns the cost.
39
- init_parameters (List[float]): The initial parameters for the optimization.
40
- store_intermediate_results (bool, optional): If True, stores a list of intermediate optimization results.
41
- Each intermediate result is recorded as an OptimizerResult containing the parameters and cost at that iteration.
42
- Defaults to False.
43
-
44
- Returns:
45
- OptimizerResult: An object containing the optimal cost, optimal parameters, and, if requested, the intermediate results.
46
- """
25
+ if TYPE_CHECKING:
26
+ from scipy.optimize import OptimizeResult
47
27
 
48
28
 
49
29
  @yaml.register_class
@@ -82,8 +62,8 @@ class SciPyOptimizer(Optimizer):
82
62
  for each element in parameter list.
83
63
 
84
64
  Extra Args:
85
- Any argument supported by `scipy.optimize.minimize <https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html>`__ can be passed.
86
- Note: the parameters, cost function and the ``args``that are passed to this function will be specified in the optimize method. Moreover, callbacks are not supported for the moment.
65
+ Any argument supported by `scipy.optimize.minimize <https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html>` can be passed.
66
+ Note: the parameters, cost function and the ``args`` that are passed to this function will be specified in the optimize method. Moreover, callbacks are not supported for the moment.
87
67
  """
88
68
  super().__init__()
89
69
  self.method = method
@@ -93,6 +73,7 @@ class SciPyOptimizer(Optimizer):
93
73
  self,
94
74
  cost_function: Callable[[list[float]], float],
95
75
  init_parameters: list[float],
76
+ bounds: list[tuple[float, float]],
96
77
  store_intermediate_results: bool = False,
97
78
  ) -> OptimizerResult:
98
79
  """optimize the cost function and return the optimal parameters.
@@ -100,6 +81,7 @@ class SciPyOptimizer(Optimizer):
100
81
  Args:
101
82
  cost_function (Callable[[list[float]], float]): a function that takes in a list of parameters and returns the cost.
102
83
  init_parameters (list[float]): the list of initial parameters. Note: the length of this list determines the number of parameters the optimizer will consider.
84
+ bounds (list[float, float]): a list of the variable value bounds.
103
85
 
104
86
  Returns:
105
87
  list[float]: the optimal set of parameters that minimize the cost function.
@@ -119,10 +101,10 @@ class SciPyOptimizer(Optimizer):
119
101
  cost_function,
120
102
  x0=init_parameters,
121
103
  method=self.method,
104
+ bounds=bounds,
122
105
  jac=self.extra_arguments.get("jac", None),
123
106
  hess=self.extra_arguments.get("hess", None),
124
107
  hessp=self.extra_arguments.get("hessp", None),
125
- bounds=self.extra_arguments.get("bounds", None),
126
108
  constraints=self.extra_arguments.get("constraints", ()),
127
109
  tol=self.extra_arguments.get("tol", None),
128
110
  options=self.extra_arguments.get("options", None),
qilisdk/settings.py ADDED
@@ -0,0 +1,78 @@
1
+ # Copyright 2025 Qilimanjaro Quantum Tech
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from enum import Enum
16
+ from functools import lru_cache
17
+ from pathlib import Path
18
+
19
+ from pydantic import Field
20
+ from pydantic_settings import BaseSettings, SettingsConfigDict
21
+
22
+
23
+ def default_logging_config_path() -> Path:
24
+ return Path(__file__).with_name("logging_config.yaml").resolve()
25
+
26
+
27
+ class Precision(str, Enum):
28
+ COMPLEX_64 = "COMPLEX_64"
29
+ COMPLEX_128 = "COMPLEX_128"
30
+
31
+
32
+ class QiliSDKSettings(BaseSettings):
33
+ """
34
+ Environment-based configuration settings for QiliSDK.
35
+
36
+ These settings are automatically loaded from environment variables
37
+ prefixed with `QILISDK_`, or from a local `.env` file if present.
38
+ """
39
+
40
+ model_config = SettingsConfigDict(env_prefix="qilisdk_", env_file=".env", env_file_encoding="utf-8")
41
+
42
+ arithmetic_precision: Precision = Field(
43
+ default=Precision.COMPLEX_128, description="[env: QILISDK_ARITHMETIC_PRECISION]"
44
+ )
45
+ logging_config_path: Path = Field(
46
+ default_factory=default_logging_config_path,
47
+ description="YAML file used for logging configuration. [env: QILISDK_LOGGING_CONFIG_PATH]",
48
+ )
49
+ speqtrum_username: str | None = Field(
50
+ default=None,
51
+ description="SpeQtrum username used for authentication. [env: QILISDK_SPEQTRUM_USERNAME]",
52
+ )
53
+ speqtrum_apikey: str | None = Field(
54
+ default=None,
55
+ description="SpeQtrum API key associated with the user account. [env: QILISDK_SPEQTRUM_APIKEY]",
56
+ )
57
+ speqtrum_api_url: str = Field(
58
+ default="https://qilimanjaro.ddns.net/public-api/api/v1",
59
+ description="Base URL of the SpeQtrum API endpoint. [env: QILISDK_SPEQTRUM_API_URL]",
60
+ )
61
+ speqtrum_audience: str = Field(
62
+ default="urn:qilimanjaro.tech:public-api:beren",
63
+ description="Audience claim expected in the JWT used for authentication. [env: QILISDK_SPEQTRUM_AUDIENCE]",
64
+ )
65
+
66
+
67
+ @lru_cache(maxsize=1)
68
+ def get_settings() -> QiliSDKSettings:
69
+ """
70
+ Returns a singleton instance of QiliSDKSettings.
71
+
72
+ This function caches the parsed environment-based settings to avoid
73
+ redundant re-parsing across the application lifecycle.
74
+
75
+ Returns:
76
+ QiliSDKSettings: The cached configuration object populated from environment variables.
77
+ """
78
+ return QiliSDKSettings()
@@ -0,0 +1,41 @@
1
+ # Copyright 2025 Qilimanjaro Quantum Tech
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ import sys
15
+
16
+ from qilisdk._optionals import ImportedFeature, OptionalFeature, Symbol, import_optional_dependencies
17
+
18
+ __all__ = []
19
+
20
+ OPTIONAL_FEATURES: list[OptionalFeature] = [
21
+ OptionalFeature(
22
+ name="speqtrum",
23
+ dependencies=["httpx", "keyring", "keyrings-alt"],
24
+ symbols=[
25
+ Symbol(path="qilisdk.speqtrum.speqtrum", name="SpeQtrum"),
26
+ Symbol(path="qilisdk.speqtrum.speqtrum_models", name="DeviceStatus"),
27
+ Symbol(path="qilisdk.speqtrum.speqtrum_models", name="DeviceType"),
28
+ Symbol(path="qilisdk.speqtrum.speqtrum_models", name="JobHandle"),
29
+ Symbol(path="qilisdk.speqtrum.speqtrum_models", name="TypedJobDetail"),
30
+ ],
31
+ ),
32
+ ]
33
+
34
+ current_module = sys.modules[__name__]
35
+
36
+ # Dynamically import (or stub) each feature's symbols and attach them
37
+ for feature in OPTIONAL_FEATURES:
38
+ imported_feature: ImportedFeature = import_optional_dependencies(feature)
39
+ for symbol_name, symbol_obj in imported_feature.symbols.items():
40
+ setattr(current_module, symbol_name, symbol_obj)
41
+ __all__ += [symbol_name] # noqa: PLE0604
@@ -12,7 +12,7 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- from .cuda.cuda_backend import CudaBackend
16
- from .qaas.qaas_backend import QaaSBackend
15
+ from .speqtrum import SpeQtrum
16
+ from .speqtrum_models import DeviceStatus, DeviceType
17
17
 
18
- __all__ = ["CudaBackend", "QaaSBackend"]
18
+ __all__ = ["DeviceStatus", "DeviceType", "SpeQtrum"]
@@ -0,0 +1,25 @@
1
+ # Copyright 2025 Qilimanjaro Quantum Tech
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ from .experiment_functional import ExperimentFunctional, RabiExperiment, T1Experiment
15
+ from .experiment_result import Dimension, ExperimentResult, RabiExperimentResult, T1ExperimentResult
16
+
17
+ __all__ = [
18
+ "Dimension",
19
+ "ExperimentFunctional",
20
+ "ExperimentResult",
21
+ "RabiExperiment",
22
+ "RabiExperimentResult",
23
+ "T1Experiment",
24
+ "T1ExperimentResult",
25
+ ]
@@ -0,0 +1,124 @@
1
+ # Copyright 2025 Qilimanjaro Quantum Tech
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ from __future__ import annotations
15
+
16
+ from abc import ABC
17
+ from typing import TYPE_CHECKING, ClassVar, Generic, TypeVar
18
+
19
+ from qilisdk.functionals.functional import Functional
20
+ from qilisdk.speqtrum.experiments.experiment_result import (
21
+ ExperimentResult,
22
+ RabiExperimentResult,
23
+ T1ExperimentResult,
24
+ )
25
+ from qilisdk.yaml import yaml
26
+
27
+ if TYPE_CHECKING:
28
+ import numpy as np
29
+
30
+ TResult_co = TypeVar("TResult_co", bound=ExperimentResult, covariant=True)
31
+
32
+
33
+ @yaml.register_class
34
+ class ExperimentFunctional(Functional, ABC, Generic[TResult_co]):
35
+ """Abstract base class for single-qubit experiment functionals.
36
+
37
+ This class serves as a generic interface for defining quantum
38
+ characterization experiments such as Rabi or T1. Each subclass
39
+ specifies a concrete `ExperimentResult` type and the corresponding
40
+ sweep parameters.
41
+ """
42
+
43
+ def __init__(self, qubit: int) -> None:
44
+ """Initialize the experiment functional.
45
+
46
+ Args:
47
+ qubit (int): The physical qubit index on which the experiment is performed.
48
+ """
49
+ self._qubit = qubit
50
+
51
+ @property
52
+ def qubit(self) -> int:
53
+ """The physical qubit index on which the experiment is performed.
54
+
55
+ Returns:
56
+ int: Index of the qubit.
57
+ """
58
+ return self._qubit
59
+
60
+
61
+ @yaml.register_class
62
+ class RabiExperiment(ExperimentFunctional[RabiExperimentResult]):
63
+ """Rabi experiment functional for a single qubit.
64
+
65
+ This functional defines a standard Rabi oscillation experiment where
66
+ the drive pulse duration is swept to measure the oscillatory response
67
+ of the qubit under continuous driving.
68
+ """
69
+
70
+ result_type: ClassVar[type[RabiExperimentResult]] = RabiExperimentResult
71
+ """Result type returned by this functional."""
72
+
73
+ def __init__(self, qubit: int, drive_duration_values: np.ndarray) -> None:
74
+ """Initialize a Rabi experiment functional.
75
+
76
+ Args:
77
+ qubit (int): The physical qubit index on which the experiment is performed.
78
+ drive_duration_values (np.ndarray): Array of drive pulse durations (in nanoseconds)
79
+ used to sweep the experiment.
80
+ """
81
+ super().__init__(qubit=qubit)
82
+ self._drive_duration_values = drive_duration_values
83
+
84
+ @property
85
+ def drive_duration_values(self) -> np.ndarray:
86
+ """Drive pulse duration sweep values.
87
+
88
+ Returns:
89
+ np.ndarray: The set of drive durations (in nanoseconds) used in the Rabi experiment.
90
+ """
91
+ return self._drive_duration_values
92
+
93
+
94
+ @yaml.register_class
95
+ class T1Experiment(ExperimentFunctional[T1ExperimentResult]):
96
+ """T1 relaxation experiment functional for a single qubit.
97
+
98
+ This functional defines a standard T1 (energy relaxation) experiment,
99
+ where the delay between excitation and measurement is varied to extract
100
+ the relaxation time constant of the qubit.
101
+ """
102
+
103
+ result_type: ClassVar[type[T1ExperimentResult]] = T1ExperimentResult
104
+ """Result type returned by this functional."""
105
+
106
+ def __init__(self, qubit: int, wait_duration_values: np.ndarray) -> None:
107
+ """Initialize a T1 experiment functional.
108
+
109
+ Args:
110
+ qubit (int): The physical qubit index on which the experiment is performed.
111
+ wait_duration_values (np.ndarray): Array of waiting times (in nanoseconds)
112
+ between excitation and measurement.
113
+ """
114
+ super().__init__(qubit=qubit)
115
+ self._wait_duration_values: np.ndarray = wait_duration_values
116
+
117
+ @property
118
+ def wait_duration_values(self) -> np.ndarray:
119
+ """Waiting time sweep values.
120
+
121
+ Returns:
122
+ np.ndarray: The set of delay durations (in nanoseconds) used in the T1 experiment.
123
+ """
124
+ return self._wait_duration_values
@@ -0,0 +1,231 @@
1
+ # Copyright 2025 Qilimanjaro Quantum Tech
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ from pathlib import Path
15
+ from typing import ClassVar
16
+
17
+ import matplotlib.pyplot as plt
18
+ import numpy as np
19
+ from matplotlib.figure import Figure
20
+
21
+ from qilisdk.functionals.functional_result import FunctionalResult
22
+ from qilisdk.yaml import yaml
23
+
24
+
25
+ @yaml.register_class
26
+ class Dimension:
27
+ """Represents a labeled dimension in an experiment sweep.
28
+
29
+ A `Dimension` defines one or more sweep parameters, such as drive
30
+ amplitude, frequency, or delay time, together with their associated
31
+ numerical values.
32
+
33
+ Attributes:
34
+ labels (list[str]): Human-readable labels for the sweep parameters.
35
+ values (list[np.ndarray]): Numeric arrays representing the values
36
+ corresponding to each label.
37
+ """
38
+
39
+ def __init__(self, labels: list[str], values: list[np.ndarray]) -> None:
40
+ """Initialize a Dimension object.
41
+
42
+ Args:
43
+ labels (list[str]): Labels describing each dimension (e.g. ``["Drive amplitude"]``).
44
+ values (list[np.ndarray]): Numerical arrays for the corresponding parameter values.
45
+ """
46
+ self.labels = labels
47
+ self.values = values
48
+
49
+
50
+ @yaml.register_class
51
+ class ExperimentResult(FunctionalResult):
52
+ """Base class for storing and visualizing experiment results.
53
+
54
+ This class defines common utilities for handling experimental data,
55
+ including computation of S21 parameters and automatic 1D or 2D plotting.
56
+ Subclasses provide specific sweep parameters and plot titles.
57
+ """
58
+
59
+ plot_title: ClassVar[str]
60
+ """Default plot title; subclasses provide the concrete label."""
61
+
62
+ def __init__(self, qubit: int, data: np.ndarray, dims: list[Dimension]) -> None:
63
+ """Initialize an experiment result.
64
+
65
+ Args:
66
+ qubit (int): The qubit index on which the experiment was performed.
67
+ data (np.ndarray): Raw experimental data array.
68
+ dims (list[Dimension]): Sweep dimensions of the experiment.
69
+ """
70
+ self.qubit = qubit
71
+ self.data = data
72
+ self.dims = dims
73
+
74
+ @property
75
+ def s21(self) -> np.ndarray:
76
+ """Complex S21 transmission parameter.
77
+
78
+ Returns:
79
+ np.ndarray: The complex-valued S21 response computed as ``Re + i * Im``.
80
+ """
81
+ return self.data[..., 0] + 1j * self.data[..., 1]
82
+
83
+ @property
84
+ def s21_modulus(self) -> np.ndarray:
85
+ """Magnitude of the S21 parameter.
86
+
87
+ Returns:
88
+ np.ndarray: The absolute value of the S21 parameter.
89
+ """
90
+ return np.abs(self.s21)
91
+
92
+ @property
93
+ def s21_db(self) -> np.ndarray:
94
+ """Magnitude of S21 in decibels (dB).
95
+
96
+ Returns:
97
+ np.ndarray: ``20 * log10(abs(S21))`` expressed in dB.
98
+ """
99
+ return 20 * np.log10(self.s21_modulus)
100
+
101
+ def plot(self, save_to: str | None = None) -> None:
102
+ """Plot the S21 parameter from experiment results.
103
+
104
+ Automatically detects whether the dataset is 1D or 2D and creates
105
+ the appropriate figure. Optionally saves the figure to disk.
106
+
107
+ Args:
108
+ save_to (str | None): Optional path or directory to save the
109
+ generated plot. If a directory is provided, the filename is
110
+ automatically generated as ``{plot_title}_qubit{qubit}.png``.
111
+
112
+ Raises:
113
+ NotImplementedError: If the experiment data has more than 2 dimensions.
114
+ """
115
+
116
+ def save_figure(figure: Figure, save_to: str | Path) -> None:
117
+ save_to = Path(save_to)
118
+
119
+ # If a directory was given, append the default filename
120
+ if save_to.is_dir():
121
+ save_to /= f"{self.plot_title}_qubit{self.qubit}.png"
122
+
123
+ save_to.parent.mkdir(parents=True, exist_ok=True)
124
+ figure.savefig(save_to)
125
+
126
+ def plot_1d(s21: np.ndarray, dims: list[Dimension]) -> None:
127
+ """Plot 1d"""
128
+ x_labels, x_values = dims[0].labels, dims[0].values
129
+
130
+ fig, ax1 = plt.subplots()
131
+ ax1.set_title(f"{self.plot_title} - Qubit {self.qubit}")
132
+ ax1.set_xlabel(x_labels[0])
133
+ ax1.set_ylabel(r"$|S_{21}|$")
134
+ ax1.plot(x_values[0], s21, ".")
135
+
136
+ if len(x_labels) > 1:
137
+ # Create secondary x-axis
138
+ ax2 = ax1.twiny()
139
+
140
+ # Set labels
141
+ ax2.set_xlabel(x_labels[1])
142
+ ax2.set_xlim(min(x_values[1]), max(x_values[1]))
143
+
144
+ # Set tick locations
145
+ ax2_ticks = np.linspace(min(x_values[1]), max(x_values[1]), num=6)
146
+ ax2.set_xticks(ax2_ticks)
147
+
148
+ # Force scientific notation
149
+ ax2.ticklabel_format(axis="x", style="sci", scilimits=(-3, 3))
150
+
151
+ if save_to:
152
+ save_figure(fig, save_to)
153
+
154
+ plt.show()
155
+
156
+ # pylint: disable=too-many-locals
157
+ def plot_2d(s21: np.ndarray, dims: list[Dimension]) -> None:
158
+ """Plot 2d"""
159
+ x_labels, x_values = dims[0].labels, dims[0].values
160
+ y_labels, y_values = dims[1].labels, dims[1].values
161
+
162
+ # Create x and y edge arrays by extrapolating the edges
163
+ x_edges = np.linspace(x_values[0].min(), x_values[0].max(), len(x_values[0]) + 1)
164
+ y_edges = np.linspace(y_values[0].min(), y_values[0].max(), len(y_values[0]) + 1)
165
+
166
+ fig, ax1 = plt.subplots()
167
+ ax1.set_title(f"{self.plot_title} - Qubit {self.qubit}")
168
+ ax1.set_xlabel(x_labels[0])
169
+ ax1.set_ylabel(y_labels[0])
170
+
171
+ # Force scientific notation
172
+ ax1.ticklabel_format(axis="both", style="sci", scilimits=(-3, 3))
173
+
174
+ mesh = ax1.pcolormesh(x_edges, y_edges, s21.T, cmap="viridis", shading="auto")
175
+ fig.colorbar(mesh, ax=ax1)
176
+
177
+ if len(x_labels) > 1:
178
+ # Create secondary x-axis
179
+ ax2 = ax1.twiny()
180
+
181
+ # Set labels
182
+ ax2.set_xlabel(x_labels[1])
183
+ ax2.set_xlim(min(x_values[1]), max(x_values[1]))
184
+
185
+ # Set tick locations
186
+ ax2_ticks = np.linspace(min(x_values[1]), max(x_values[1]), num=6)
187
+ ax2.set_xticks(ax2_ticks)
188
+
189
+ # Force scientific notation
190
+ ax2.ticklabel_format(axis="x", style="sci", scilimits=(-3, 3))
191
+ if len(y_labels) > 1:
192
+ ax3 = ax1.twinx()
193
+ ax3.set_ylabel(y_labels[1])
194
+ ax3.set_ylim(min(y_values[1]), max(y_values[1]))
195
+
196
+ # Set tick locations
197
+ ax3_ticks = np.linspace(min(y_values[1]), max(y_values[1]), num=6)
198
+ ax3.set_xticks(ax3_ticks)
199
+
200
+ # Force scientific notation
201
+ ax3.ticklabel_format(axis="y", style="sci", scilimits=(-3, 3))
202
+
203
+ if save_to:
204
+ save_figure(fig, save_to)
205
+
206
+ plt.tight_layout()
207
+ plt.show()
208
+
209
+ n_dimensions = len(self.s21_modulus.shape)
210
+ if n_dimensions == 1:
211
+ plot_1d(self.s21_modulus, self.dims)
212
+ elif n_dimensions == 2: # noqa: PLR2004
213
+ plot_2d(self.s21_modulus, self.dims)
214
+ else:
215
+ raise NotImplementedError("3D and higher dimension plots are not supported yet.")
216
+
217
+
218
+ @yaml.register_class
219
+ class RabiExperimentResult(ExperimentResult):
220
+ """Result container for Rabi experiments."""
221
+
222
+ plot_title: ClassVar[str] = "Rabi"
223
+ """Default title for Rabi experiment plots."""
224
+
225
+
226
+ @yaml.register_class
227
+ class T1ExperimentResult(ExperimentResult):
228
+ """Result container for T1 relaxation experiments."""
229
+
230
+ plot_title: ClassVar[str] = "T1"
231
+ """Default title for T1 experiment plots."""
@@ -13,11 +13,12 @@
13
13
  # limitations under the License.
14
14
 
15
15
  import keyring
16
+ from keyring.errors import PasswordDeleteError
16
17
  from pydantic import ValidationError
17
18
 
18
- from .models import Token
19
+ from .speqtrum_models import Token
19
20
 
20
- KEYRING_IDENTIFIER = "QaaSKeyring"
21
+ KEYRING_IDENTIFIER = "SpeQtrumKeyring"
21
22
 
22
23
 
23
24
  def store_credentials(username: str, token: Token) -> None:
@@ -32,8 +33,11 @@ def delete_credentials() -> None:
32
33
  """
33
34
  Delete username and token from the keyring.
34
35
  """
35
- keyring.delete_password(KEYRING_IDENTIFIER, "username")
36
- keyring.delete_password(KEYRING_IDENTIFIER, "token")
36
+ try:
37
+ keyring.delete_password(KEYRING_IDENTIFIER, "username")
38
+ keyring.delete_password(KEYRING_IDENTIFIER, "token")
39
+ except PasswordDeleteError:
40
+ return
37
41
 
38
42
 
39
43
  def load_credentials() -> tuple[str, Token] | None: