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.
Files changed (34) hide show
  1. qilisdk/analog/__init__.py +1 -2
  2. qilisdk/analog/hamiltonian.py +1 -68
  3. qilisdk/analog/schedule.py +288 -313
  4. qilisdk/backends/backend.py +5 -1
  5. qilisdk/backends/cuda_backend.py +9 -5
  6. qilisdk/backends/qutip_backend.py +23 -12
  7. qilisdk/core/__init__.py +4 -0
  8. qilisdk/core/interpolator.py +406 -0
  9. qilisdk/core/parameterizable.py +66 -10
  10. qilisdk/core/variables.py +150 -7
  11. qilisdk/digital/circuit.py +1 -0
  12. qilisdk/digital/circuit_transpiler.py +46 -0
  13. qilisdk/digital/circuit_transpiler_passes/__init__.py +18 -0
  14. qilisdk/digital/circuit_transpiler_passes/circuit_transpiler_pass.py +36 -0
  15. qilisdk/digital/circuit_transpiler_passes/decompose_multi_controlled_gates_pass.py +216 -0
  16. qilisdk/digital/circuit_transpiler_passes/numeric_helpers.py +82 -0
  17. qilisdk/digital/gates.py +12 -2
  18. qilisdk/{speqtrum/experiments → experiments}/__init__.py +13 -2
  19. qilisdk/{speqtrum/experiments → experiments}/experiment_functional.py +90 -2
  20. qilisdk/{speqtrum/experiments → experiments}/experiment_result.py +16 -0
  21. qilisdk/functionals/sampling.py +8 -1
  22. qilisdk/functionals/time_evolution.py +6 -2
  23. qilisdk/functionals/variational_program.py +58 -0
  24. qilisdk/speqtrum/speqtrum.py +360 -130
  25. qilisdk/speqtrum/speqtrum_models.py +108 -19
  26. qilisdk/utils/openfermion/__init__.py +38 -0
  27. qilisdk/{core/algorithm.py → utils/openfermion/__init__.pyi} +2 -3
  28. qilisdk/utils/openfermion/openfermion.py +45 -0
  29. qilisdk/utils/visualization/schedule_renderers.py +16 -8
  30. {qilisdk-0.1.6.dist-info → qilisdk-0.1.7.dist-info}/METADATA +74 -24
  31. {qilisdk-0.1.6.dist-info → qilisdk-0.1.7.dist-info}/RECORD +33 -26
  32. {qilisdk-0.1.6.dist-info → qilisdk-0.1.7.dist-info}/WHEEL +1 -1
  33. qilisdk/analog/linear_schedule.py +0 -121
  34. {qilisdk-0.1.6.dist-info → qilisdk-0.1.7.dist-info}/licenses/LICENCE +0 -0
@@ -13,28 +13,38 @@
13
13
  # limitations under the License.
14
14
  from __future__ import annotations
15
15
 
16
- from abc import ABC, abstractmethod
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
- return hash((self._label, self._domain.value, self._bounds))
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 set_value(self, value: RealNumber) -> None:
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 = 0) -> 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
- _var_values[var] = var.value
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__())
@@ -32,6 +32,7 @@ class Circuit(Parameterizable):
32
32
  Args:
33
33
  nqubits (int): The number of qubits in the circuit.
34
34
  """
35
+ super(Circuit, self).__init__()
35
36
  self._nqubits: int = nqubits
36
37
  self._gates: list[Gate] = []
37
38
  self._init_state: np.ndarray = np.zeros(nqubits)
@@ -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
+ ...