classiq 0.84.0__py3-none-any.whl → 0.85.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 (40) hide show
  1. classiq/applications/combinatorial_optimization/combinatorial_problem.py +20 -42
  2. classiq/evaluators/classical_expression.py +32 -15
  3. classiq/execution/execution_session.py +49 -6
  4. classiq/interface/_version.py +1 -1
  5. classiq/interface/debug_info/debug_info.py +0 -4
  6. classiq/interface/generator/expressions/atomic_expression_functions.py +5 -0
  7. classiq/interface/generator/functions/builtins/internal_operators.py +2 -0
  8. classiq/interface/generator/functions/concrete_types.py +20 -0
  9. classiq/interface/generator/generated_circuit_data.py +5 -10
  10. classiq/interface/ide/operation_registry.py +45 -0
  11. classiq/interface/ide/visual_model.py +62 -1
  12. classiq/interface/model/bounds.py +12 -2
  13. classiq/interface/model/quantum_expressions/arithmetic_operation.py +7 -4
  14. classiq/interface/model/variable_declaration_statement.py +33 -6
  15. classiq/model_expansions/atomic_expression_functions_defs.py +5 -1
  16. classiq/model_expansions/function_builder.py +4 -1
  17. classiq/model_expansions/interpreters/generative_interpreter.py +16 -1
  18. classiq/model_expansions/quantum_operations/assignment_result_processor.py +63 -21
  19. classiq/model_expansions/quantum_operations/bounds.py +7 -1
  20. classiq/model_expansions/quantum_operations/classical_var_emitter.py +16 -0
  21. classiq/model_expansions/quantum_operations/variable_decleration.py +30 -10
  22. classiq/model_expansions/scope.py +7 -0
  23. classiq/model_expansions/transformers/var_splitter.py +1 -1
  24. classiq/open_library/functions/__init__.py +0 -2
  25. classiq/open_library/functions/qaoa_penalty.py +8 -1
  26. classiq/open_library/functions/state_preparation.py +1 -32
  27. classiq/qmod/__init__.py +2 -0
  28. classiq/qmod/builtins/operations.py +65 -1
  29. classiq/qmod/classical_variable.py +74 -0
  30. classiq/qmod/native/pretty_printer.py +18 -14
  31. classiq/qmod/pretty_print/pretty_printer.py +34 -15
  32. classiq/qmod/qfunc.py +2 -19
  33. classiq/qmod/qmod_variable.py +5 -8
  34. classiq/qmod/quantum_expandable.py +1 -1
  35. classiq/qmod/quantum_function.py +42 -2
  36. classiq/qmod/symbolic_type.py +2 -1
  37. {classiq-0.84.0.dist-info → classiq-0.85.0.dist-info}/METADATA +1 -1
  38. {classiq-0.84.0.dist-info → classiq-0.85.0.dist-info}/RECORD +39 -37
  39. classiq/interface/model/quantum_variable_declaration.py +0 -7
  40. {classiq-0.84.0.dist-info → classiq-0.85.0.dist-info}/WHEEL +0 -0
@@ -1,12 +1,11 @@
1
1
  import math
2
2
  import re
3
- from typing import Callable, Optional
3
+ from typing import Callable, Optional, cast
4
4
 
5
5
  import numpy as np
6
6
  import pandas as pd
7
7
  import pyomo.core as pyo
8
8
  import scipy
9
- from tqdm import tqdm
10
9
 
11
10
  from classiq.interface.executor.execution_preferences import ExecutionPreferences
12
11
  from classiq.interface.executor.result import ExecutionDetails
@@ -44,8 +43,8 @@ class CombinatorialProblem:
44
43
  self.num_layers_ = num_layers
45
44
  self.model_ = None
46
45
  self.qprog_ = None
47
- self.es_ = None
48
- self.optimized_params_ = None
46
+ self._es: ExecutionSession | None = None
47
+ self._optimized_params: list[float] | None = None
49
48
  self.params_trace_: list[np.ndarray] = []
50
49
  self.cost_trace_: list = []
51
50
 
@@ -58,8 +57,8 @@ class CombinatorialProblem:
58
57
  return self.params_trace_
59
58
 
60
59
  @property
61
- def optimized_params(self) -> list:
62
- return self.optimized_params_ # type:ignore[return-value]
60
+ def optimized_params(self) -> list[float]:
61
+ return self._optimized_params # type:ignore[return-value]
63
62
 
64
63
  def get_model(
65
64
  self,
@@ -100,22 +99,9 @@ class CombinatorialProblem:
100
99
  ) -> list[float]:
101
100
  if self.qprog_ is None:
102
101
  self.get_qprog()
103
- self.es_ = ExecutionSession(
104
- self.qprog_, execution_preferences # type:ignore[assignment,arg-type]
102
+ _es = ExecutionSession(
103
+ self.qprog_, execution_preferences # type:ignore[arg-type]
105
104
  )
106
- self.params_trace_ = []
107
- self.cost_trace_ = []
108
-
109
- def estimate_cost_wrapper(params: np.ndarray) -> float:
110
- cost = self.es_.estimate_cost( # type:ignore[attr-defined]
111
- lambda state: self.cost_func(state["v"]),
112
- {"params": params.tolist()},
113
- quantile=quantile,
114
- )
115
- self.cost_trace_.append(cost)
116
- self.params_trace_.append(params)
117
- return cost
118
-
119
105
  initial_params = (
120
106
  np.concatenate(
121
107
  (
@@ -125,31 +111,23 @@ class CombinatorialProblem:
125
111
  )
126
112
  * math.pi
127
113
  )
128
-
129
- with tqdm(total=maxiter, desc="Optimization Progress", leave=True) as pbar:
130
-
131
- def _minimze_callback(xk: np.ndarray) -> None:
132
- pbar.update(1) # increment progress bar
133
- self.optimized_params_ = xk.tolist() # save recent optimized value
134
-
135
- self.optimized_params_ = scipy.optimize.minimize(
136
- estimate_cost_wrapper,
137
- callback=_minimze_callback,
138
- x0=initial_params,
139
- method="COBYLA",
140
- options={"maxiter": maxiter},
141
- ).x.tolist()
142
-
143
- return self.optimized_params_ # type:ignore[return-value]
114
+ result = _es.minimize(
115
+ lambda v: self.cost_func(v), # type:ignore[arg-type]
116
+ {"params": initial_params.tolist()},
117
+ maxiter,
118
+ quantile,
119
+ )
120
+ _optimized_params = cast(list[float], result[-1][1]["params"])
121
+ self._optimized_params = _optimized_params
122
+ self._es = _es
123
+ return _optimized_params
144
124
 
145
125
  def sample_uniform(self) -> pd.DataFrame:
146
126
  return self.sample([0] * self.num_layers_ * 2)
147
127
 
148
128
  def sample(self, params: list) -> pd.DataFrame:
149
- assert self.es_ is not None
150
- res = self.es_.sample( # type:ignore[unreachable]
151
- {"params": params}
152
- )
129
+ assert self._es is not None
130
+ res = self._es.sample({"params": params})
153
131
  parsed_result = [
154
132
  {
155
133
  "solution": {
@@ -157,7 +135,7 @@ class CombinatorialProblem:
157
135
  for key, value in sampled.state["v"].items()
158
136
  if not re.match(".*_slack_var_.*", key)
159
137
  },
160
- "probability": sampled.shots / res.num_shots,
138
+ "probability": sampled.shots / res.num_shots, # type:ignore[operator]
161
139
  "cost": self.cost_func(sampled.state["v"]),
162
140
  }
163
141
  for sampled in res.parsed_counts
@@ -11,26 +11,43 @@ from classiq.interface.generator.expressions.proxies.classical.any_classical_val
11
11
  from classiq.interface.model.handle_binding import HandleBinding
12
12
 
13
13
  from classiq.evaluators.expression_evaluator import evaluate
14
- from classiq.model_expansions.scope import Evaluated, QuantumSymbol, Scope
14
+ from classiq.model_expansions.scope import (
15
+ ClassicalSymbol,
16
+ Evaluated,
17
+ QuantumSymbol,
18
+ Scope,
19
+ )
15
20
 
16
21
 
17
22
  def evaluate_classical_expression(expr: Expression, scope: Scope) -> Evaluated:
18
23
  all_symbols = scope.items()
19
- locals_dict = {
20
- name: EvaluatedExpression(value=evaluated.value)
21
- for name, evaluated in all_symbols
22
- if isinstance(evaluated.value, get_args(ExpressionValue))
23
- } | {
24
- name: EvaluatedExpression(
25
- value=(
26
- evaluated.value.quantum_type.get_proxy(HandleBinding(name=name))
27
- if evaluated.value.quantum_type.is_evaluated
28
- else AnyClassicalValue(name)
24
+ locals_dict = (
25
+ {
26
+ name: EvaluatedExpression(value=evaluated.value)
27
+ for name, evaluated in all_symbols
28
+ if isinstance(evaluated.value, get_args(ExpressionValue))
29
+ }
30
+ | {
31
+ name: EvaluatedExpression(
32
+ value=(
33
+ evaluated.value.quantum_type.get_proxy(HandleBinding(name=name))
34
+ if evaluated.value.quantum_type.is_evaluated
35
+ else AnyClassicalValue(name)
36
+ )
37
+ )
38
+ for name, evaluated in all_symbols
39
+ if isinstance(evaluated.value, QuantumSymbol)
40
+ }
41
+ | {
42
+ name: EvaluatedExpression(
43
+ value=evaluated.value.classical_type.get_classical_proxy(
44
+ HandleBinding(name=name)
45
+ )
29
46
  )
30
- )
31
- for name, evaluated in all_symbols
32
- if isinstance(evaluated.value, QuantumSymbol)
33
- }
47
+ for name, evaluated in all_symbols
48
+ if isinstance(evaluated.value, ClassicalSymbol)
49
+ }
50
+ )
34
51
 
35
52
  ret = evaluate(expr, locals_dict)
36
53
  return Evaluated(value=ret.value)
@@ -1,10 +1,15 @@
1
1
  import inspect
2
2
  import random
3
+ import warnings
3
4
  from types import TracebackType
4
- from typing import Callable, Optional, Union, cast
5
+ from typing import Any, Callable, Optional, Union, cast
5
6
 
6
7
  from classiq.interface.chemistry.operator import PauliOperator, pauli_integers_to_str
7
- from classiq.interface.exceptions import ClassiqError, ClassiqValueError
8
+ from classiq.interface.exceptions import (
9
+ ClassiqDeprecationWarning,
10
+ ClassiqError,
11
+ ClassiqValueError,
12
+ )
8
13
  from classiq.interface.execution.primitives import (
9
14
  EstimateInput,
10
15
  MinimizeClassicalCostInput,
@@ -71,6 +76,20 @@ def parse_params(params: ExecutionParams) -> ParsedExecutionParams:
71
76
  return result
72
77
 
73
78
 
79
+ def _hamiltonian_deprecation_warning(hamiltonian: Any) -> None:
80
+ if isinstance(hamiltonian, list):
81
+ warnings.warn(
82
+ (
83
+ "Parameter type list[PauliTerm] to 'ExecutionSession' methods is "
84
+ "deprecated and will no longer be supported starting on 21/7/2025 "
85
+ "at the earliest. Instead, send a 'SparsePauliOp' (see "
86
+ "https://docs.classiq.io/latest/qmod-reference/language-reference/classical-types/#hamiltonians)."
87
+ ),
88
+ ClassiqDeprecationWarning,
89
+ stacklevel=3,
90
+ )
91
+
92
+
74
93
  class ExecutionSession:
75
94
  """
76
95
  A session for executing a quantum program.
@@ -228,11 +247,18 @@ class ExecutionSession:
228
247
  Returns:
229
248
  EstimationResult: The result of the estimation.
230
249
  """
231
- job = self.submit_estimate(hamiltonian=hamiltonian, parameters=parameters)
250
+ _hamiltonian_deprecation_warning(hamiltonian)
251
+ job = self.submit_estimate(
252
+ hamiltonian=hamiltonian, parameters=parameters, _check_deprecation=False
253
+ )
232
254
  return job.get_estimate_result(_http_client=self._async_client)
233
255
 
234
256
  def submit_estimate(
235
- self, hamiltonian: Hamiltonian, parameters: Optional[ExecutionParams] = None
257
+ self,
258
+ hamiltonian: Hamiltonian,
259
+ parameters: Optional[ExecutionParams] = None,
260
+ *,
261
+ _check_deprecation: bool = True,
236
262
  ) -> ExecutionJob:
237
263
  """
238
264
  Initiates an execution job with the `estimate` primitive.
@@ -247,6 +273,8 @@ class ExecutionSession:
247
273
  Returns:
248
274
  The execution job.
249
275
  """
276
+ if _check_deprecation:
277
+ _hamiltonian_deprecation_warning(hamiltonian)
250
278
  execution_primitives_input = PrimitivesInput(
251
279
  estimate=EstimateInput(
252
280
  hamiltonian=self._hamiltonian_to_pauli_operator(hamiltonian),
@@ -270,11 +298,18 @@ class ExecutionSession:
270
298
  Returns:
271
299
  List[EstimationResult]: The results of all the estimation iterations.
272
300
  """
273
- job = self.submit_batch_estimate(hamiltonian=hamiltonian, parameters=parameters)
301
+ _hamiltonian_deprecation_warning(hamiltonian)
302
+ job = self.submit_batch_estimate(
303
+ hamiltonian=hamiltonian, parameters=parameters, _check_deprecation=False
304
+ )
274
305
  return job.get_batch_estimate_result(_http_client=self._async_client)
275
306
 
276
307
  def submit_batch_estimate(
277
- self, hamiltonian: Hamiltonian, parameters: list[ExecutionParams]
308
+ self,
309
+ hamiltonian: Hamiltonian,
310
+ parameters: list[ExecutionParams],
311
+ *,
312
+ _check_deprecation: bool = True,
278
313
  ) -> ExecutionJob:
279
314
  """
280
315
  Initiates an execution job with the `batch_estimate` primitive.
@@ -289,6 +324,8 @@ class ExecutionSession:
289
324
  Returns:
290
325
  The execution job.
291
326
  """
327
+ if _check_deprecation:
328
+ _hamiltonian_deprecation_warning(hamiltonian)
292
329
  execution_primitives_input = PrimitivesInput(
293
330
  estimate=EstimateInput(
294
331
  hamiltonian=self._hamiltonian_to_pauli_operator(hamiltonian),
@@ -322,11 +359,13 @@ class ExecutionSession:
322
359
  A list of tuples, each containing the estimated cost and the corresponding parameters for that iteration.
323
360
  `cost` is a float, and `parameters` is a dictionary matching the execution parameter format.
324
361
  """
362
+ _hamiltonian_deprecation_warning(cost_function)
325
363
  job = self.submit_minimize(
326
364
  cost_function=cost_function,
327
365
  initial_params=initial_params,
328
366
  max_iteration=max_iteration,
329
367
  quantile=quantile,
368
+ _check_deprecation=False,
330
369
  )
331
370
  result = job.get_minimization_result(_http_client=self._async_client)
332
371
 
@@ -340,6 +379,8 @@ class ExecutionSession:
340
379
  initial_params: ExecutionParams,
341
380
  max_iteration: int,
342
381
  quantile: float = 1.0,
382
+ *,
383
+ _check_deprecation: bool = True,
343
384
  ) -> ExecutionJob:
344
385
  """
345
386
  Initiates an execution job with the `minimize` primitive.
@@ -363,6 +404,8 @@ class ExecutionSession:
363
404
  Returns:
364
405
  The execution job.
365
406
  """
407
+ if _check_deprecation:
408
+ _hamiltonian_deprecation_warning(cost_function)
366
409
  if len(initial_params) != 1:
367
410
  raise ClassiqValueError(
368
411
  "The initial parameters must be a dictionary with a single key-value pair."
@@ -3,5 +3,5 @@ from packaging.version import Version
3
3
  # This file was generated automatically
4
4
  # Please don't track in version control (DONTTRACK)
5
5
 
6
- SEMVER_VERSION = '0.84.0'
6
+ SEMVER_VERSION = '0.85.0'
7
7
  VERSION = str(Version(SEMVER_VERSION))
@@ -9,7 +9,6 @@ from classiq.interface.generator.generated_circuit_data import (
9
9
  FunctionDebugInfoInterface,
10
10
  StatementType,
11
11
  )
12
- from classiq.interface.model.block import Block
13
12
  from classiq.interface.model.handle_binding import ConcreteHandleBinding
14
13
  from classiq.interface.model.port_declaration import PortDeclaration
15
14
  from classiq.interface.model.quantum_function_call import ArgValue
@@ -82,9 +81,6 @@ def get_back_refs(
82
81
  ) -> list[ConcreteQuantumStatement]:
83
82
  back_refs: list[ConcreteQuantumStatement] = []
84
83
  while (node := debug_info.node) is not None:
85
- # For backwards compatibility, we make sure that the back_ref is not a block
86
- # Remove this check when we start saving blocks in the debug info collection.
87
- assert not isinstance(node, Block)
88
84
  if len(back_refs) > 0 and node.back_ref == back_refs[0].back_ref:
89
85
  break
90
86
  back_refs.insert(0, node)
@@ -20,9 +20,14 @@ CLASSIQ_EXPR_FUNCTIONS = {
20
20
  "get_field",
21
21
  }
22
22
 
23
+ MEASUREMENT_FUNCTIONS = {
24
+ "measure",
25
+ }
26
+
23
27
  SUPPORTED_CLASSIQ_BUILTIN_FUNCTIONS = {
24
28
  *CLASSIQ_BUILTIN_CLASSICAL_FUNCTIONS,
25
29
  *CLASSIQ_EXPR_FUNCTIONS,
30
+ *MEASUREMENT_FUNCTIONS,
26
31
  }
27
32
 
28
33
  SUPPORTED_CLASSIQ_SYMPY_WRAPPERS = {
@@ -6,6 +6,7 @@ CLASSICAL_IF_OPERATOR_NAME = "classical_if"
6
6
  POWER_OPERATOR_NAME = "power"
7
7
  UNCOMPUTE_OPERATOR_NAME = "uncompute"
8
8
  WITHIN_APPLY_NAME = "within_apply"
9
+ BLOCK_OPERATOR_NAME = "block"
9
10
 
10
11
  All_BUILTINS_OPERATORS = {
11
12
  CONTROL_OPERATOR_NAME,
@@ -14,4 +15,5 @@ All_BUILTINS_OPERATORS = {
14
15
  POWER_OPERATOR_NAME,
15
16
  UNCOMPUTE_OPERATOR_NAME,
16
17
  WITHIN_APPLY_NAME,
18
+ BLOCK_OPERATOR_NAME,
17
19
  }
@@ -53,3 +53,23 @@ QuantumBitvector.model_rebuild()
53
53
  TypeName.model_rebuild()
54
54
  QStructDeclaration.model_rebuild()
55
55
  RegisterQuantumType.model_rebuild()
56
+
57
+ ConcreteType = Annotated[
58
+ Union[
59
+ Integer,
60
+ Real,
61
+ Bool,
62
+ StructMetaType,
63
+ TypeName,
64
+ ClassicalArray,
65
+ ClassicalTuple,
66
+ VQEResult,
67
+ Histogram,
68
+ Estimation,
69
+ IQAERes,
70
+ QuantumBit,
71
+ QuantumBitvector,
72
+ QuantumNumeric,
73
+ ],
74
+ Field(discriminator="kind"),
75
+ ]
@@ -52,8 +52,6 @@ VISUALIZATION_HIDE_LIST = [
52
52
  "stmt_block",
53
53
  ]
54
54
 
55
- CONTROLLED_PREFIX = "c_"
56
-
57
55
 
58
56
  def last_name_in_call_hierarchy(name: str) -> str:
59
57
  return name.split(CLASSIQ_HIERARCHY_SEPARATOR)[-1]
@@ -147,6 +145,7 @@ class StatementType(StrEnum):
147
145
  INPLACE_XOR = "inplace xor"
148
146
  INPLACE_ADD = "inplace add"
149
147
  REPEAT = "repeat"
148
+ BLOCK = "block"
150
149
 
151
150
 
152
151
  # Mapping between statement kind (or sub-kind) and statement type (visualization name)
@@ -167,6 +166,7 @@ STATEMENTS_NAME: dict[str, StatementType] = {
167
166
  ArithmeticOperationKind.InplaceXor.value: StatementType.INPLACE_XOR,
168
167
  ArithmeticOperationKind.InplaceAdd.value: StatementType.INPLACE_ADD,
169
168
  "Repeat": StatementType.REPEAT,
169
+ "Block": StatementType.BLOCK,
170
170
  }
171
171
 
172
172
 
@@ -207,8 +207,7 @@ class FunctionDebugInfoInterface(pydantic.BaseModel):
207
207
  ARITH_ENGINE_PREFIX
208
208
  )
209
209
  name_with_suffix = self.add_suffix_from_generated_name(generated_name, name)
210
- modified_name = self.modify_name_for_controlled_qfunc(name_with_suffix)
211
- return modified_name
210
+ return name_with_suffix
212
211
 
213
212
  statement_kind: str = back_ref.kind
214
213
  if isinstance(back_ref, ArithmeticOperation):
@@ -217,11 +216,6 @@ class FunctionDebugInfoInterface(pydantic.BaseModel):
217
216
  generated_name, STATEMENTS_NAME[statement_kind]
218
217
  )
219
218
 
220
- def modify_name_for_controlled_qfunc(self, generated_name: str) -> str:
221
- if self.control_variable is None:
222
- return generated_name
223
- return f"{CONTROLLED_PREFIX}{generated_name}"
224
-
225
219
  def add_suffix_from_generated_name(self, generated_name: str, name: str) -> str:
226
220
  if part_match := PART_SUFFIX_REGEX.match(generated_name):
227
221
  suffix = f" [{part_match.group(1)}]"
@@ -272,7 +266,8 @@ class FunctionDebugInfoInterface(pydantic.BaseModel):
272
266
  for register in self.registers
273
267
  for qubit in register.qubit_indexes_absolute
274
268
  if register.role is RegisterRole.INPUT
275
- and register.name == self.control_variable
269
+ and self.port_to_passed_variable_map.get(register.name, register.name)
270
+ == self.control_variable
276
271
  )
277
272
 
278
273
  def propagate_absolute_qubits(self) -> "FunctionDebugInfoInterface":
@@ -0,0 +1,45 @@
1
+ from typing import Any
2
+
3
+ from classiq.interface.ide.visual_model import Operation
4
+
5
+
6
+ class OperationRegistry:
7
+ def __init__(self) -> None:
8
+ self._operation_hash_to_op_id: dict[int, int] = {}
9
+ self._id_to_operations: dict[int, Operation] = {}
10
+ self._unique_op_counter = 0
11
+ self._deduped_op_counter = 0
12
+
13
+ def build_operation(self, **kwargs: Any) -> Operation:
14
+ operation = Operation(**kwargs)
15
+ return self.add_operation(operation)
16
+
17
+ def add_operation(self, op: Operation) -> Operation:
18
+ """
19
+ Adds an operation to the global dictionaries for operations.
20
+ if operation already exist in the registry, it returns the existing operation.
21
+ """
22
+ op_hash = hash(op)
23
+ if op_hash not in self._operation_hash_to_op_id:
24
+ self._operation_hash_to_op_id[op_hash] = op.id
25
+ self._id_to_operations[op.id] = op
26
+ self._unique_op_counter += 1
27
+ else:
28
+ self._deduped_op_counter += 1
29
+ op = self._id_to_operations[self._operation_hash_to_op_id[op_hash]]
30
+ return op
31
+
32
+ def get_operation_mapping(self) -> dict[int, Operation]:
33
+ return self._id_to_operations
34
+
35
+ def get_operations(self, op_ids: list[int]) -> list[Operation]:
36
+ """
37
+ Returns a list of operations based on their IDs.
38
+ """
39
+ return [self._id_to_operations[op_id] for op_id in op_ids]
40
+
41
+ def get_unique_op_number(self) -> int:
42
+ return self._unique_op_counter
43
+
44
+ def get_deduped_op_number(self) -> int:
45
+ return self._deduped_op_counter
@@ -1,5 +1,8 @@
1
+ import json
1
2
  from collections import Counter
3
+ from collections.abc import Iterator
2
4
  from functools import cached_property
5
+ from itertools import count
3
6
  from typing import Any, Optional
4
7
 
5
8
  import pydantic
@@ -13,6 +16,26 @@ from classiq.interface.generator.hardware.hardware_data import SynthesisHardware
13
16
  from classiq.interface.helpers.versioned_model import VersionedModel
14
17
 
15
18
 
19
+ class OperationIdCounter:
20
+ _op_id_counter: Iterator[int] = count()
21
+
22
+ def next_id(self) -> int:
23
+ return next(self._op_id_counter)
24
+
25
+ def reset_operation_counter(self) -> None:
26
+ self._op_id_counter = count()
27
+
28
+
29
+ _operation_id_counter = OperationIdCounter()
30
+
31
+
32
+ def reset_operation_counter() -> None:
33
+ """
34
+ Call this at the start of every new task to restart ids at 0.
35
+ """
36
+ _operation_id_counter.reset_operation_counter()
37
+
38
+
16
39
  class OperationType(StrEnum):
17
40
  REGULAR = "REGULAR"
18
41
  INVISIBLE = "INVISIBLE"
@@ -108,6 +131,7 @@ class AtomicGate(StrEnum):
108
131
 
109
132
  class Operation(pydantic.BaseModel):
110
133
  name: str
134
+ _id: int = pydantic.PrivateAttr(default_factory=_operation_id_counter.next_id)
111
135
  qasm_name: str = pydantic.Field(default="")
112
136
  details: str = pydantic.Field(default="")
113
137
  children: list["Operation"] = pydantic.Field(default_factory=list)
@@ -120,7 +144,7 @@ class Operation(pydantic.BaseModel):
120
144
  target_qubits: tuple[int, ...]
121
145
  operation_level: OperationLevel
122
146
  operation_type: OperationType = pydantic.Field(
123
- description="Identifies unique operations that are visualized differently"
147
+ description="Identifies unique operations that are visualized differently",
124
148
  )
125
149
  gate: AtomicGate = pydantic.Field(
126
150
  default=AtomicGate.UNKNOWN, description="Gate type"
@@ -129,9 +153,46 @@ class Operation(pydantic.BaseModel):
129
153
  expanded: bool = pydantic.Field(default=False)
130
154
  show_expanded_label: bool = pydantic.Field(default=False)
131
155
 
156
+ @property
157
+ def id(self) -> int:
158
+ return self._id
159
+
160
+ def __hash__(self) -> int:
161
+ """
162
+ using a custom hashable_dict in order to compare the operation
163
+ with the qubits in order
164
+ """
165
+ js = json.dumps(
166
+ self._hashable_dict(),
167
+ sort_keys=True,
168
+ default=lambda o: o.value if hasattr(o, "value") else str(o),
169
+ )
170
+ return hash(js)
171
+
172
+ def __eq__(self, other: object) -> bool:
173
+ return (
174
+ isinstance(other, Operation)
175
+ and self._hashable_dict() == other._hashable_dict()
176
+ )
177
+
178
+ def _hashable_dict(self) -> dict:
179
+ data = self.model_dump(
180
+ exclude_none=True,
181
+ )
182
+ # force qubit order for equality
183
+ for key in ("target_qubits", "auxiliary_qubits", "control_qubits"):
184
+ data[key] = sorted(data[key])
185
+ return data
186
+
132
187
 
133
188
  class ProgramVisualModel(VersionedModel):
134
189
  main_operation: Operation = pydantic.Field(default=None)
135
190
  id_to_operations: dict[int, Operation] = pydantic.Field(default_factory=dict)
136
191
  main_operation_id: int = pydantic.Field(default=None)
137
192
  program_data: ProgramData
193
+
194
+ @property
195
+ def main_op_from_mapping(self) -> Operation:
196
+ if self.main_operation_id is None:
197
+ raise ValueError("Main operation ID is not set.")
198
+ return self.id_to_operations[self.main_operation_id]
@@ -1,6 +1,6 @@
1
1
  from typing import Literal, Optional
2
2
 
3
- from classiq.interface.helpers.custom_pydantic_types import PydanticFloatTuple
3
+ from classiq.interface.generator.expressions.expression import Expression
4
4
  from classiq.interface.model.handle_binding import ConcreteHandleBinding
5
5
  from classiq.interface.model.quantum_statement import QuantumOperation
6
6
 
@@ -9,4 +9,14 @@ class SetBoundsStatement(QuantumOperation):
9
9
  kind: Literal["SetBoundsStatement"]
10
10
 
11
11
  target: ConcreteHandleBinding
12
- bounds: Optional[PydanticFloatTuple]
12
+ lower_bound: Optional[Expression]
13
+ upper_bound: Optional[Expression]
14
+
15
+ @property
16
+ def expressions(self) -> list[Expression]:
17
+ exprs = []
18
+ if self.lower_bound is not None:
19
+ exprs.append(self.lower_bound)
20
+ if self.upper_bound is not None:
21
+ exprs.append(self.upper_bound)
22
+ return exprs
@@ -1,6 +1,8 @@
1
1
  from collections.abc import Mapping, Sequence
2
2
  from typing import Literal
3
3
 
4
+ from pydantic import PrivateAttr
5
+
4
6
  from classiq.interface.enum_utils import StrEnum
5
7
  from classiq.interface.generator.arith.arithmetic import (
6
8
  ARITHMETIC_EXPRESSION_RESULT_NAME,
@@ -27,6 +29,7 @@ class ArithmeticOperation(QuantumAssignmentOperation):
27
29
  kind: Literal["ArithmeticOperation"]
28
30
 
29
31
  operation_kind: ArithmeticOperationKind
32
+ _classical_assignment: bool = PrivateAttr(default=False)
30
33
 
31
34
  @property
32
35
  def is_inplace(self) -> bool:
@@ -50,7 +53,7 @@ class ArithmeticOperation(QuantumAssignmentOperation):
50
53
  self,
51
54
  ) -> Mapping[str, ConcreteHandleBinding]:
52
55
  inouts = dict(super().wiring_inouts)
53
- if self.is_inplace:
56
+ if self.is_inplace and not self._classical_assignment:
54
57
  inouts[self.result_name()] = self.result_var
55
58
  return inouts
56
59
 
@@ -60,7 +63,7 @@ class ArithmeticOperation(QuantumAssignmentOperation):
60
63
  HandleMetadata(handle=handle, readable_location="in an expression")
61
64
  for handle in self.var_handles
62
65
  ]
63
- if self.is_inplace:
66
+ if self.is_inplace and not self._classical_assignment:
64
67
  inouts.append(
65
68
  HandleMetadata(
66
69
  handle=self.result_var,
@@ -71,13 +74,13 @@ class ArithmeticOperation(QuantumAssignmentOperation):
71
74
 
72
75
  @property
73
76
  def wiring_outputs(self) -> Mapping[str, HandleBinding]:
74
- if self.is_inplace:
77
+ if self.is_inplace or self._classical_assignment:
75
78
  return {}
76
79
  return super().wiring_outputs
77
80
 
78
81
  @property
79
82
  def readable_outputs(self) -> Sequence[HandleMetadata]:
80
- if self.is_inplace:
83
+ if self.is_inplace or self._classical_assignment:
81
84
  return []
82
85
  return [
83
86
  HandleMetadata(