qilisdk 0.1.6__py3-none-any.whl → 0.1.7__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.
- qilisdk/analog/__init__.py +1 -2
- qilisdk/analog/hamiltonian.py +1 -68
- qilisdk/analog/schedule.py +288 -313
- qilisdk/backends/backend.py +5 -1
- qilisdk/backends/cuda_backend.py +9 -5
- qilisdk/backends/qutip_backend.py +23 -12
- qilisdk/core/__init__.py +4 -0
- qilisdk/core/interpolator.py +406 -0
- qilisdk/core/parameterizable.py +66 -10
- qilisdk/core/variables.py +150 -7
- qilisdk/digital/circuit.py +1 -0
- qilisdk/digital/circuit_transpiler.py +46 -0
- qilisdk/digital/circuit_transpiler_passes/__init__.py +18 -0
- qilisdk/digital/circuit_transpiler_passes/circuit_transpiler_pass.py +36 -0
- qilisdk/digital/circuit_transpiler_passes/decompose_multi_controlled_gates_pass.py +216 -0
- qilisdk/digital/circuit_transpiler_passes/numeric_helpers.py +82 -0
- qilisdk/digital/gates.py +12 -2
- qilisdk/{speqtrum/experiments → experiments}/__init__.py +13 -2
- qilisdk/{speqtrum/experiments → experiments}/experiment_functional.py +90 -2
- qilisdk/{speqtrum/experiments → experiments}/experiment_result.py +16 -0
- qilisdk/functionals/sampling.py +8 -1
- qilisdk/functionals/time_evolution.py +6 -2
- qilisdk/functionals/variational_program.py +58 -0
- qilisdk/speqtrum/speqtrum.py +360 -130
- qilisdk/speqtrum/speqtrum_models.py +108 -19
- qilisdk/utils/openfermion/__init__.py +38 -0
- qilisdk/{core/algorithm.py → utils/openfermion/__init__.pyi} +2 -3
- qilisdk/utils/openfermion/openfermion.py +45 -0
- qilisdk/utils/visualization/schedule_renderers.py +16 -8
- {qilisdk-0.1.6.dist-info → qilisdk-0.1.7.dist-info}/METADATA +74 -24
- {qilisdk-0.1.6.dist-info → qilisdk-0.1.7.dist-info}/RECORD +33 -26
- {qilisdk-0.1.6.dist-info → qilisdk-0.1.7.dist-info}/WHEEL +1 -1
- qilisdk/analog/linear_schedule.py +0 -121
- {qilisdk-0.1.6.dist-info → qilisdk-0.1.7.dist-info}/licenses/LICENCE +0 -0
qilisdk/core/parameterizable.py
CHANGED
|
@@ -13,28 +13,38 @@
|
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
-
from abc import ABC
|
|
16
|
+
from abc import ABC
|
|
17
|
+
from typing import TYPE_CHECKING
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from qilisdk.core.variables import BaseVariable, ComparisonTerm, Parameter
|
|
17
21
|
|
|
18
22
|
|
|
19
23
|
class Parameterizable(ABC):
|
|
24
|
+
"""Mixin for objects that expose tunable parameters and constraints."""
|
|
25
|
+
|
|
26
|
+
def __init__(self) -> None:
|
|
27
|
+
super(Parameterizable, self).__init__()
|
|
28
|
+
self._parameters: dict[str, Parameter] = {}
|
|
29
|
+
self._parameter_constraints: list[ComparisonTerm] = []
|
|
30
|
+
|
|
20
31
|
@property
|
|
21
|
-
@abstractmethod
|
|
22
32
|
def nparameters(self) -> int:
|
|
23
33
|
"""Number of tunable parameters defined by the object."""
|
|
34
|
+
return len(self._parameters)
|
|
24
35
|
|
|
25
|
-
@abstractmethod
|
|
26
36
|
def get_parameter_values(self) -> list[float]:
|
|
27
37
|
"""Return the current numerical values of the parameters."""
|
|
38
|
+
return [param.value for param in self._parameters.values()]
|
|
28
39
|
|
|
29
|
-
@abstractmethod
|
|
30
40
|
def get_parameter_names(self) -> list[str]:
|
|
31
41
|
"""Return the ordered list of parameter labels."""
|
|
42
|
+
return list(self._parameters.keys())
|
|
32
43
|
|
|
33
|
-
@abstractmethod
|
|
34
44
|
def get_parameters(self) -> dict[str, float]:
|
|
35
45
|
"""Return a mapping from parameter labels to their current numerical values."""
|
|
46
|
+
return {label: param.value for label, param in self._parameters.items()}
|
|
36
47
|
|
|
37
|
-
@abstractmethod
|
|
38
48
|
def set_parameter_values(self, values: list[float]) -> None:
|
|
39
49
|
"""
|
|
40
50
|
Update all parameter values at once.
|
|
@@ -45,8 +55,12 @@ class Parameterizable(ABC):
|
|
|
45
55
|
Raises:
|
|
46
56
|
ValueError: If ``values`` does not contain exactly ``nparameters`` entries.
|
|
47
57
|
"""
|
|
58
|
+
if len(values) != self.nparameters:
|
|
59
|
+
raise ValueError(f"Provided {len(values)} but this object has {self.nparameters} parameters.")
|
|
60
|
+
param_names = self.get_parameter_names()
|
|
61
|
+
value_dict = {param_names[i]: values[i] for i in range(len(values))}
|
|
62
|
+
self.set_parameters(value_dict)
|
|
48
63
|
|
|
49
|
-
@abstractmethod
|
|
50
64
|
def set_parameters(self, parameters: dict[str, float]) -> None:
|
|
51
65
|
"""
|
|
52
66
|
Update a subset of parameters by label.
|
|
@@ -55,14 +69,21 @@ class Parameterizable(ABC):
|
|
|
55
69
|
parameters (dict[str, float]): Mapping from parameter labels to updated numeric values.
|
|
56
70
|
|
|
57
71
|
Raises:
|
|
58
|
-
ValueError: If an unknown parameter label is provided.
|
|
72
|
+
ValueError: If an unknown parameter label is provided or constraints are violated.
|
|
59
73
|
"""
|
|
74
|
+
if not self.check_constraints(parameters):
|
|
75
|
+
raise ValueError(
|
|
76
|
+
f"New assignation of the parameters breaks the parameter constraints: \n{self.get_constraints()}"
|
|
77
|
+
)
|
|
78
|
+
for label, param in parameters.items():
|
|
79
|
+
if label not in self._parameters:
|
|
80
|
+
raise ValueError(f"Parameter {label} is not defined for this object.")
|
|
81
|
+
self._parameters[label].set_value(param)
|
|
60
82
|
|
|
61
|
-
@abstractmethod
|
|
62
83
|
def get_parameter_bounds(self) -> dict[str, tuple[float, float]]:
|
|
63
84
|
"""Return the ``(lower, upper)`` bounds associated with each parameter."""
|
|
85
|
+
return {label: param.bounds for label, param in self._parameters.items()}
|
|
64
86
|
|
|
65
|
-
@abstractmethod
|
|
66
87
|
def set_parameter_bounds(self, ranges: dict[str, tuple[float, float]]) -> None:
|
|
67
88
|
"""
|
|
68
89
|
Update the allowable ranges for the specified parameters.
|
|
@@ -73,3 +94,38 @@ class Parameterizable(ABC):
|
|
|
73
94
|
Raises:
|
|
74
95
|
ValueError: If an unknown parameter label is provided.
|
|
75
96
|
"""
|
|
97
|
+
for label, bound in ranges.items():
|
|
98
|
+
if label not in self._parameters:
|
|
99
|
+
raise ValueError(
|
|
100
|
+
f"The provided parameter label {label} is not defined in the list of parameters in this object."
|
|
101
|
+
)
|
|
102
|
+
self._parameters[label].set_bounds(bound[0], bound[1])
|
|
103
|
+
|
|
104
|
+
def get_constraints(self) -> list[ComparisonTerm]:
|
|
105
|
+
"""Get all constraints on the parameters.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
list[ComparisonTerm]: A list of comparison terms involving the parameters of the Object.
|
|
109
|
+
"""
|
|
110
|
+
return self._parameter_constraints
|
|
111
|
+
|
|
112
|
+
def check_constraints(self, parameters: dict[str, float]) -> bool:
|
|
113
|
+
"""Validate that proposed parameter updates satisfy all constraints.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
parameters (dict[str, float]): Candidate parameter values keyed by label.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
bool: True if every constraint evaluates to True for the provided values.
|
|
120
|
+
|
|
121
|
+
Raises:
|
|
122
|
+
ValueError: If an unknown parameter label is provided.
|
|
123
|
+
"""
|
|
124
|
+
evaluate_dict: dict[BaseVariable, float] = {}
|
|
125
|
+
for label, value in parameters.items():
|
|
126
|
+
if label not in self._parameters:
|
|
127
|
+
raise ValueError(f"Parameter {label} is not defined for this object.")
|
|
128
|
+
evaluate_dict[self._parameters[label]] = value
|
|
129
|
+
constraints = self.get_constraints()
|
|
130
|
+
valid = all(con.evaluate(evaluate_dict) for con in constraints)
|
|
131
|
+
return valid
|
qilisdk/core/variables.py
CHANGED
|
@@ -18,7 +18,7 @@ import copy
|
|
|
18
18
|
import re
|
|
19
19
|
from abc import ABC, abstractmethod
|
|
20
20
|
from enum import Enum
|
|
21
|
-
from typing import TYPE_CHECKING, Iterator, Mapping, Sequence, TypeVar
|
|
21
|
+
from typing import TYPE_CHECKING, Iterator, Mapping, Sequence, TypeVar, overload
|
|
22
22
|
|
|
23
23
|
import numpy as np
|
|
24
24
|
from loguru import logger
|
|
@@ -246,6 +246,7 @@ class Operation(str, Enum):
|
|
|
246
246
|
ADD = "+"
|
|
247
247
|
DIV = "/"
|
|
248
248
|
SUB = "-"
|
|
249
|
+
MATH_MAP = "mathematical_map"
|
|
249
250
|
|
|
250
251
|
@classmethod
|
|
251
252
|
def to_yaml(cls, representer: RoundTripRepresenter, node: Operation) -> ScalarNode:
|
|
@@ -761,6 +762,7 @@ class BaseVariable(ABC):
|
|
|
761
762
|
if lower_bound > upper_bound:
|
|
762
763
|
raise InvalidBoundsError("lower bound can't be larger than the upper bound.")
|
|
763
764
|
self._bounds = (lower_bound, upper_bound)
|
|
765
|
+
self._hash_cache: int | None = None
|
|
764
766
|
|
|
765
767
|
@property
|
|
766
768
|
def bounds(self) -> tuple[float, float]:
|
|
@@ -819,6 +821,7 @@ class BaseVariable(ABC):
|
|
|
819
821
|
OutOfBoundsException: the lower bound or the upper bound don't correspond to the variable domain.
|
|
820
822
|
InvalidBoundsError: the lower bound is higher than the upper bound.
|
|
821
823
|
"""
|
|
824
|
+
self._hash_cache = None
|
|
822
825
|
if lower_bound is None:
|
|
823
826
|
lower_bound = self._domain.min()
|
|
824
827
|
if upper_bound is None:
|
|
@@ -866,7 +869,7 @@ class BaseVariable(ABC):
|
|
|
866
869
|
domain (Domain): The updated domain of the variable.
|
|
867
870
|
bounds (tuple[float | None, float | None]): The updated bounds of the variable. Defaults to (None, None)
|
|
868
871
|
"""
|
|
869
|
-
|
|
872
|
+
self._hash_cache = None
|
|
870
873
|
self._domain = domain
|
|
871
874
|
self.set_bounds(bounds[0], bounds[1])
|
|
872
875
|
|
|
@@ -982,7 +985,9 @@ class BaseVariable(ABC):
|
|
|
982
985
|
return out
|
|
983
986
|
|
|
984
987
|
def __hash__(self) -> int:
|
|
985
|
-
|
|
988
|
+
if self._hash_cache is None:
|
|
989
|
+
self._hash_cache = hash((self._label, self._domain.value, self._bounds))
|
|
990
|
+
return self._hash_cache
|
|
986
991
|
|
|
987
992
|
def __eq__(self, other: object) -> bool:
|
|
988
993
|
if not isinstance(other, BaseVariable):
|
|
@@ -1147,6 +1152,8 @@ class Variable(BaseVariable):
|
|
|
1147
1152
|
return super().update_variable(domain, bounds)
|
|
1148
1153
|
|
|
1149
1154
|
def evaluate(self, value: list[int] | RealNumber) -> RealNumber:
|
|
1155
|
+
if not isinstance(value, (list, RealNumber)):
|
|
1156
|
+
raise ValueError("Invalid Value Provided to evaluate a Variable.")
|
|
1150
1157
|
if isinstance(value, int | float):
|
|
1151
1158
|
if not self.domain.check_value(value):
|
|
1152
1159
|
raise ValueError(f"The value {value} is invalid for the domain {self.domain.value}")
|
|
@@ -1228,13 +1235,19 @@ class Parameter(BaseVariable):
|
|
|
1228
1235
|
def value(self) -> RealNumber:
|
|
1229
1236
|
return self._value
|
|
1230
1237
|
|
|
1231
|
-
def
|
|
1238
|
+
def check_value(self, value: RealNumber) -> None:
|
|
1232
1239
|
if not self.domain.check_value(value):
|
|
1233
1240
|
raise ValueError(
|
|
1234
1241
|
f"Parameter value provided ({value}) doesn't correspond to the parameter's domain ({self.domain.name})"
|
|
1235
1242
|
)
|
|
1236
1243
|
if value > self.bounds[1] or value < self.bounds[0]:
|
|
1237
1244
|
raise ValueError(f"The value provided ({value}) is outside the bound of the parameter {self.bounds}")
|
|
1245
|
+
|
|
1246
|
+
def set_value(self, value: RealNumber) -> None:
|
|
1247
|
+
self.check_value(value)
|
|
1248
|
+
|
|
1249
|
+
if isinstance(value, np.generic):
|
|
1250
|
+
value = value.item()
|
|
1238
1251
|
self._value = value
|
|
1239
1252
|
|
|
1240
1253
|
def num_binary_equivalent(self) -> int: # noqa: PLR6301
|
|
@@ -1244,7 +1257,7 @@ class Parameter(BaseVariable):
|
|
|
1244
1257
|
"""
|
|
1245
1258
|
return 0
|
|
1246
1259
|
|
|
1247
|
-
def evaluate(self, value: list[int] | RealNumber =
|
|
1260
|
+
def evaluate(self, value: list[int] | RealNumber | None = None) -> RealNumber:
|
|
1248
1261
|
"""Evaluates the value of the variable given a binary string or a number.
|
|
1249
1262
|
|
|
1250
1263
|
Args:
|
|
@@ -1256,6 +1269,11 @@ class Parameter(BaseVariable):
|
|
|
1256
1269
|
Returns:
|
|
1257
1270
|
float: the evaluated vale of the variable.
|
|
1258
1271
|
"""
|
|
1272
|
+
if value is not None:
|
|
1273
|
+
if isinstance(value, RealNumber):
|
|
1274
|
+
self.check_value(value)
|
|
1275
|
+
return value
|
|
1276
|
+
raise NotImplementedError("Evaluating the value of a parameter with a list is not supported.")
|
|
1259
1277
|
return self.value
|
|
1260
1278
|
|
|
1261
1279
|
def to_binary(self) -> Term:
|
|
@@ -1290,6 +1308,40 @@ class Parameter(BaseVariable):
|
|
|
1290
1308
|
|
|
1291
1309
|
self.set_bounds(lower_bound=bounds[0], upper_bound=bounds[1])
|
|
1292
1310
|
|
|
1311
|
+
__hash__ = BaseVariable.__hash__
|
|
1312
|
+
|
|
1313
|
+
def __eq__(self, other: object) -> bool:
|
|
1314
|
+
if isinstance(other, BaseVariable):
|
|
1315
|
+
return super().__eq__(other)
|
|
1316
|
+
if isinstance(other, (float, int)):
|
|
1317
|
+
return self.value == other
|
|
1318
|
+
return False
|
|
1319
|
+
|
|
1320
|
+
def __ne__(self, other: object) -> bool:
|
|
1321
|
+
if isinstance(other, (float, int)):
|
|
1322
|
+
return self.value != other
|
|
1323
|
+
return NotImplemented
|
|
1324
|
+
|
|
1325
|
+
def __le__(self, other: object) -> bool:
|
|
1326
|
+
if isinstance(other, (float, int)):
|
|
1327
|
+
return self.value <= other
|
|
1328
|
+
return NotImplemented
|
|
1329
|
+
|
|
1330
|
+
def __lt__(self, other: object) -> bool:
|
|
1331
|
+
if isinstance(other, (float, int)):
|
|
1332
|
+
return self.value < other
|
|
1333
|
+
return NotImplemented
|
|
1334
|
+
|
|
1335
|
+
def __ge__(self, other: object) -> bool:
|
|
1336
|
+
if isinstance(other, (float, int)):
|
|
1337
|
+
return self.value >= other
|
|
1338
|
+
return NotImplemented
|
|
1339
|
+
|
|
1340
|
+
def __gt__(self, other: object) -> bool:
|
|
1341
|
+
if isinstance(other, (float, int)):
|
|
1342
|
+
return self.value > other
|
|
1343
|
+
return NotImplemented
|
|
1344
|
+
|
|
1293
1345
|
|
|
1294
1346
|
# Terms ###
|
|
1295
1347
|
|
|
@@ -1468,7 +1520,7 @@ class Term:
|
|
|
1468
1520
|
Returns:
|
|
1469
1521
|
(Term | BaseVariable): the simplified term.
|
|
1470
1522
|
"""
|
|
1471
|
-
if len(self) == 1:
|
|
1523
|
+
if len(self) == 1 and not isinstance(self, MathematicalMap):
|
|
1472
1524
|
item = next(iter(self._elements.keys()))
|
|
1473
1525
|
if self._elements[item] == 1:
|
|
1474
1526
|
return item
|
|
@@ -1586,7 +1638,13 @@ class Term:
|
|
|
1586
1638
|
_var_values = dict(var_values)
|
|
1587
1639
|
for var in self.variables():
|
|
1588
1640
|
if isinstance(var, Parameter):
|
|
1589
|
-
|
|
1641
|
+
if var not in _var_values:
|
|
1642
|
+
_var_values[var] = var.value
|
|
1643
|
+
else:
|
|
1644
|
+
value = _var_values[var]
|
|
1645
|
+
if not isinstance(value, RealNumber):
|
|
1646
|
+
raise ValueError(f"setting a parameter ({var}) value with a list is not supported.")
|
|
1647
|
+
# var.set_value(value)
|
|
1590
1648
|
if var not in _var_values:
|
|
1591
1649
|
raise ValueError(f"Can not evaluate term because the value of the variable {var} is not provided.")
|
|
1592
1650
|
output = complex(0.0) if self.operation in {Operation.ADD, Operation.SUB} else complex(1.0)
|
|
@@ -1967,3 +2025,88 @@ class ComparisonTerm:
|
|
|
1967
2025
|
"Symbolic Constraint Term objects do not have an inherent truth value. "
|
|
1968
2026
|
"Use a method like .evaluate() to obtain a Boolean value."
|
|
1969
2027
|
)
|
|
2028
|
+
|
|
2029
|
+
def __hash__(self) -> int:
|
|
2030
|
+
return hash((hash(self._lhs), self.operation, hash(self._rhs)))
|
|
2031
|
+
|
|
2032
|
+
def __eq__(self, other: object) -> bool:
|
|
2033
|
+
if not isinstance(other, ComparisonTerm):
|
|
2034
|
+
return False
|
|
2035
|
+
return hash(self) == hash(other)
|
|
2036
|
+
|
|
2037
|
+
|
|
2038
|
+
class MathematicalMap(Term, ABC):
|
|
2039
|
+
"""Base class for applying a mathematical map (e.g., sin, cos) to a single term or parameter."""
|
|
2040
|
+
|
|
2041
|
+
MATH_SYMBOL = ""
|
|
2042
|
+
|
|
2043
|
+
@overload
|
|
2044
|
+
def __init__(self, arg: Term, /) -> None: ...
|
|
2045
|
+
@overload
|
|
2046
|
+
def __init__(self, arg: Parameter, /) -> None: ...
|
|
2047
|
+
@overload
|
|
2048
|
+
def __init__(self, arg: BaseVariable, /) -> None: ...
|
|
2049
|
+
|
|
2050
|
+
def __init__(self, arg: Term | Parameter | BaseVariable) -> None:
|
|
2051
|
+
if isinstance(arg, Term):
|
|
2052
|
+
self._initialize_with_term(arg)
|
|
2053
|
+
elif isinstance(arg, Parameter):
|
|
2054
|
+
self._initialize_with_parameter(arg)
|
|
2055
|
+
elif isinstance(arg, BaseVariable):
|
|
2056
|
+
self._initialize_with_variable(arg)
|
|
2057
|
+
else:
|
|
2058
|
+
raise TypeError("Sin expects Term | Parameter | BaseVariable")
|
|
2059
|
+
|
|
2060
|
+
def _initialize_with_term(self, term: Term) -> None:
|
|
2061
|
+
super().__init__(elements=[term], operation=Operation.MATH_MAP)
|
|
2062
|
+
|
|
2063
|
+
def _initialize_with_parameter(self, parameter: Parameter) -> None:
|
|
2064
|
+
super().__init__(elements=[parameter], operation=Operation.MATH_MAP)
|
|
2065
|
+
|
|
2066
|
+
def _initialize_with_variable(self, variable: BaseVariable) -> None:
|
|
2067
|
+
super().__init__(elements=[variable], operation=Operation.MATH_MAP)
|
|
2068
|
+
|
|
2069
|
+
@abstractmethod
|
|
2070
|
+
def _apply_mathematical_map(self, value: Number) -> Number: ...
|
|
2071
|
+
|
|
2072
|
+
def evaluate(self, var_values: Mapping[BaseVariable, list[int] | RealNumber]) -> Number:
|
|
2073
|
+
value: Number = 0
|
|
2074
|
+
|
|
2075
|
+
for e in self:
|
|
2076
|
+
if e not in var_values and isinstance(e, Parameter):
|
|
2077
|
+
aux: Number = e.evaluate()
|
|
2078
|
+
else:
|
|
2079
|
+
aux = e.evaluate(var_values) if isinstance(e, Term) else e.evaluate(var_values[e])
|
|
2080
|
+
|
|
2081
|
+
value += aux * self[e]
|
|
2082
|
+
|
|
2083
|
+
return self._apply_mathematical_map(value)
|
|
2084
|
+
|
|
2085
|
+
def __repr__(self) -> str:
|
|
2086
|
+
return f"{self.MATH_SYMBOL}[{super().__repr__()}]"
|
|
2087
|
+
|
|
2088
|
+
__str__ = __repr__
|
|
2089
|
+
|
|
2090
|
+
|
|
2091
|
+
class Sin(MathematicalMap):
|
|
2092
|
+
"""Apply a sine map to a parameter or term."""
|
|
2093
|
+
|
|
2094
|
+
MATH_SYMBOL = "sin"
|
|
2095
|
+
|
|
2096
|
+
def _apply_mathematical_map(self, value: Number) -> Number: # noqa: PLR6301
|
|
2097
|
+
return float(np.sin(_assert_real(value)))
|
|
2098
|
+
|
|
2099
|
+
def __copy__(self) -> Sin:
|
|
2100
|
+
return Sin(super().__copy__())
|
|
2101
|
+
|
|
2102
|
+
|
|
2103
|
+
class Cos(MathematicalMap):
|
|
2104
|
+
"""Apply a cosine map to a parameter or term."""
|
|
2105
|
+
|
|
2106
|
+
MATH_SYMBOL = "cos"
|
|
2107
|
+
|
|
2108
|
+
def _apply_mathematical_map(self, value: Number) -> Number: # noqa: PLR6301
|
|
2109
|
+
return float(np.cos(_assert_real(value)))
|
|
2110
|
+
|
|
2111
|
+
def __copy__(self) -> Cos:
|
|
2112
|
+
return Cos(super().__copy__())
|
qilisdk/digital/circuit.py
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
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 qilisdk.digital import Circuit
|
|
15
|
+
|
|
16
|
+
from .circuit_transpiler_passes import CircuitTranspilerPass, DecomposeMultiControlledGatesPass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CircuitTranspiler:
|
|
20
|
+
"""Apply an ordered pipeline of circuit transpilation passes.
|
|
21
|
+
|
|
22
|
+
The transpiler acts as a thin orchestrator: each pass receives the circuit from the previous
|
|
23
|
+
pass and must return a brand-new circuit, allowing both structural rewrites and device-specific
|
|
24
|
+
lowering steps to be chained deterministically. Today the pipeline defaults to a single
|
|
25
|
+
`DecomposeMultiControlledGatesPass`, but the API is designed so additional passes—e.g. layout,
|
|
26
|
+
routing, or hardware-aware optimizers—can be composed in future iterations without changing
|
|
27
|
+
backend code.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
pipeline (list[CircuitTranspilerPass] | None): Sequential list of passes to execute while transpiling.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, pipeline: list[CircuitTranspilerPass] | None = None) -> None:
|
|
34
|
+
self._pipeline = pipeline or [DecomposeMultiControlledGatesPass()]
|
|
35
|
+
|
|
36
|
+
def transpile(self, circuit: Circuit) -> Circuit:
|
|
37
|
+
"""Run the configured pass pipeline over the provided circuit.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
circuit (Circuit): Circuit to be rewritten by the transpiler passes.
|
|
41
|
+
Returns:
|
|
42
|
+
Circuit: The circuit returned by the last pass in the pipeline.
|
|
43
|
+
"""
|
|
44
|
+
for transpiler_pass in self._pipeline:
|
|
45
|
+
circuit = transpiler_pass.run(circuit)
|
|
46
|
+
return circuit
|
|
@@ -0,0 +1,18 @@
|
|
|
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 .circuit_transpiler_pass import CircuitTranspilerPass
|
|
16
|
+
from .decompose_multi_controlled_gates_pass import DecomposeMultiControlledGatesPass
|
|
17
|
+
|
|
18
|
+
__all__ = ["CircuitTranspilerPass", "DecomposeMultiControlledGatesPass"]
|
|
@@ -0,0 +1,36 @@
|
|
|
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 abc import ABC, abstractmethod
|
|
16
|
+
|
|
17
|
+
from qilisdk.digital import Circuit
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CircuitTranspilerPass(ABC):
|
|
21
|
+
"""Base class for non-mutating circuit transpiler passes.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
CircuitTranspilerPass: Instances expose the `run` API required by the transpiler.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def run(self, circuit: Circuit) -> Circuit:
|
|
29
|
+
"""Create a new circuit built from `circuit` without mutating the input.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
circuit (Circuit): Circuit to be transpiled.
|
|
33
|
+
Returns:
|
|
34
|
+
Circuit: Newly transpiled circuit.
|
|
35
|
+
"""
|
|
36
|
+
...
|