classiq 0.85.0__py3-none-any.whl → 0.86.1__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 (59) hide show
  1. classiq/applications/combinatorial_optimization/combinatorial_problem.py +4 -3
  2. classiq/evaluators/qmod_annotated_expression.py +207 -0
  3. classiq/evaluators/qmod_expression_visitors/__init__.py +0 -0
  4. classiq/evaluators/qmod_expression_visitors/qmod_expression_bwc.py +134 -0
  5. classiq/evaluators/qmod_expression_visitors/qmod_expression_evaluator.py +232 -0
  6. classiq/evaluators/qmod_expression_visitors/qmod_expression_renamer.py +44 -0
  7. classiq/evaluators/qmod_expression_visitors/qmod_expression_simplifier.py +308 -0
  8. classiq/evaluators/qmod_node_evaluators/__init__.py +0 -0
  9. classiq/evaluators/qmod_node_evaluators/attribute_evaluation.py +112 -0
  10. classiq/evaluators/qmod_node_evaluators/binary_op_evaluation.py +132 -0
  11. classiq/evaluators/qmod_node_evaluators/bool_op_evaluation.py +70 -0
  12. classiq/evaluators/qmod_node_evaluators/classical_function_evaluation.py +311 -0
  13. classiq/evaluators/qmod_node_evaluators/compare_evaluation.py +107 -0
  14. classiq/evaluators/qmod_node_evaluators/constant_evaluation.py +67 -0
  15. classiq/evaluators/qmod_node_evaluators/list_evaluation.py +107 -0
  16. classiq/evaluators/qmod_node_evaluators/measurement_evaluation.py +25 -0
  17. classiq/evaluators/qmod_node_evaluators/name_evaluation.py +50 -0
  18. classiq/evaluators/qmod_node_evaluators/struct_instantiation_evaluation.py +66 -0
  19. classiq/evaluators/qmod_node_evaluators/subscript_evaluation.py +225 -0
  20. classiq/evaluators/qmod_node_evaluators/unary_op_evaluation.py +58 -0
  21. classiq/evaluators/qmod_node_evaluators/utils.py +80 -0
  22. classiq/execution/execution_session.py +4 -0
  23. classiq/interface/_version.py +1 -1
  24. classiq/interface/analyzer/analysis_params.py +1 -1
  25. classiq/interface/analyzer/result.py +1 -1
  26. classiq/interface/executor/quantum_code.py +2 -2
  27. classiq/interface/generator/arith/arithmetic_expression_validator.py +5 -1
  28. classiq/interface/generator/arith/binary_ops.py +43 -51
  29. classiq/interface/generator/arith/number_utils.py +3 -2
  30. classiq/interface/generator/arith/register_user_input.py +15 -0
  31. classiq/interface/generator/arith/unary_ops.py +32 -28
  32. classiq/interface/generator/expressions/expression_types.py +2 -2
  33. classiq/interface/generator/expressions/proxies/classical/qmod_struct_instance.py +7 -0
  34. classiq/interface/generator/functions/classical_function_declaration.py +0 -4
  35. classiq/interface/generator/functions/classical_type.py +0 -32
  36. classiq/interface/generator/generated_circuit_data.py +2 -0
  37. classiq/interface/generator/quantum_program.py +6 -1
  38. classiq/interface/generator/synthesis_metadata/synthesis_execution_data.py +29 -0
  39. classiq/interface/ide/operation_registry.py +2 -2
  40. classiq/interface/ide/visual_model.py +22 -1
  41. classiq/interface/model/quantum_type.py +67 -33
  42. classiq/model_expansions/arithmetic.py +115 -0
  43. classiq/model_expansions/arithmetic_compute_result_attrs.py +71 -0
  44. classiq/model_expansions/atomic_expression_functions_defs.py +5 -5
  45. classiq/model_expansions/generative_functions.py +15 -2
  46. classiq/model_expansions/interpreters/base_interpreter.py +7 -0
  47. classiq/model_expansions/interpreters/generative_interpreter.py +2 -0
  48. classiq/model_expansions/quantum_operations/call_emitter.py +5 -2
  49. classiq/model_expansions/scope_initialization.py +2 -0
  50. classiq/model_expansions/sympy_conversion/sympy_to_python.py +1 -1
  51. classiq/model_expansions/transformers/type_modifier_inference.py +5 -0
  52. classiq/model_expansions/visitors/boolean_expression_transformers.py +1 -1
  53. classiq/qmod/builtins/operations.py +1 -1
  54. classiq/qmod/declaration_inferrer.py +5 -3
  55. classiq/qmod/write_qmod.py +3 -1
  56. {classiq-0.85.0.dist-info → classiq-0.86.1.dist-info}/METADATA +1 -1
  57. {classiq-0.85.0.dist-info → classiq-0.86.1.dist-info}/RECORD +59 -37
  58. /classiq/{model_expansions/sympy_conversion/arithmetics.py → evaluators/qmod_expression_visitors/sympy_wrappers.py} +0 -0
  59. {classiq-0.85.0.dist-info → classiq-0.86.1.dist-info}/WHEEL +0 -0
@@ -11,6 +11,8 @@ from classiq.interface.helpers.hashable_pydantic_base_model import (
11
11
  HashablePydanticBaseModel,
12
12
  )
13
13
 
14
+ MAX_REGISTER_SIZE = 10000
15
+
14
16
 
15
17
  class RegisterArithmeticInfo(HashablePydanticBaseModel):
16
18
  size: pydantic.PositiveInt = pydantic.Field(default=1)
@@ -56,6 +58,19 @@ class RegisterArithmeticInfo(HashablePydanticBaseModel):
56
58
  fraction_places = info.data.get("fraction_places", 0)
57
59
  if not isinstance(size, int):
58
60
  raise ClassiqValueError("RegisterArithmeticInfo must have an integer size")
61
+ if not isinstance(fraction_places, int):
62
+ raise ClassiqValueError(
63
+ "RegisterArithmeticInfo must have an integer fraction_places"
64
+ )
65
+
66
+ if size > MAX_REGISTER_SIZE:
67
+ raise ValueError(
68
+ f"Register size {size} exceeds maximum size {MAX_REGISTER_SIZE}."
69
+ )
70
+ if fraction_places > size:
71
+ raise ValueError(
72
+ f"Number of fraction places {fraction_places} exceeds register size {size}."
73
+ )
59
74
 
60
75
  maximal_bounds = cls.get_maximal_bounds(
61
76
  size=size, is_signed=is_signed, fraction_places=fraction_places
@@ -1,11 +1,11 @@
1
1
  from collections.abc import Iterable
2
- from typing import TYPE_CHECKING, Final, Optional
2
+ from typing import Final, Optional
3
3
 
4
4
  import pydantic
5
5
  from pydantic import ConfigDict
6
6
 
7
7
  from classiq.interface.exceptions import ClassiqValueError
8
- from classiq.interface.generator.arith import argument_utils, number_utils
8
+ from classiq.interface.generator.arith import number_utils
9
9
  from classiq.interface.generator.arith.arithmetic_operations import (
10
10
  MODULO_WITH_FRACTION_PLACES_ERROR_MSG,
11
11
  ArithmeticOperationParams,
@@ -13,6 +13,12 @@ from classiq.interface.generator.arith.arithmetic_operations import (
13
13
  from classiq.interface.generator.arith.register_user_input import RegisterArithmeticInfo
14
14
  from classiq.interface.generator.function_params import get_zero_input_name
15
15
 
16
+ from classiq.model_expansions.arithmetic import NumericAttributes
17
+ from classiq.model_expansions.arithmetic_compute_result_attrs import (
18
+ compute_result_attrs_bitwise_invert,
19
+ compute_result_attrs_negate,
20
+ )
21
+
16
22
  UNARY_ARG_NAME: Final[str] = "arg"
17
23
 
18
24
 
@@ -65,13 +71,17 @@ class BitwiseInvert(UnaryOpParams):
65
71
  output_name = "inverted"
66
72
 
67
73
  def _get_result_register(self) -> RegisterArithmeticInfo:
68
- eff_arg = argument_utils.limit_fraction_places(self.arg, self.machine_precision)
69
- if TYPE_CHECKING:
70
- assert isinstance(eff_arg, RegisterArithmeticInfo)
74
+ arg_attrs = NumericAttributes.from_register_arithmetic_info(
75
+ self.arg, self.machine_precision
76
+ )
77
+ result_attrs = compute_result_attrs_bitwise_invert(
78
+ arg_attrs, self.machine_precision
79
+ )
80
+
71
81
  return RegisterArithmeticInfo(
72
- size=self.output_size or eff_arg.size,
73
- fraction_places=eff_arg.fraction_places,
74
- is_signed=eff_arg.is_signed and self._include_sign,
82
+ size=self.output_size or result_attrs.size,
83
+ fraction_places=result_attrs.fraction_digits,
84
+ is_signed=result_attrs.is_signed and self._include_sign,
75
85
  )
76
86
 
77
87
 
@@ -81,40 +91,34 @@ class Negation(UnaryOpParams):
81
91
  ) # True for efficient subtraction
82
92
  output_name = "negated"
83
93
 
84
- @staticmethod
85
- def _expected_result_size(arg: RegisterArithmeticInfo) -> pydantic.PositiveInt:
86
- if arg.size == 1:
87
- return 1
88
- return arg.fraction_places + number_utils.bounds_to_integer_part_size(
89
- *(-bound for bound in arg.bounds)
90
- )
91
-
92
94
  def _get_result_register(self) -> RegisterArithmeticInfo:
93
- eff_arg = self.arg.limit_fraction_places(self.machine_precision)
94
- is_signed = max(eff_arg.bounds) > 0 and self._include_sign
95
- bounds = (-max(eff_arg.bounds), -min(eff_arg.bounds))
95
+ arg_attrs = NumericAttributes.from_register_arithmetic_info(
96
+ self.arg, self.machine_precision
97
+ )
98
+ result_attrs = compute_result_attrs_negate(arg_attrs, self.machine_precision)
99
+ is_signed = result_attrs.is_signed and self._include_sign
100
+ bounds = result_attrs.bounds
96
101
  if self.output_size and not self.bypass_bounds_validation:
97
- if eff_arg.fraction_places:
102
+ if result_attrs.fraction_digits:
98
103
  raise ValueError(MODULO_WITH_FRACTION_PLACES_ERROR_MSG)
99
104
  max_bounds = RegisterArithmeticInfo.get_maximal_bounds(
100
105
  size=self.output_size, is_signed=False, fraction_places=0
101
106
  )
102
107
  bounds = number_utils.bounds_cut(bounds, max_bounds)
103
108
  return RegisterArithmeticInfo(
104
- size=self.output_size or self._expected_result_size(eff_arg),
105
- fraction_places=eff_arg.fraction_places,
109
+ size=self.output_size or result_attrs.size,
110
+ fraction_places=result_attrs.fraction_digits,
106
111
  is_signed=is_signed,
107
112
  bypass_bounds_validation=self.bypass_bounds_validation,
108
113
  bounds=bounds,
109
114
  )
110
115
 
111
116
  def zero_input_for_extension(self) -> pydantic.NonNegativeInt:
112
- eff_arg = self.arg.limit_fraction_places(self.machine_precision)
113
- if (self.output_size or eff_arg.size) == 1:
114
- return 0
115
- return (
116
- self.output_size or self._expected_result_size(self.arg)
117
- ) - self.arg.size
117
+ arg_integers = self.arg.size - self.arg.fraction_places
118
+ result_integers = (
119
+ self.result_register.size - self.result_register.fraction_places
120
+ )
121
+ return result_integers - arg_integers
118
122
 
119
123
 
120
124
  class Sign(UnaryOpParams):
@@ -1,6 +1,6 @@
1
1
  from typing import Union
2
2
 
3
- from sympy import Expr
3
+ from sympy import Basic
4
4
  from sympy.logic.boolalg import Boolean
5
5
 
6
6
  from classiq.interface.generator.expressions.handle_identifier import HandleIdentifier
@@ -31,5 +31,5 @@ Proxies = Union[
31
31
  QmodSizedProxy,
32
32
  ClassicalProxy,
33
33
  ]
34
- RuntimeExpression = Union[AnyClassicalValue, Expr, Boolean]
34
+ RuntimeExpression = Union[AnyClassicalValue, Basic, Boolean]
35
35
  ExpressionValue = Union[RuntimeConstant, Proxies, RuntimeExpression]
@@ -41,3 +41,10 @@ class QmodStructInstance:
41
41
  for field_name, field_value in self._fields.items()
42
42
  )
43
43
  return f"{self.struct_declaration.name}({fields})"
44
+
45
+ def __eq__(self, other: Any) -> bool:
46
+ return (
47
+ isinstance(other, QmodStructInstance)
48
+ and self.struct_declaration.name == other.struct_declaration.name
49
+ and self._fields == other._fields
50
+ )
@@ -28,10 +28,6 @@ class ClassicalFunctionDeclaration(FunctionDeclaration):
28
28
  default=None,
29
29
  )
30
30
 
31
- BUILTIN_FUNCTION_DECLARATIONS: ClassVar[
32
- dict[str, "ClassicalFunctionDeclaration"]
33
- ] = {}
34
-
35
31
  FOREIGN_FUNCTION_DECLARATIONS: ClassVar[
36
32
  dict[str, "ClassicalFunctionDeclaration"]
37
33
  ] = {}
@@ -113,7 +113,6 @@ class StructMetaType(ClassicalType):
113
113
  class ClassicalArray(ClassicalType):
114
114
  kind: Literal["array"]
115
115
  element_type: "ConcreteClassicalType"
116
- size: Optional[int] = pydantic.Field(exclude=True, default=None)
117
116
  length: Optional[Expression] = None
118
117
 
119
118
  @pydantic.model_validator(mode="before")
@@ -121,37 +120,6 @@ class ClassicalArray(ClassicalType):
121
120
  def _set_kind(cls, values: Any) -> dict[str, Any]:
122
121
  return values_with_discriminator(values, "kind", "array")
123
122
 
124
- @pydantic.model_validator(mode="before")
125
- @classmethod
126
- def _set_length(cls, values: Any) -> Any:
127
- if isinstance(values, dict):
128
- size = values.get("size")
129
- length = values.get("length")
130
- else:
131
- size = values.size
132
- length = values.length
133
- if size is not None:
134
- if isinstance(values, dict):
135
- values["length"] = Expression(expr=str(size))
136
- else:
137
- values.length = Expression(expr=str(size))
138
- elif length is not None:
139
- if isinstance(length, dict):
140
- expr = length["expr"]
141
- else:
142
- expr = length.expr
143
- expr_size: Optional[int] = None
144
- try: # noqa: SIM105
145
- expr_size = int(expr)
146
- except ValueError:
147
- pass
148
- if expr_size is not None:
149
- if isinstance(values, dict):
150
- values["size"] = expr_size
151
- else:
152
- values.size = expr_size
153
- return values
154
-
155
123
  @property
156
124
  def has_length(self) -> bool:
157
125
  return self.length is not None and self.length.is_evaluated()
@@ -146,6 +146,7 @@ class StatementType(StrEnum):
146
146
  INPLACE_ADD = "inplace add"
147
147
  REPEAT = "repeat"
148
148
  BLOCK = "block"
149
+ IF = "if"
149
150
 
150
151
 
151
152
  # Mapping between statement kind (or sub-kind) and statement type (visualization name)
@@ -167,6 +168,7 @@ STATEMENTS_NAME: dict[str, StatementType] = {
167
168
  ArithmeticOperationKind.InplaceAdd.value: StatementType.INPLACE_ADD,
168
169
  "Repeat": StatementType.REPEAT,
169
170
  "Block": StatementType.BLOCK,
171
+ "ClassicalIf": StatementType.IF,
170
172
  }
171
173
 
172
174
 
@@ -76,6 +76,8 @@ class QuantumProgram(VersionedModel, CircuitCodeInterface):
76
76
  execution_primitives_input: Optional[PrimitivesInput] = pydantic.Field(default=None)
77
77
  synthesis_warnings: Optional[list[str]] = pydantic.Field(default=None)
78
78
  should_warn: bool = pydantic.Field(default=False)
79
+ # Unique identifier for the circuit (since the program_id might change when running show). Used for the circuit store.
80
+ circuit_id: str = pydantic.Field(default_factory=get_uuid_as_str)
79
81
 
80
82
  def __str__(self) -> str:
81
83
  return self.model_dump_json(indent=2)
@@ -168,7 +170,10 @@ class QuantumProgram(VersionedModel, CircuitCodeInterface):
168
170
 
169
171
  @property
170
172
  def _can_use_transpiled_code(self) -> bool:
171
- return self.data.execution_data is None
173
+ return (
174
+ self.data.execution_data is None
175
+ or not self.data.execution_data.function_execution
176
+ )
172
177
 
173
178
  @property
174
179
  def program_circuit(self) -> CircuitCodeInterface:
@@ -3,6 +3,7 @@ from typing import Optional
3
3
 
4
4
  import pydantic
5
5
  import sympy
6
+ from typing_extensions import Self
6
7
 
7
8
  from classiq.interface.backend.pydantic_backend import PydanticExecutionParameter
8
9
  from classiq.interface.generator.parameters import ParameterType
@@ -34,3 +35,31 @@ class ExecutionData(pydantic.BaseModel):
34
35
  if function_execution_data.power_vars is not None
35
36
  )
36
37
  )
38
+
39
+ def to_inverse(self) -> Self:
40
+ return type(self)(
41
+ function_execution={
42
+ self._inverse_name(name): value
43
+ for name, value in self.function_execution.items()
44
+ }
45
+ )
46
+
47
+ def to_control(self) -> Self:
48
+ return type(self)(
49
+ function_execution={
50
+ self._control_name(name): value
51
+ for name, value in self.function_execution.items()
52
+ }
53
+ )
54
+
55
+ @staticmethod
56
+ def _inverse_name(name: str) -> str:
57
+ # see inverse of qiskit.circuit.Instruction
58
+ if name.endswith("_dg"):
59
+ return name[:-3]
60
+ return f"{name}_dg"
61
+
62
+ @staticmethod
63
+ def _control_name(name: str) -> str:
64
+ # see inverse of qiskit.circuit.QuantumCircuit
65
+ return f"c_{name}"
@@ -12,9 +12,9 @@ class OperationRegistry:
12
12
 
13
13
  def build_operation(self, **kwargs: Any) -> Operation:
14
14
  operation = Operation(**kwargs)
15
- return self.add_operation(operation)
15
+ return self._add_operation(operation)
16
16
 
17
- def add_operation(self, op: Operation) -> Operation:
17
+ def _add_operation(self, op: Operation) -> Operation:
18
18
  """
19
19
  Adds an operation to the global dictionaries for operations.
20
20
  if operation already exist in the registry, it returns the existing operation.
@@ -6,7 +6,7 @@ from itertools import count
6
6
  from typing import Any, Optional
7
7
 
8
8
  import pydantic
9
- from pydantic import ConfigDict
9
+ from pydantic import ConfigDict, field_validator
10
10
 
11
11
  from classiq.interface.enum_utils import StrEnum
12
12
  from classiq.interface.generator.generated_circuit_data import (
@@ -50,6 +50,8 @@ class OperationData(pydantic.BaseModel):
50
50
  width: int
51
51
  gate_count: Counter[str] = pydantic.Field(default_factory=dict)
52
52
 
53
+ model_config = ConfigDict(frozen=True)
54
+
53
55
 
54
56
  class CircuitMetrics(pydantic.BaseModel):
55
57
  depth: int
@@ -83,6 +85,21 @@ class OperationLinks(pydantic.BaseModel):
83
85
  inputs: list[OperationLink]
84
86
  outputs: list[OperationLink]
85
87
 
88
+ model_config = ConfigDict(frozen=True)
89
+
90
+ @field_validator("inputs", "outputs", mode="after")
91
+ @classmethod
92
+ def sort_links(cls, v: list[OperationLink]) -> Any:
93
+ """
94
+ sorting the input/output links on creation
95
+ the sort is done by 'label-qubits-type'
96
+ since hash is non-deterministic between runs
97
+ """
98
+ return sorted(v, key=hash)
99
+
100
+ def __hash__(self) -> int:
101
+ return hash(json.dumps(self.model_dump(exclude_none=True), sort_keys=True))
102
+
86
103
  @cached_property
87
104
  def input_width(self) -> int:
88
105
  return sum(len(link.qubits) for link in self.inputs)
@@ -131,6 +148,7 @@ class AtomicGate(StrEnum):
131
148
 
132
149
  class Operation(pydantic.BaseModel):
133
150
  name: str
151
+ inner_label: Optional[str] = None
134
152
  _id: int = pydantic.PrivateAttr(default_factory=_operation_id_counter.next_id)
135
153
  qasm_name: str = pydantic.Field(default="")
136
154
  details: str = pydantic.Field(default="")
@@ -152,6 +170,9 @@ class Operation(pydantic.BaseModel):
152
170
  is_daggered: bool = pydantic.Field(default=False)
153
171
  expanded: bool = pydantic.Field(default=False)
154
172
  show_expanded_label: bool = pydantic.Field(default=False)
173
+ is_low_level_fallback: bool = pydantic.Field(default=False)
174
+
175
+ model_config = ConfigDict(frozen=True)
155
176
 
156
177
  @property
157
178
  def id(self) -> int:
@@ -85,6 +85,35 @@ class QuantumScalar(QuantumType):
85
85
  def get_proxy(self, handle: "HandleBinding") -> QmodQScalarProxy:
86
86
  return QmodQScalarProxy(handle, size=self.size_in_bits)
87
87
 
88
+ @property
89
+ def has_sign(self) -> bool:
90
+ raise NotImplementedError
91
+
92
+ @property
93
+ def sign_value(self) -> bool:
94
+ raise NotImplementedError
95
+
96
+ @property
97
+ def has_fraction_digits(self) -> bool:
98
+ raise NotImplementedError
99
+
100
+ @property
101
+ def fraction_digits_value(self) -> int:
102
+ raise NotImplementedError
103
+
104
+ def get_effective_bounds(
105
+ self, machine_precision: Optional[int] = None
106
+ ) -> tuple[float, float]:
107
+ raise NotImplementedError
108
+
109
+ @property
110
+ def is_qbit(self) -> bool:
111
+ return (
112
+ self.size_in_bits == 1
113
+ and self.fraction_digits_value == 0
114
+ and not self.sign_value
115
+ )
116
+
88
117
 
89
118
  class QuantumBit(QuantumScalar):
90
119
  kind: Literal["qbit"]
@@ -117,6 +146,27 @@ class QuantumBit(QuantumScalar):
117
146
  def is_evaluated(self) -> bool:
118
147
  return True
119
148
 
149
+ @property
150
+ def has_sign(self) -> bool:
151
+ return True
152
+
153
+ @property
154
+ def sign_value(self) -> bool:
155
+ return False
156
+
157
+ @property
158
+ def has_fraction_digits(self) -> bool:
159
+ return True
160
+
161
+ @property
162
+ def fraction_digits_value(self) -> int:
163
+ return 0
164
+
165
+ def get_effective_bounds(
166
+ self, machine_precision: Optional[int] = None
167
+ ) -> tuple[float, float]:
168
+ return (0, 1)
169
+
120
170
 
121
171
  class QuantumBitvector(QuantumType):
122
172
  element_type: "ConcreteQuantumType" = Field(
@@ -243,14 +293,6 @@ class QuantumNumeric(QuantumScalar):
243
293
  0 if self.fraction_digits is None else self.fraction_digits.to_int_value()
244
294
  )
245
295
 
246
- @property
247
- def is_qbit(self) -> bool:
248
- return (
249
- self.size_in_bits == 1
250
- and self.fraction_digits_value == 0
251
- and not self.sign_value
252
- )
253
-
254
296
  def _update_size_in_bits_from_declaration(self) -> None:
255
297
  if self.size is not None and self.size.is_evaluated():
256
298
  self._size_in_bits = self.size.to_int_value()
@@ -302,20 +344,8 @@ class QuantumNumeric(QuantumScalar):
302
344
  exprs.append(self.fraction_digits)
303
345
  return exprs
304
346
 
305
- def get_bounds(
306
- self, machine_precision: Optional[int] = None
307
- ) -> Optional[tuple[float, float]]:
308
- if (
309
- self._bounds is None
310
- or machine_precision is None
311
- or self.fraction_digits_value <= machine_precision
312
- ):
313
- return self._bounds
314
-
315
- return (
316
- number_utils.limit_fraction_places(self._bounds[0], machine_precision),
317
- number_utils.limit_fraction_places(self._bounds[1], machine_precision),
318
- )
347
+ def get_bounds(self) -> Optional[tuple[float, float]]:
348
+ return self._bounds
319
349
 
320
350
  def set_bounds(self, bounds: Optional[tuple[float, float]]) -> None:
321
351
  self._bounds = bounds
@@ -323,19 +353,23 @@ class QuantumNumeric(QuantumScalar):
323
353
  def reset_bounds(self) -> None:
324
354
  self.set_bounds(None)
325
355
 
326
- def get_maximal_bounds(
356
+ def get_maximal_bounds(self) -> tuple[float, float]:
357
+ return RegisterArithmeticInfo.get_maximal_bounds(
358
+ size=self.size_in_bits,
359
+ is_signed=self.sign_value,
360
+ fraction_places=self.fraction_digits_value,
361
+ )
362
+
363
+ def get_effective_bounds(
327
364
  self, machine_precision: Optional[int] = None
328
365
  ) -> tuple[float, float]:
329
- size = self.size_in_bits
330
- fraction_digits = self.fraction_digits_value
331
- if machine_precision is not None and fraction_digits > machine_precision:
332
- size -= fraction_digits - machine_precision
333
- fraction_digits = machine_precision
366
+ bounds = self.get_bounds() or self.get_maximal_bounds()
334
367
 
335
- return RegisterArithmeticInfo.get_maximal_bounds(
336
- size=size,
337
- is_signed=self.sign_value,
338
- fraction_places=fraction_digits,
368
+ if machine_precision is None or machine_precision >= self.fraction_digits_value:
369
+ return bounds
370
+ return (
371
+ number_utils.limit_fraction_places(bounds[0], machine_precision),
372
+ number_utils.limit_fraction_places(bounds[1], machine_precision),
339
373
  )
340
374
 
341
375
 
@@ -359,7 +393,7 @@ def register_info_to_quantum_type(reg_info: RegisterArithmeticInfo) -> QuantumNu
359
393
  result.set_size_in_bits(reg_info.size)
360
394
  result.is_signed = Expression(expr=str(reg_info.is_signed))
361
395
  result.fraction_digits = Expression(expr=str(reg_info.fraction_places))
362
- result.set_bounds(reg_info.bounds)
396
+ result.set_bounds(tuple(reg_info.bounds)) # type: ignore[arg-type]
363
397
  return result
364
398
 
365
399
 
@@ -0,0 +1,115 @@
1
+ from dataclasses import dataclass
2
+ from typing import Optional, Union
3
+
4
+ from classiq.interface.generator.arith import number_utils
5
+ from classiq.interface.generator.arith.register_user_input import RegisterArithmeticInfo
6
+ from classiq.interface.model.quantum_type import (
7
+ QuantumScalar,
8
+ register_info_to_quantum_type,
9
+ )
10
+
11
+
12
+ @dataclass
13
+ class NumericAttributes:
14
+ size: int
15
+ is_signed: bool
16
+ fraction_digits: int
17
+ bounds: tuple[float, float]
18
+
19
+ def __init__(
20
+ self,
21
+ size: int,
22
+ is_signed: bool,
23
+ fraction_digits: int,
24
+ bounds: Optional[tuple[float, float]] = None,
25
+ trim_bounds: bool = False,
26
+ ) -> None:
27
+ self.size = size
28
+ self.is_signed = is_signed
29
+ self.fraction_digits = fraction_digits
30
+ if bounds is None:
31
+ bounds = RegisterArithmeticInfo.get_maximal_bounds(
32
+ size=size,
33
+ is_signed=is_signed,
34
+ fraction_places=fraction_digits,
35
+ )
36
+ if trim_bounds:
37
+ bounds = (
38
+ number_utils.limit_fraction_places(bounds[0], fraction_digits),
39
+ number_utils.limit_fraction_places(bounds[1], fraction_digits),
40
+ )
41
+ self.bounds = bounds
42
+
43
+ @property
44
+ def lb(self) -> float:
45
+ return self.bounds[0]
46
+
47
+ @property
48
+ def ub(self) -> float:
49
+ return self.bounds[1]
50
+
51
+ @classmethod
52
+ def from_bounds(
53
+ cls, lb: float, ub: float, fraction_places: int, machine_precision: int
54
+ ) -> "NumericAttributes":
55
+ size, is_signed, fraction_digits = number_utils.bounds_to_attributes(
56
+ lb, ub, fraction_places, machine_precision
57
+ )
58
+ return cls(
59
+ size=size,
60
+ is_signed=is_signed,
61
+ fraction_digits=fraction_digits,
62
+ bounds=(lb, ub),
63
+ )
64
+
65
+ @classmethod
66
+ def from_constant(
67
+ cls,
68
+ value: float,
69
+ machine_precision: Optional[int] = None,
70
+ ) -> "NumericAttributes":
71
+ if machine_precision is not None:
72
+ value = number_utils.limit_fraction_places(value, machine_precision)
73
+
74
+ return cls(
75
+ size=number_utils.size(value),
76
+ is_signed=value < 0,
77
+ fraction_digits=number_utils.fraction_places(value),
78
+ bounds=(value, value),
79
+ )
80
+
81
+ @classmethod
82
+ def from_quantum_scalar(
83
+ cls,
84
+ quantum_type: QuantumScalar,
85
+ machine_precision: Optional[int] = None,
86
+ ) -> "NumericAttributes":
87
+ return cls(
88
+ size=quantum_type.size_in_bits,
89
+ is_signed=quantum_type.sign_value,
90
+ fraction_digits=quantum_type.fraction_digits_value,
91
+ bounds=quantum_type.get_effective_bounds(machine_precision),
92
+ )
93
+
94
+ @classmethod
95
+ def from_register_arithmetic_info(
96
+ cls,
97
+ register: RegisterArithmeticInfo,
98
+ machine_precision: Optional[int] = None,
99
+ ) -> "NumericAttributes":
100
+ return cls.from_quantum_scalar(
101
+ quantum_type=register_info_to_quantum_type(register),
102
+ machine_precision=machine_precision,
103
+ )
104
+
105
+ @classmethod
106
+ def from_type_or_constant(
107
+ cls,
108
+ from_: Union[float, QuantumScalar, RegisterArithmeticInfo],
109
+ machine_precision: Optional[int] = None,
110
+ ) -> "NumericAttributes":
111
+ if isinstance(from_, QuantumScalar):
112
+ return cls.from_quantum_scalar(from_, machine_precision)
113
+ if isinstance(from_, RegisterArithmeticInfo):
114
+ return cls.from_register_arithmetic_info(from_, machine_precision)
115
+ return cls.from_constant(from_, machine_precision)