classiq 0.61.0__py3-none-any.whl → 0.63.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. classiq/__init__.py +3 -0
  2. classiq/_internals/api_wrapper.py +6 -26
  3. classiq/_internals/client.py +1 -9
  4. classiq/applications/chemistry/chemistry_model_constructor.py +1 -1
  5. classiq/applications/combinatorial_helpers/combinatorial_problem_utils.py +26 -8
  6. classiq/applications/combinatorial_helpers/optimization_model.py +13 -2
  7. classiq/applications/combinatorial_helpers/pyomo_utils.py +143 -13
  8. classiq/applications/combinatorial_optimization/combinatorial_optimization_model_constructor.py +1 -1
  9. classiq/applications/combinatorial_optimization/combinatorial_problem.py +58 -23
  10. classiq/applications/grover/grover_model_constructor.py +1 -1
  11. classiq/applications/libraries/qmci_library.py +2 -1
  12. classiq/execution/execution_session.py +66 -96
  13. classiq/execution/jobs.py +12 -10
  14. classiq/interface/_version.py +1 -1
  15. classiq/interface/backend/backend_preferences.py +26 -5
  16. classiq/interface/backend/pydantic_backend.py +1 -1
  17. classiq/interface/backend/quantum_backend_providers.py +3 -1
  18. classiq/interface/chemistry/operator.py +0 -204
  19. classiq/interface/execution/primitives.py +1 -0
  20. classiq/interface/generator/compiler_keywords.py +4 -0
  21. classiq/interface/generator/copy.py +47 -0
  22. classiq/interface/generator/function_param_list_without_self_reference.py +2 -0
  23. classiq/interface/generator/functions/type_name.py +6 -0
  24. classiq/interface/generator/generated_circuit_data.py +22 -7
  25. classiq/interface/generator/model/model.py +3 -0
  26. classiq/interface/generator/model/preferences/preferences.py +14 -1
  27. classiq/interface/generator/quantum_function_call.py +4 -2
  28. classiq/interface/generator/types/compilation_metadata.py +2 -1
  29. classiq/interface/model/handle_binding.py +50 -5
  30. classiq/interface/model/quantum_type.py +16 -0
  31. classiq/interface/server/routes.py +1 -3
  32. classiq/model_expansions/capturing/captured_vars.py +114 -28
  33. classiq/model_expansions/closure.py +25 -65
  34. classiq/model_expansions/function_builder.py +19 -9
  35. classiq/model_expansions/generative_functions.py +16 -2
  36. classiq/model_expansions/interpreter.py +110 -66
  37. classiq/model_expansions/model_tables.py +4 -0
  38. classiq/model_expansions/quantum_operations/call_emitter.py +83 -20
  39. classiq/model_expansions/quantum_operations/classicalif.py +1 -1
  40. classiq/model_expansions/quantum_operations/control.py +3 -10
  41. classiq/model_expansions/quantum_operations/emitter.py +3 -4
  42. classiq/model_expansions/quantum_operations/quantum_assignment_operation.py +1 -2
  43. classiq/model_expansions/quantum_operations/quantum_function_call.py +1 -1
  44. classiq/model_expansions/quantum_operations/repeat.py +4 -3
  45. classiq/model_expansions/quantum_operations/shallow_emitter.py +9 -3
  46. classiq/model_expansions/scope.py +9 -13
  47. classiq/model_expansions/scope_initialization.py +34 -25
  48. classiq/model_expansions/transformers/var_splitter.py +57 -7
  49. classiq/open_library/__init__.py +4 -0
  50. classiq/open_library/functions/__init__.py +130 -0
  51. classiq/{qmod/builtins → open_library}/functions/amplitude_estimation.py +2 -2
  52. classiq/{qmod/builtins → open_library}/functions/discrete_sine_cosine_transform.py +6 -4
  53. classiq/{qmod/builtins → open_library}/functions/grover.py +2 -2
  54. classiq/{qmod/builtins → open_library}/functions/linear_pauli_rotation.py +1 -1
  55. classiq/{qmod/builtins → open_library}/functions/modular_exponentiation.py +2 -2
  56. classiq/{qmod/builtins → open_library}/functions/qpe.py +2 -2
  57. classiq/{qmod/builtins → open_library}/functions/state_preparation.py +6 -149
  58. classiq/{qmod/builtins → open_library}/functions/swap_test.py +1 -1
  59. classiq/open_library/functions/utility_functions.py +81 -0
  60. classiq/{qmod/builtins → open_library}/functions/variational.py +1 -1
  61. classiq/qmod/builtins/functions/__init__.py +4 -130
  62. classiq/qmod/builtins/functions/allocation.py +150 -0
  63. classiq/qmod/builtins/functions/arithmetic.py +0 -34
  64. classiq/qmod/builtins/functions/operators.py +0 -6
  65. classiq/qmod/builtins/operations.py +19 -80
  66. classiq/qmod/create_model_function.py +8 -162
  67. classiq/qmod/generative.py +0 -16
  68. classiq/qmod/model_state_container.py +7 -0
  69. classiq/qmod/native/pretty_printer.py +10 -11
  70. classiq/qmod/pretty_print/pretty_printer.py +1 -1
  71. classiq/qmod/python_classical_type.py +1 -5
  72. classiq/qmod/qfunc.py +11 -12
  73. classiq/qmod/qmod_variable.py +1 -3
  74. classiq/qmod/quantum_expandable.py +23 -1
  75. classiq/qmod/quantum_function.py +69 -7
  76. {classiq-0.61.0.dist-info → classiq-0.63.0.dist-info}/METADATA +2 -1
  77. {classiq-0.61.0.dist-info → classiq-0.63.0.dist-info}/RECORD +82 -78
  78. classiq/qmod/builtins/functions/utility_functions.py +0 -43
  79. /classiq/{qmod/builtins → open_library}/functions/hea.py +0 -0
  80. /classiq/{qmod/builtins → open_library}/functions/qaoa_penalty.py +0 -0
  81. /classiq/{qmod/builtins → open_library}/functions/qft_functions.py +0 -0
  82. /classiq/{qmod/builtins → open_library}/functions/qsvt.py +0 -0
  83. {classiq-0.61.0.dist-info → classiq-0.63.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,47 @@
1
+ from typing import Optional
2
+
3
+ import pydantic
4
+
5
+ from classiq.interface.generator.arith import argument_utils
6
+ from classiq.interface.generator.arith.register_user_input import RegisterArithmeticInfo
7
+ from classiq.interface.generator.function_params import FunctionParams
8
+
9
+
10
+ class Copy(FunctionParams):
11
+ source: argument_utils.RegisterOrConst
12
+ target: RegisterArithmeticInfo
13
+ output_size: Optional[pydantic.PositiveInt] = pydantic.Field(default=None)
14
+
15
+ @property
16
+ def source_size(self) -> int:
17
+ return argument_utils.size(self.source)
18
+
19
+ @property
20
+ def source_reg_size(self) -> int:
21
+ return (
22
+ self.source.size if isinstance(self.source, RegisterArithmeticInfo) else 0
23
+ )
24
+
25
+ @property
26
+ def source_fraction_places(self) -> int:
27
+ return argument_utils.fraction_places(self.source)
28
+
29
+ @property
30
+ def offset(self) -> int:
31
+ return self.target.fraction_places - self.source_fraction_places
32
+
33
+ @property
34
+ def source_name(self) -> str:
35
+ return "source"
36
+
37
+ @property
38
+ def target_name(self) -> str:
39
+ return "target"
40
+
41
+ def _create_ios(self) -> None:
42
+ self._inputs = {
43
+ self.target_name: self.target,
44
+ }
45
+ if isinstance(self.source, RegisterArithmeticInfo):
46
+ self._inputs[self.source_name] = self.source
47
+ self._outputs = {**self._inputs}
@@ -28,6 +28,7 @@ from classiq.interface.generator.arith.unary_ops import BitwiseInvert, Negation,
28
28
  from classiq.interface.generator.commuting_pauli_exponentiation import (
29
29
  CommutingPauliExponentiation,
30
30
  )
31
+ from classiq.interface.generator.copy import Copy
31
32
  from classiq.interface.generator.entangler_params import (
32
33
  GridEntangler,
33
34
  HypercubeEntangler,
@@ -148,6 +149,7 @@ function_param_library_without_self_reference: FunctionParamLibrary = (
148
149
  PiecewiseLinearAmplitudeLoading,
149
150
  PiecewiseLinearRotationAmplitudeLoading,
150
151
  HadamardTransform,
152
+ Copy,
151
153
  },
152
154
  standard_gate_function_param_library.param_list,
153
155
  oracle_function_param_library.param_list,
@@ -69,6 +69,12 @@ class TypeName(ClassicalType, QuantumType):
69
69
  def set_fields(self, fields: Mapping[str, "ConcreteQuantumType"]) -> None:
70
70
  self._assigned_fields = fields
71
71
 
72
+ @property
73
+ def is_instantiated(self) -> bool:
74
+ return self.has_fields and all(
75
+ field_type.is_instantiated for field_type in self.fields.values()
76
+ )
77
+
72
78
 
73
79
  class Enum(TypeName):
74
80
  pass
@@ -197,14 +197,16 @@ class FunctionDebugInfoInterface(pydantic.BaseModel):
197
197
 
198
198
  updated_children: list[FunctionDebugInfoInterface] = []
199
199
  for child in self.children:
200
- updated_child = child.model_copy(
201
- update=dict(
202
- absolute_qubits=_get_absolute_from_relative(
203
- self.absolute_qubits, child.relative_qubits
204
- )
200
+ updated_child = child.white_new_absolute_qubits(self.absolute_qubits)
201
+ if updated_child.override_debug_info is None:
202
+ updated_child = updated_child.propagate_absolute_qubits()
203
+ else:
204
+ updated_child.override_debug_info = (
205
+ updated_child.override_debug_info.white_new_absolute_qubits(
206
+ absolute_qubits=self.absolute_qubits
207
+ ).propagate_absolute_qubits()
205
208
  )
206
- )
207
- updated_children.append(updated_child.propagate_absolute_qubits())
209
+ updated_children.append(updated_child)
208
210
 
209
211
  return self.model_copy(
210
212
  update=dict(
@@ -213,6 +215,17 @@ class FunctionDebugInfoInterface(pydantic.BaseModel):
213
215
  )
214
216
  )
215
217
 
218
+ def white_new_absolute_qubits(
219
+ self, absolute_qubits: tuple[int, ...]
220
+ ) -> "FunctionDebugInfoInterface":
221
+ return self.model_copy(
222
+ update=dict(
223
+ absolute_qubits=_get_absolute_from_relative(
224
+ absolute_qubits, self.relative_qubits
225
+ )
226
+ )
227
+ )
228
+
216
229
  def inverse(self) -> "FunctionDebugInfoInterface":
217
230
  inverted_children = [child.inverse() for child in self.children[::-1]]
218
231
  return self.model_copy(
@@ -226,6 +239,8 @@ class FunctionDebugInfoInterface(pydantic.BaseModel):
226
239
  def _get_absolute_from_relative(
227
240
  absolute_qubits: tuple[int, ...], relative_qubits: tuple[int, ...]
228
241
  ) -> tuple[int, ...]:
242
+ if len(relative_qubits) == 0:
243
+ return tuple()
229
244
  if max(relative_qubits) >= len(absolute_qubits):
230
245
  _logger.warning(
231
246
  "Invalid qubit computation (relative qubits: %s, absolute qubits: %s)",
@@ -68,3 +68,6 @@ class ExecutionModel(ClassiqBaseModel):
68
68
  description="Mapping between a measured register name and its qmod type",
69
69
  default=dict(),
70
70
  )
71
+ register_filter_bitstrings: dict[str, list[str]] = pydantic.Field(
72
+ default_factory=dict,
73
+ )
@@ -1,4 +1,5 @@
1
1
  from collections.abc import Sequence
2
+ from enum import IntEnum
2
3
  from typing import TYPE_CHECKING, Annotated, Any, Optional, Union
3
4
 
4
5
  import pydantic
@@ -64,6 +65,13 @@ else:
64
65
  ]
65
66
 
66
67
 
68
+ class OptimizationLevel(IntEnum):
69
+ NONE = 0
70
+ LIGHT = 1
71
+ MEDIUM = 2
72
+ HIGH = 3
73
+
74
+
67
75
  class TranspilationOption(StrEnum):
68
76
  NONE = "none"
69
77
  DECOMPOSE = "decompose"
@@ -156,7 +164,12 @@ class Preferences(pydantic.BaseModel, extra="forbid"):
156
164
  )
157
165
  synthesize_all_separately: bool = pydantic.Field(
158
166
  default=False,
159
- description="If true, all functions will be synthesized separately",
167
+ description="If true, a heuristic is used to determine if a function should be synthesized separately",
168
+ deprecated=True,
169
+ )
170
+ optimization_level: OptimizationLevel = pydantic.Field(
171
+ default=OptimizationLevel.HIGH,
172
+ description="The optimization level used during synthesis; determines the trade-off between synthesis speed and the quality of the results",
160
173
  )
161
174
  output_format: PydanticConstrainedQuantumFormatList = pydantic.Field(
162
175
  default=[QuantumFormat.QASM],
@@ -22,7 +22,9 @@ from pydantic_core.core_schema import ValidationInfo
22
22
  from classiq.interface.exceptions import ClassiqControlError, ClassiqValueError
23
23
  from classiq.interface.generator import function_param_list, function_params as f_params
24
24
  from classiq.interface.generator.arith.arithmetic import Arithmetic
25
- from classiq.interface.generator.compiler_keywords import EXPANDED_KEYWORD
25
+ from classiq.interface.generator.compiler_keywords import (
26
+ generate_original_function_name,
27
+ )
26
28
  from classiq.interface.generator.control_state import ControlState
27
29
  from classiq.interface.generator.function_params import (
28
30
  NAME_REGEX,
@@ -237,7 +239,7 @@ class SynthesisQuantumFunctionCall(BaseModel):
237
239
  suffix = f"{SUFFIX_MARKER}_{randomize_suffix()}"
238
240
  if not function or params is None:
239
241
  return name if name else suffix
240
- return f"{function.split(f'_{EXPANDED_KEYWORD}')[0]}_{suffix}"
242
+ return f"{generate_original_function_name(function)}_{suffix}"
241
243
 
242
244
  @pydantic.model_validator(mode="before")
243
245
  @classmethod
@@ -1,5 +1,6 @@
1
- from pydantic import BaseModel, Field
1
+ from pydantic import BaseModel, Field, NonNegativeInt
2
2
 
3
3
 
4
4
  class CompilationMetadata(BaseModel):
5
5
  should_synthesize_separately: bool = Field(default=False)
6
+ occurrences_number: NonNegativeInt = Field(default=1)
@@ -149,8 +149,7 @@ class SubscriptHandleBinding(NestedHandleBinding):
149
149
  if (
150
150
  isinstance(other_handle, SlicedHandleBinding)
151
151
  and self.index.is_evaluated()
152
- and other_handle.start.is_evaluated()
153
- and other_handle.end.is_evaluated()
152
+ and other_handle._is_evaluated()
154
153
  ):
155
154
  return (
156
155
  other_handle.start.to_int_value()
@@ -159,6 +158,26 @@ class SubscriptHandleBinding(NestedHandleBinding):
159
158
  )
160
159
  return False
161
160
 
161
+ def replace_prefix(
162
+ self, prefix: HandleBinding, replacement: HandleBinding
163
+ ) -> HandleBinding:
164
+ if (
165
+ isinstance(prefix, SlicedHandleBinding)
166
+ and self.base_handle == prefix.base_handle
167
+ and self.index.is_evaluated()
168
+ and prefix._is_evaluated()
169
+ and prefix.start.to_int_value()
170
+ <= self.index.to_int_value()
171
+ < prefix.end.to_int_value()
172
+ ):
173
+ return SubscriptHandleBinding(
174
+ base_handle=replacement,
175
+ index=Expression(
176
+ expr=str(self.index.to_int_value() - prefix.start.to_int_value())
177
+ ),
178
+ )
179
+ return super().replace_prefix(prefix, replacement)
180
+
162
181
 
163
182
  class SlicedHandleBinding(NestedHandleBinding):
164
183
  start: Expression
@@ -192,7 +211,7 @@ class SlicedHandleBinding(NestedHandleBinding):
192
211
  )
193
212
 
194
213
  def _tail_overlaps(self, other_handle: "HandleBinding") -> bool:
195
- if not self.start.is_evaluated() or not self.end.is_evaluated():
214
+ if not self._is_evaluated():
196
215
  return False
197
216
  start = self.start.to_int_value()
198
217
  end = self.end.to_int_value()
@@ -203,8 +222,7 @@ class SlicedHandleBinding(NestedHandleBinding):
203
222
  return start <= other_handle.index.to_int_value() < end
204
223
  if (
205
224
  isinstance(other_handle, SlicedHandleBinding)
206
- and other_handle.start.is_evaluated()
207
- and other_handle.end.is_evaluated()
225
+ and other_handle._is_evaluated()
208
226
  ):
209
227
  other_start = other_handle.start.to_int_value()
210
228
  other_end = other_handle.end.to_int_value()
@@ -231,6 +249,33 @@ class SlicedHandleBinding(NestedHandleBinding):
231
249
  )
232
250
  return Expression(expr=f"({self.base_handle.end})-({self.end})")
233
251
 
252
+ def replace_prefix(
253
+ self, prefix: HandleBinding, replacement: HandleBinding
254
+ ) -> HandleBinding:
255
+ if (
256
+ isinstance(prefix, SlicedHandleBinding)
257
+ and self.base_handle == prefix.base_handle
258
+ and self._is_evaluated()
259
+ and prefix._is_evaluated()
260
+ ):
261
+ prefix_start = prefix.start.to_int_value()
262
+ prefix_end = prefix.end.to_int_value()
263
+ self_start = self.start.to_int_value()
264
+ self_end = self.end.to_int_value()
265
+ if (
266
+ prefix_start <= self_start < prefix_end
267
+ and prefix_start < self_end <= prefix_end
268
+ ):
269
+ return SlicedHandleBinding(
270
+ base_handle=replacement,
271
+ start=Expression(expr=str(self_start - prefix_start)),
272
+ end=Expression(expr=str(self_end - prefix_start)),
273
+ )
274
+ return super().replace_prefix(prefix, replacement)
275
+
276
+ def _is_evaluated(self) -> bool:
277
+ return self.start.is_evaluated() and self.end.is_evaluated()
278
+
234
279
 
235
280
  class FieldHandleBinding(NestedHandleBinding):
236
281
  field: str
@@ -59,6 +59,10 @@ class QuantumType(HashableASTNode):
59
59
  def type_name(self) -> str:
60
60
  raise NotImplementedError
61
61
 
62
+ @property
63
+ def is_instantiated(self) -> bool:
64
+ raise NotImplementedError
65
+
62
66
 
63
67
  class QuantumScalar(QuantumType):
64
68
  def get_proxy(self, handle: "HandleBinding") -> QmodQScalarProxy:
@@ -88,6 +92,10 @@ class QuantumBit(QuantumScalar):
88
92
  def type_name(self) -> str:
89
93
  return "Quantum bit"
90
94
 
95
+ @property
96
+ def is_instantiated(self) -> bool:
97
+ return True
98
+
91
99
 
92
100
  class QuantumBitvector(QuantumType):
93
101
  element_type: "ConcreteQuantumType" = Field(
@@ -142,6 +150,10 @@ class QuantumBitvector(QuantumType):
142
150
  def type_name(self) -> str:
143
151
  return "Quantum array"
144
152
 
153
+ @property
154
+ def is_instantiated(self) -> bool:
155
+ return self.length is not None and self.element_type.is_instantiated
156
+
145
157
 
146
158
  class QuantumNumeric(QuantumScalar):
147
159
  kind: Literal["qnum"]
@@ -211,6 +223,10 @@ class QuantumNumeric(QuantumScalar):
211
223
  def type_name(self) -> str:
212
224
  return "Quantum numeric"
213
225
 
226
+ @property
227
+ def is_instantiated(self) -> bool:
228
+ return self.size is not None
229
+
214
230
 
215
231
  class RegisterQuantumType(BaseModel):
216
232
  quantum_types: "ConcreteQuantumType"
@@ -64,9 +64,7 @@ GENERATE_RESOURCE_ESTIMATOR_REPORT = "/resource_estimator_report"
64
64
  TASKS_SOLVE_EXACT_SUFFIX = "/tasks/solve_exact"
65
65
 
66
66
  GENERATE_HAMILTONIAN_SUFFIX = "/generate_hamiltonian"
67
- GENERATE_HAMILTONIAN_FULL_PATH = (
68
- SYNTHESIS_NON_VERSIONED_PREFIX + GENERATE_HAMILTONIAN_SUFFIX
69
- )
67
+ GENERATE_HAMILTONIAN_FULL_PATH = CHEMISTRY_PREFIX + GENERATE_HAMILTONIAN_SUFFIX
70
68
 
71
69
  CONVERSION_GENERATED_CIRCUIT_TO_EXECUTION_INPUT_SUFFIX = "/execution_input"
72
70
  CONVERSION_GENERATED_CIRCUIT_TO_EXECUTION_INPUT_FULL = (
@@ -1,6 +1,5 @@
1
1
  import dataclasses
2
- from collections.abc import Iterator, Sequence
3
- from contextlib import contextmanager
2
+ from collections.abc import Sequence
4
3
  from dataclasses import dataclass, field
5
4
  from typing import TYPE_CHECKING, Optional
6
5
 
@@ -9,13 +8,18 @@ from classiq.interface.exceptions import (
9
8
  ClassiqExpansionError,
10
9
  ClassiqInternalExpansionError,
11
10
  )
11
+ from classiq.interface.generator.expressions.expression import Expression
12
12
  from classiq.interface.generator.functions.port_declaration import (
13
13
  PortDeclarationDirection,
14
14
  )
15
- from classiq.interface.model.handle_binding import HandleBinding, NestedHandleBinding
15
+ from classiq.interface.model.handle_binding import (
16
+ HandleBinding,
17
+ NestedHandleBinding,
18
+ SlicedHandleBinding,
19
+ )
16
20
  from classiq.interface.model.port_declaration import PortDeclaration
17
21
  from classiq.interface.model.quantum_function_call import ArgValue
18
- from classiq.interface.model.quantum_type import QuantumType
22
+ from classiq.interface.model.quantum_type import QuantumBitvector, QuantumType
19
23
  from classiq.interface.model.variable_declaration_statement import (
20
24
  VariableDeclarationStatement,
21
25
  )
@@ -105,6 +109,11 @@ class _CapturedHandle:
105
109
  return dataclasses.replace(self, is_propagated=False)
106
110
  return self
107
111
 
112
+ def set_symbol(
113
+ self, handle: HandleBinding, quantum_type: QuantumType
114
+ ) -> "_CapturedHandle":
115
+ return dataclasses.replace(self, handle=handle, quantum_type=quantum_type)
116
+
108
117
 
109
118
  @dataclass
110
119
  class CapturedVars:
@@ -146,23 +155,10 @@ class CapturedVars:
146
155
  captured_handle = self._conjugate_direction(
147
156
  existing_captured_handle, captured_handle
148
157
  )
149
- elif captured_handle.handle in existing_captured_handle.handle:
150
- if existing_captured_handle.direction in (
151
- PortDirection.Input,
152
- PortDirection.Outin,
153
- ):
154
- raise ClassiqInternalExpansionError(
155
- "Captured handle is already freed"
156
- )
157
- captured_handle = existing_captured_handle
158
- elif existing_captured_handle.handle in captured_handle.handle:
159
- if captured_handle.direction in (
160
- PortDirection.Output,
161
- PortDirection.Outin,
162
- ):
163
- raise ClassiqInternalExpansionError(
164
- "Captured handle is already allocated"
165
- )
158
+ elif captured_handle.handle.overlaps(existing_captured_handle.handle):
159
+ captured_handle = self._intersect_handles(
160
+ existing_captured_handle, captured_handle
161
+ )
166
162
  else:
167
163
  new_captured_handles.append(existing_captured_handle)
168
164
  new_captured_handles.append(captured_handle)
@@ -205,6 +201,92 @@ class CapturedVars:
205
201
  raise ClassiqInternalExpansionError("Captured handle is already freed")
206
202
  return captured_handle
207
203
 
204
+ def _intersect_handles(
205
+ self,
206
+ existing_captured_handle: _CapturedHandle,
207
+ captured_handle: _CapturedHandle,
208
+ ) -> _CapturedHandle:
209
+ if captured_handle.handle in existing_captured_handle.handle:
210
+ if existing_captured_handle.direction in (
211
+ PortDirection.Input,
212
+ PortDirection.Outin,
213
+ ):
214
+ raise ClassiqInternalExpansionError("Captured handle is already freed")
215
+ return existing_captured_handle
216
+
217
+ if existing_captured_handle.handle in captured_handle.handle:
218
+ if captured_handle.direction in (
219
+ PortDirection.Output,
220
+ PortDirection.Outin,
221
+ ):
222
+ raise ClassiqInternalExpansionError(
223
+ "Captured handle is already allocated"
224
+ )
225
+ return captured_handle
226
+
227
+ sliced_handle, quantum_type, other_handle = self._get_sliced_handle(
228
+ existing_captured_handle, captured_handle
229
+ )
230
+ if not isinstance(other_handle, SlicedHandleBinding):
231
+ return captured_handle.set_symbol(sliced_handle, quantum_type)
232
+
233
+ merged_handle, merged_quantum_type = self._merge_sliced_handles(
234
+ sliced_handle, other_handle, quantum_type
235
+ )
236
+ return captured_handle.set_symbol(merged_handle, merged_quantum_type)
237
+
238
+ @staticmethod
239
+ def _get_sliced_handle(
240
+ existing_captured_handle: _CapturedHandle,
241
+ captured_handle: _CapturedHandle,
242
+ ) -> tuple[SlicedHandleBinding, QuantumBitvector, HandleBinding]:
243
+ handle_1 = existing_captured_handle.handle
244
+ quantum_type_1 = existing_captured_handle.quantum_type
245
+ handle_2 = captured_handle.handle
246
+ quantum_type_2 = captured_handle.quantum_type
247
+ if isinstance(handle_1, SlicedHandleBinding):
248
+ sliced_handle = handle_1
249
+ other_handle = handle_2
250
+ quantum_type = quantum_type_1
251
+ elif isinstance(handle_2, SlicedHandleBinding):
252
+ sliced_handle = handle_2
253
+ other_handle = handle_1
254
+ quantum_type = quantum_type_2
255
+ else:
256
+ raise ClassiqInternalExpansionError(
257
+ f"Unexpected overlapping handles {handle_1} and {handle_2}"
258
+ )
259
+ if not isinstance(quantum_type, QuantumBitvector):
260
+ raise ClassiqInternalExpansionError
261
+ return sliced_handle, quantum_type, other_handle
262
+
263
+ @staticmethod
264
+ def _merge_sliced_handles(
265
+ handle_1: SlicedHandleBinding,
266
+ handle_2: SlicedHandleBinding,
267
+ quantum_type: QuantumBitvector,
268
+ ) -> tuple[HandleBinding, QuantumBitvector]:
269
+ if (
270
+ not handle_1.start.is_evaluated()
271
+ or not handle_1.end.is_evaluated()
272
+ or not handle_2.start.is_evaluated()
273
+ or not handle_2.end.is_evaluated()
274
+ ):
275
+ raise ClassiqInternalExpansionError
276
+
277
+ new_start = min(handle_1.start.to_int_value(), handle_2.start.to_int_value())
278
+ new_end = max(handle_1.end.to_int_value(), handle_2.end.to_int_value())
279
+ merged_handle = SlicedHandleBinding(
280
+ base_handle=handle_1.base_handle,
281
+ start=Expression(expr=str(new_start)),
282
+ end=Expression(expr=str(new_end)),
283
+ )
284
+ merged_quantum_type = QuantumBitvector(
285
+ element_type=quantum_type.element_type,
286
+ length=Expression(expr=str(new_end - new_start)),
287
+ )
288
+ return merged_handle, merged_quantum_type
289
+
208
290
  def update(self, other_captured_vars: "CapturedVars") -> None:
209
291
  for captured_handle in other_captured_vars._captured_handles:
210
292
  self._capture_handle(captured_handle)
@@ -277,11 +359,8 @@ class CapturedVars:
277
359
  if not captured_handle.is_propagated
278
360
  }
279
361
 
280
- @contextmanager
281
- def freeze(self) -> Iterator[None]:
282
- previous = self._captured_handles
283
- yield
284
- self._captured_handles = previous
362
+ def clone(self) -> "CapturedVars":
363
+ return CapturedVars(_captured_handles=list(self._captured_handles))
285
364
 
286
365
 
287
366
  def _same_closure(closure_1: "FunctionClosure", closure_2: "FunctionClosure") -> bool:
@@ -311,7 +390,9 @@ def validate_args_are_not_propagated(
311
390
  )
312
391
 
313
392
 
314
- def validate_captured_directions(captured_vars: CapturedVars) -> None:
393
+ def validate_captured_directions(
394
+ captured_vars: CapturedVars, report_outin: bool = True
395
+ ) -> None:
315
396
  captured_inputs = [
316
397
  captured_handle.handle.name
317
398
  for captured_handle in captured_vars._captured_handles
@@ -320,7 +401,12 @@ def validate_captured_directions(captured_vars: CapturedVars) -> None:
320
401
  captured_outputs = [
321
402
  captured_handle.handle.name
322
403
  for captured_handle in captured_vars._captured_handles
323
- if captured_handle.direction == PortDirection.Output
404
+ if captured_handle.direction
405
+ in (
406
+ (PortDirection.Output, PortDirection.Outin)
407
+ if report_outin
408
+ else (PortDirection.Output,)
409
+ )
324
410
  ]
325
411
  if len(captured_inputs) > 0:
326
412
  raise ClassiqExpansionError(