classiq 0.46.1__py3-none-any.whl → 0.47.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 (29) hide show
  1. classiq/_internals/api_wrapper.py +45 -8
  2. classiq/execution/execution_session.py +96 -39
  3. classiq/execution/jobs.py +108 -1
  4. classiq/interface/_version.py +1 -1
  5. classiq/interface/backend/quantum_backend_providers.py +0 -1
  6. classiq/interface/debug_info/debug_info.py +23 -1
  7. classiq/interface/execution/primitives.py +9 -0
  8. classiq/interface/generator/arith/arithmetic_operations.py +5 -2
  9. classiq/interface/generator/arith/binary_ops.py +21 -14
  10. classiq/interface/generator/arith/extremum_operations.py +9 -1
  11. classiq/interface/generator/arith/number_utils.py +6 -0
  12. classiq/interface/generator/arith/register_user_input.py +30 -21
  13. classiq/interface/generator/arith/unary_ops.py +13 -1
  14. classiq/interface/generator/generated_circuit_data.py +47 -2
  15. classiq/interface/generator/quantum_program.py +10 -2
  16. classiq/interface/ide/visual_model.py +7 -1
  17. classiq/interface/interface_version.py +1 -1
  18. classiq/interface/model/phase_operation.py +11 -0
  19. classiq/interface/model/statement_block.py +3 -0
  20. classiq/interface/server/routes.py +0 -3
  21. classiq/model_expansions/interpreter.py +6 -0
  22. classiq/model_expansions/quantum_operations/emitter.py +8 -2
  23. classiq/model_expansions/quantum_operations/phase.py +177 -0
  24. classiq/qmod/builtins/operations.py +14 -0
  25. classiq/qmod/native/pretty_printer.py +6 -0
  26. classiq/qmod/pretty_print/pretty_printer.py +6 -0
  27. {classiq-0.46.1.dist-info → classiq-0.47.0.dist-info}/METADATA +7 -4
  28. {classiq-0.46.1.dist-info → classiq-0.47.0.dist-info}/RECORD +29 -26
  29. {classiq-0.46.1.dist-info → classiq-0.47.0.dist-info}/WHEEL +0 -0
@@ -1,5 +1,5 @@
1
1
  import json
2
- from typing import Dict, List, Optional, Protocol, Type, TypeVar
2
+ from typing import Any, Dict, List, Optional, Protocol, Type, TypeVar
3
3
 
4
4
  import pydantic
5
5
 
@@ -7,6 +7,7 @@ import classiq.interface.executor.execution_result
7
7
  import classiq.interface.pyomo_extension
8
8
  from classiq.interface.analyzer import analysis_params, result as analysis_result
9
9
  from classiq.interface.analyzer.analysis_params import AnalysisRBParams
10
+ from classiq.interface.analyzer.result import GraphStatus
10
11
  from classiq.interface.chemistry import ground_state_problem, operator
11
12
  from classiq.interface.enum_utils import StrEnum
12
13
  from classiq.interface.exceptions import ClassiqAPIError, ClassiqValueError
@@ -16,7 +17,7 @@ from classiq.interface.execution.jobs import (
16
17
  )
17
18
  from classiq.interface.executor import execution_request
18
19
  from classiq.interface.generator import quantum_program as generator_result
19
- from classiq.interface.hardware import HardwareInformation
20
+ from classiq.interface.hardware import HardwareInformation, Provider
20
21
  from classiq.interface.jobs import JobDescription, JobID, JSONObject
21
22
  from classiq.interface.model.model import Model
22
23
  from classiq.interface.server import routes
@@ -36,6 +37,7 @@ class HTTPMethod(StrEnum):
36
37
  GET = "GET"
37
38
  POST = "POST"
38
39
  PATCH = "PATCH"
40
+ PUT = "PUT"
39
41
 
40
42
 
41
43
  class StatusType(Protocol):
@@ -80,8 +82,9 @@ class ApiWrapper:
80
82
  params: Optional[Dict] = None,
81
83
  use_versioned_url: bool = True,
82
84
  headers: Optional[Dict[str, str]] = None,
85
+ allow_none: bool = False,
83
86
  ) -> dict:
84
- res = await client().call_api(
87
+ res: Any = await client().call_api(
85
88
  http_method=http_method,
86
89
  url=url,
87
90
  body=body,
@@ -89,6 +92,8 @@ class ApiWrapper:
89
92
  params=params,
90
93
  use_versioned_url=use_versioned_url,
91
94
  )
95
+ if allow_none and res is None:
96
+ return {}
92
97
  if not isinstance(res, dict):
93
98
  raise ClassiqValueError(f"Unexpected returned value: {res}")
94
99
  return res
@@ -169,6 +174,18 @@ class ApiWrapper:
169
174
  )
170
175
  return ExecutionJobDetailsV1.parse_obj(data)
171
176
 
177
+ @classmethod
178
+ async def call_cancel_execution_job(
179
+ cls,
180
+ job_id: JobID,
181
+ ) -> None:
182
+ await cls._call_task(
183
+ http_method=HTTPMethod.PUT,
184
+ url=f"{routes.EXECUTION_JOBS_NON_VERSIONED_FULL_PATH}/{job_id.job_id}/cancel",
185
+ use_versioned_url=False,
186
+ allow_none=True,
187
+ )
188
+
172
189
  @classmethod
173
190
  async def call_query_execution_jobs(
174
191
  cls,
@@ -267,12 +284,32 @@ class ApiWrapper:
267
284
  cls,
268
285
  params: analysis_params.AnalysisOptionalDevicesParams,
269
286
  ) -> analysis_result.DevicesResult:
270
- data = await cls._call_task(
271
- http_method=HTTPMethod.POST,
272
- url=routes.ANALYZER_OPTIONAL_DEVICES_FULL_PATH,
273
- body=params.dict(),
287
+ hardware_info = await cls.call_get_all_hardware_devices()
288
+ return cls._get_devices_from_hardware_info(hardware_info, params)
289
+
290
+ @staticmethod
291
+ def _get_devices_from_hardware_info(
292
+ hardware_info: List[HardwareInformation],
293
+ params: analysis_params.AnalysisOptionalDevicesParams,
294
+ ) -> analysis_result.DevicesResult:
295
+ available_hardware: Dict[Provider, Dict[str, bool]] = {
296
+ Provider.IBM_QUANTUM: {},
297
+ Provider.AMAZON_BRAKET: {},
298
+ Provider.AZURE_QUANTUM: {},
299
+ }
300
+ for info in hardware_info:
301
+ if info.provider not in available_hardware:
302
+ continue
303
+ is_available = info.number_of_qubits >= params.qubit_count
304
+ available_hardware[info.provider][info.display_name] = is_available
305
+ return analysis_result.DevicesResult(
306
+ devices=analysis_result.AvailableHardware(
307
+ ibm_quantum=available_hardware[Provider.IBM_QUANTUM],
308
+ azure_quantum=available_hardware[Provider.AZURE_QUANTUM],
309
+ amazon_braket=available_hardware[Provider.AMAZON_BRAKET],
310
+ ),
311
+ status=GraphStatus.SUCCESS,
274
312
  )
275
- return analysis_result.DevicesResult.parse_obj(data)
276
313
 
277
314
  @classmethod
278
315
  async def call_get_all_hardware_devices(cls) -> List[HardwareInformation]:
@@ -2,13 +2,11 @@ import json
2
2
  from typing import Any, Callable, Dict, List, Optional, Tuple, Union, cast
3
3
 
4
4
  from classiq.interface.exceptions import ClassiqValueError
5
+ from classiq.interface.execution.primitives import PrimitivesInput
5
6
  from classiq.interface.executor.execution_preferences import ExecutionPreferences
6
- from classiq.interface.executor.execution_result import ResultsCollection
7
7
  from classiq.interface.executor.result import (
8
8
  EstimationResult,
9
- EstimationResults,
10
9
  ExecutionDetails,
11
- MultipleExecutionDetails,
12
10
  )
13
11
  from classiq.interface.generator.functions.qmod_python_interface import QmodPyStruct
14
12
  from classiq.interface.generator.quantum_program import QuantumProgram
@@ -17,6 +15,7 @@ from classiq.applications.combinatorial_helpers.pauli_helpers.pauli_utils import
17
15
  _pauli_dict_to_str,
18
16
  _pauli_terms_to_qmod,
19
17
  )
18
+ from classiq.execution.jobs import ExecutionJob
20
19
  from classiq.executor import execute
21
20
  from classiq.qmod.builtins import PauliTerm
22
21
  from classiq.qmod.builtins.classical_execution_primitives import (
@@ -165,14 +164,6 @@ class ExecutionSession:
165
164
  if execution_preferences is not None:
166
165
  self.program.model.execution_preferences = execution_preferences
167
166
 
168
- def _execute_quantum_program(
169
- self, operation: str, **kwargs: Any
170
- ) -> ResultsCollection:
171
- self.program.model.classical_execution_code = generate_code_snippet(
172
- operation, **kwargs
173
- )
174
- return execute(SerializedQuantumProgram(self.qprog)).result()
175
-
176
167
  def sample(self, parameters: Optional[ExecutionParams] = None) -> ExecutionDetails:
177
168
  """
178
169
  Samples the quantum program with the given parameters, if any.
@@ -180,16 +171,34 @@ class ExecutionSession:
180
171
  Args:
181
172
  parameters: The values to set for the parameters of the quantum program when sampling. Each key should be the name of a parameter in the quantum program (parameters of the main function), and the value should be the value to set for that parameter.
182
173
 
174
+ Returns:
175
+ The result of the sampling.
176
+ """
177
+ job = self.submit_sample(parameters=parameters)
178
+ return job.get_sample_result()
179
+
180
+ def submit_sample(
181
+ self, parameters: Optional[ExecutionParams] = None
182
+ ) -> ExecutionJob:
183
+ """
184
+ Initiates an execution job with the `sample` primitive.
185
+
186
+ This is a non-blocking version of `sample`: it gets the same parameters and initiates the same execution job, but instead
187
+ of waiting for the result, it returns the job object immediately.
188
+
189
+ Args:
190
+ parameters: The values to set for the parameters of the quantum program when sampling. Each key should be the name of a parameter in the quantum program (parameters of the main function), and the value should be the value to set for that parameter.
183
191
 
184
192
  Returns:
185
- ExecutionDetails: The result of the sampling.
193
+ The execution job.
186
194
  """
187
- return cast(
188
- ExecutionDetails,
189
- self._execute_quantum_program(
190
- SupportedPrimitives.SAMPLE, parameters=format_parameters(parameters)
191
- )[0].value,
195
+ self.program.model.classical_execution_code = generate_code_snippet(
196
+ SupportedPrimitives.SAMPLE, parameters=format_parameters(parameters)
197
+ )
198
+ self.program.execution_primitives_input = PrimitivesInput(
199
+ sample=[parameters] if parameters is not None else [{}]
192
200
  )
201
+ return execute(SerializedQuantumProgram(self.qprog))
193
202
 
194
203
  def batch_sample(self, parameters: List[ExecutionParams]) -> List[ExecutionDetails]:
195
204
  """
@@ -201,13 +210,27 @@ class ExecutionSession:
201
210
  Returns:
202
211
  List[ExecutionDetails]: The results of all the sampling iterations.
203
212
  """
204
- return cast(
205
- MultipleExecutionDetails,
206
- self._execute_quantum_program(
207
- SupportedPrimitives.BATCH_SAMPLE,
208
- parameters=format_parameters(parameters),
209
- )[0].value,
210
- ).details
213
+ job = self.submit_batch_sample(parameters=parameters)
214
+ return job.get_batch_sample_result()
215
+
216
+ def submit_batch_sample(self, parameters: List[ExecutionParams]) -> ExecutionJob:
217
+ """
218
+ Initiates an execution job with the `batch_sample` primitive.
219
+
220
+ This is a non-blocking version of `batch_sample`: it gets the same parameters and initiates the same execution job, but instead
221
+ of waiting for the result, it returns the job object immediately.
222
+
223
+ Args:
224
+ parameters: A list of the parameters for each iteration. Each item is a dictionary where each key should be the name of a parameter in the quantum program (parameters of the main function), and the value should be the value to set for that parameter.
225
+
226
+ Returns:
227
+ The execution job.
228
+ """
229
+ self.program.model.classical_execution_code = generate_code_snippet(
230
+ SupportedPrimitives.BATCH_SAMPLE, parameters=format_parameters(parameters)
231
+ )
232
+ self.program.execution_primitives_input = PrimitivesInput(sample=parameters)
233
+ return execute(SerializedQuantumProgram(self.qprog))
211
234
 
212
235
  def estimate(
213
236
  self, hamiltonian: Hamiltonian, parameters: Optional[ExecutionParams] = None
@@ -222,14 +245,31 @@ class ExecutionSession:
222
245
  Returns:
223
246
  EstimationResult: The result of the estimation.
224
247
  """
225
- return cast(
226
- EstimationResult,
227
- self._execute_quantum_program(
228
- SupportedPrimitives.ESTIMATE,
229
- parameters=format_parameters(parameters),
230
- hamiltonian=to_hamiltonian_str(hamiltonian),
231
- )[0].value,
248
+ job = self.submit_estimate(hamiltonian=hamiltonian, parameters=parameters)
249
+ return job.get_estimate_result()
250
+
251
+ def submit_estimate(
252
+ self, hamiltonian: Hamiltonian, parameters: Optional[ExecutionParams] = None
253
+ ) -> ExecutionJob:
254
+ """
255
+ Initiates an execution job with the `estimate` primitive.
256
+
257
+ This is a non-blocking version of `estimate`: it gets the same parameters and initiates the same execution job, but instead
258
+ of waiting for the result, it returns the job object immediately.
259
+
260
+ Args:
261
+ hamiltonian: The Hamiltonian to estimate the expectation value of.
262
+ parameters: The values to set for the parameters of the quantum program when estimating. Each key should be the name of a parameter in the quantum program (parameters of the main function), and the value should be the value to set for that parameter.
263
+
264
+ Returns:
265
+ The execution job.
266
+ """
267
+ self.program.model.classical_execution_code = generate_code_snippet(
268
+ SupportedPrimitives.ESTIMATE,
269
+ parameters=format_parameters(parameters),
270
+ hamiltonian=to_hamiltonian_str(hamiltonian),
232
271
  )
272
+ return execute(SerializedQuantumProgram(self.qprog))
233
273
 
234
274
  def batch_estimate(
235
275
  self, hamiltonian: Hamiltonian, parameters: List[ExecutionParams]
@@ -244,11 +284,28 @@ class ExecutionSession:
244
284
  Returns:
245
285
  List[EstimationResult]: The results of all the estimation iterations.
246
286
  """
247
- return cast(
248
- EstimationResults,
249
- self._execute_quantum_program(
250
- SupportedPrimitives.BATCH_ESTIMATE,
251
- parameters=format_parameters(parameters),
252
- hamiltonian=to_hamiltonian_str(hamiltonian),
253
- )[0].value,
254
- ).results
287
+ job = self.submit_batch_estimate(hamiltonian=hamiltonian, parameters=parameters)
288
+ return job.get_batch_estimate_result()
289
+
290
+ def submit_batch_estimate(
291
+ self, hamiltonian: Hamiltonian, parameters: List[ExecutionParams]
292
+ ) -> ExecutionJob:
293
+ """
294
+ Initiates an execution job with the `batch_estimate` primitive.
295
+
296
+ This is a non-blocking version of `batch_estimate`: it gets the same parameters and initiates the same execution job, but instead
297
+ of waiting for the result, it returns the job object immediately.
298
+
299
+ Args:
300
+ hamiltonian: The Hamiltonian to estimate the expectation value of.
301
+ parameters: A list of the parameters for each iteration. Each item is a dictionary where each key should be the name of a parameter in the quantum program (parameters of the main function), and the value should be the value to set for that parameter.
302
+
303
+ Returns:
304
+ The execution job.
305
+ """
306
+ self.program.model.classical_execution_code = generate_code_snippet(
307
+ SupportedPrimitives.BATCH_ESTIMATE,
308
+ parameters=format_parameters(parameters),
309
+ hamiltonian=to_hamiltonian_str(hamiltonian),
310
+ )
311
+ return execute(SerializedQuantumProgram(self.qprog))
classiq/execution/jobs.py CHANGED
@@ -3,10 +3,19 @@ from datetime import datetime
3
3
  from typing import Any, List, Optional, Union
4
4
  from urllib.parse import urljoin
5
5
 
6
- from classiq.interface.exceptions import ClassiqAPIError
6
+ from classiq.interface.exceptions import (
7
+ ClassiqAPIError,
8
+ ClassiqError,
9
+ )
7
10
  from classiq.interface.execution.jobs import ExecutionJobDetailsV1
8
11
  from classiq.interface.executor.execution_request import ExecutionJobDetails
9
12
  from classiq.interface.executor.execution_result import ResultsCollection
13
+ from classiq.interface.executor.result import (
14
+ EstimationResult,
15
+ EstimationResults,
16
+ ExecutionDetails,
17
+ MultipleExecutionDetails,
18
+ )
10
19
  from classiq.interface.jobs import JobStatus, JSONObject
11
20
  from classiq.interface.server.routes import EXECUTION_JOBS_NON_VERSIONED_FULL_PATH
12
21
 
@@ -21,6 +30,13 @@ _JOB_DETAILS_VERSION = "v1"
21
30
  _JOB_RESULT_VERSION = "v1"
22
31
 
23
32
 
33
+ class ClassiqExecutionResultError(ClassiqError):
34
+ def __init__(self, primitive: str) -> None:
35
+ super().__init__(
36
+ f"Execution job does not contain a single {primitive!r} result, make sure you use the 'get_*_result' method matching the primitive you executed. You can use the 'result' method to see the general result."
37
+ )
38
+
39
+
24
40
  class ExecutionJob:
25
41
  _details: _JobDetails
26
42
  _result: Optional[ResultsCollection]
@@ -112,6 +128,84 @@ class ExecutionJob:
112
128
  def result_value(self, *args: Any, **kwargs: Any) -> Any:
113
129
  return self.result(*args, **kwargs)[0].value
114
130
 
131
+ def get_sample_result(self) -> ExecutionDetails:
132
+ """
133
+ Returns the job's result as a single sample result after validation. If the result is not yet available, waits for it.
134
+
135
+ Returns:
136
+ The sample result of the execution job.
137
+
138
+ Raises:
139
+ ClassiqExecutionResultError: In case the result does not contain a single sample result.
140
+ ClassiqAPIError: In case the job has failed.
141
+ """
142
+ results = self.result()
143
+ if len(results) != 1:
144
+ raise ClassiqExecutionResultError("sample")
145
+
146
+ result = results[0].value
147
+ if isinstance(result, ExecutionDetails):
148
+ return result
149
+ if isinstance(result, MultipleExecutionDetails) and len(result.details) == 1:
150
+ return result.details[0]
151
+ raise ClassiqExecutionResultError("sample")
152
+
153
+ def get_batch_sample_result(self) -> List[ExecutionDetails]:
154
+ """
155
+ Returns the job's result as a single batch_sample result after validation. If the result is not yet available, waits for it.
156
+
157
+ Returns:
158
+ The batch_sample result of the execution job.
159
+
160
+ Raises:
161
+ ClassiqExecutionResultError: In case the result does not contain a single batch_sample result.
162
+ ClassiqAPIError: In case the job has failed.
163
+ """
164
+ results = self.result()
165
+ if len(results) != 1 or not isinstance(
166
+ results[0].value, MultipleExecutionDetails
167
+ ):
168
+ raise ClassiqExecutionResultError("batch_sample")
169
+ return results[0].value.details
170
+
171
+ def get_estimate_result(self) -> EstimationResult:
172
+ """
173
+ Returns the job's result as a single estimate result after validation. If the result is not yet available, waits for it.
174
+
175
+ Returns:
176
+ The estimate result of the execution job.
177
+
178
+ Raises:
179
+ ClassiqExecutionResultError: In case the result does not contain a single estimate result.
180
+ ClassiqAPIError: In case the job has failed.
181
+ """
182
+ results = self.result()
183
+ if len(results) != 1:
184
+ raise ClassiqExecutionResultError("estimate")
185
+
186
+ result = results[0].value
187
+ if isinstance(result, EstimationResult):
188
+ return result
189
+ if isinstance(result, EstimationResults) and len(result.results) == 1:
190
+ return result.results[0]
191
+ raise ClassiqExecutionResultError("estimate")
192
+
193
+ def get_batch_estimate_result(self) -> List[EstimationResult]:
194
+ """
195
+ Returns the job's result as a single batch_estimate result after validation. If the result is not yet available, waits for it.
196
+
197
+ Returns:
198
+ The batch_estimate result of the execution job.
199
+
200
+ Raises:
201
+ ClassiqExecutionResultError: In case the result does not contain a single batch_estimate result.
202
+ ClassiqAPIError: In case the job has failed.
203
+ """
204
+ results = self.result()
205
+ if len(results) != 1 or not isinstance(results[0].value, EstimationResults):
206
+ raise ClassiqExecutionResultError("batch_estimate")
207
+ return results[0].value.results
208
+
115
209
  async def poll_async(self, timeout_sec: Optional[float] = None) -> None:
116
210
  if not self.status.is_final():
117
211
  await self._poll_job(timeout_sec=timeout_sec)
@@ -141,6 +235,19 @@ class ExecutionJob:
141
235
 
142
236
  rename = syncify_function(rename_async)
143
237
 
238
+ async def cancel_async(self) -> None:
239
+ """
240
+ Cancels the execution job. This implies the cancellation of any ongoing jobs
241
+ sent to the provider during this execution job.
242
+
243
+ The function returns without waiting to the actual cancellation. It is possible
244
+ to continue polling the job in order to ensure its cancellation, which might
245
+ not be immediate.
246
+ """
247
+ await ApiWrapper.call_cancel_execution_job(self._job_id)
248
+
249
+ cancel = syncify_function(cancel_async)
250
+
144
251
  @property
145
252
  def ide_url(self) -> str:
146
253
  base_url = client().config.ide
@@ -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.46.1'
6
+ SEMVER_VERSION = '0.47.0'
7
7
  VERSION = str(Version(SEMVER_VERSION))
@@ -68,7 +68,6 @@ class AzureQuantumBackendNames(StrEnum):
68
68
  IONQ_SIMULATOR = "ionq.simulator"
69
69
  MICROSOFT_ESTIMATOR = "microsoft.estimator"
70
70
  MICROSOFT_FULLSTATE_SIMULATOR = "microsoft.simulator.fullstate"
71
- RIGETTI_ASPEN3 = "rigetti.qpu.aspen-m-3"
72
71
  RIGETTI_SIMULATOR = "rigetti.sim.qvm"
73
72
  RIGETTI_ANKAA2 = "rigetti.qpu.ankaa-2"
74
73
  RIGETTI_ANKAA9 = "rigetti.qpu.ankaa-9q-1"
@@ -1,5 +1,5 @@
1
1
  import json
2
- from typing import Any, Dict, Optional, Union
2
+ from typing import Any, Dict, Mapping, Optional, Tuple, Union
3
3
  from uuid import UUID
4
4
 
5
5
  from pydantic import BaseModel, Field
@@ -14,10 +14,12 @@ ParameterValue = Union[float, int, str, None]
14
14
 
15
15
  class FunctionDebugInfo(BaseModel):
16
16
  name: str
17
+ # Parameters describe classical parameters passed to function
17
18
  # ParameterValue appears in type only for backwards compatibility
18
19
  parameters: Dict[str, str] = Field(type=Dict[str, ParameterValue])
19
20
  level: OperationLevel
20
21
  is_allocate_or_free: bool = Field(default=False)
22
+ port_to_passed_variable_map: Dict[str, str] = Field(default_factory=dict)
21
23
 
22
24
  @staticmethod
23
25
  def param_controller(value: Any) -> str:
@@ -26,6 +28,26 @@ class FunctionDebugInfo(BaseModel):
26
28
  except TypeError:
27
29
  return repr(value)
28
30
 
31
+ def update_map_from_port_mapping(self, port_mapping: Mapping[str, str]) -> None:
32
+ new_port_to_passed_variable_map = self.port_to_passed_variable_map.copy()
33
+ for old_key, new_key in port_mapping.items():
34
+ if old_key in new_port_to_passed_variable_map:
35
+ new_port_to_passed_variable_map[new_key] = (
36
+ new_port_to_passed_variable_map.pop(old_key)
37
+ )
38
+ self.port_to_passed_variable_map = new_port_to_passed_variable_map
39
+
40
+ def update_map_from_inout_port_mapping(
41
+ self, port_mapping: Mapping[str, Tuple[str, str]]
42
+ ) -> None:
43
+ new_port_to_passed_variable_map = self.port_to_passed_variable_map.copy()
44
+ for old_key, (new_key1, new_key2) in port_mapping.items():
45
+ if old_key in new_port_to_passed_variable_map:
46
+ value = new_port_to_passed_variable_map.pop(old_key)
47
+ new_port_to_passed_variable_map[new_key1] = value
48
+ new_port_to_passed_variable_map[new_key2] = value
49
+ self.port_to_passed_variable_map = new_port_to_passed_variable_map
50
+
29
51
 
30
52
  class DebugInfoCollection(BaseModel):
31
53
  # Pydantic only started supporting UUID as keys in Pydantic V2
@@ -0,0 +1,9 @@
1
+ from typing import List, Optional
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ from classiq.interface.executor.quantum_code import Arguments
6
+
7
+
8
+ class PrimitivesInput(BaseModel):
9
+ sample: Optional[List[Arguments]] = Field(default=None)
@@ -1,5 +1,5 @@
1
1
  import abc
2
- from typing import ClassVar, Iterable, Optional, Tuple
2
+ from typing import ClassVar, Final, Iterable, Optional, Tuple
3
3
 
4
4
  import pydantic
5
5
 
@@ -9,7 +9,10 @@ from classiq.interface.generator.arith.machine_precision import (
9
9
  from classiq.interface.generator.arith.register_user_input import RegisterArithmeticInfo
10
10
  from classiq.interface.generator.function_params import FunctionParams
11
11
 
12
- DEFAULT_GARBAGE_OUT_NAME: str = "extra_qubits"
12
+ DEFAULT_GARBAGE_OUT_NAME: Final[str] = "extra_qubits"
13
+ MODULO_WITH_FRACTION_PLACES_ERROR_MSG: Final[str] = (
14
+ "Modulo with fraction places not supported"
15
+ )
13
16
 
14
17
 
15
18
  class ArithmeticOperationParams(FunctionParams):
@@ -21,6 +21,7 @@ from classiq.interface.exceptions import ClassiqValueError
21
21
  from classiq.interface.generator.arith import argument_utils, number_utils
22
22
  from classiq.interface.generator.arith.argument_utils import RegisterOrConst
23
23
  from classiq.interface.generator.arith.arithmetic_operations import (
24
+ MODULO_WITH_FRACTION_PLACES_ERROR_MSG,
24
25
  ArithmeticOperationParams,
25
26
  )
26
27
  from classiq.interface.generator.arith.ast_node_rewrite import (
@@ -391,17 +392,20 @@ class Subtractor(InplacableBinaryOpParams[RegisterOrConst, RegisterOrConst]):
391
392
  arg=self.effective_right_arg,
392
393
  output_size=self.negation_output_size,
393
394
  inplace=self.should_inplace_negation,
395
+ bypass_bounds_validation=True,
394
396
  )
395
397
  negation_result = negation_params.result_register
396
398
  if self.output_size is None and max(self.effective_right_arg.bounds) > 0:
397
- negation_result = negation_result.copy(
398
- update=dict(
399
- is_signed=True,
400
- bounds=(
401
- -max(self.effective_right_arg.bounds),
402
- -min(self.effective_right_arg.bounds),
403
- ),
404
- )
399
+ bounds = (
400
+ -max(self.effective_right_arg.bounds),
401
+ -min(self.effective_right_arg.bounds),
402
+ )
403
+ negation_result = RegisterArithmeticInfo(
404
+ size=negation_result.size,
405
+ fraction_places=negation_result.fraction_places,
406
+ is_signed=True,
407
+ bounds=bounds,
408
+ bypass_bounds_validation=True,
405
409
  )
406
410
  adder_params = Adder(
407
411
  left_arg=self.effective_left_arg,
@@ -465,12 +469,8 @@ class Multiplier(BinaryOpWithFloatInputs):
465
469
  for right in argument_utils.bounds(args[1])
466
470
  ]
467
471
  return (
468
- number_utils.limit_fraction_places(
469
- min(extremal_values), machine_precision=machine_precision
470
- ),
471
- number_utils.limit_fraction_places(
472
- max(extremal_values), machine_precision=machine_precision
473
- ),
472
+ number_utils.limit_fraction_places(min(extremal_values), machine_precision),
473
+ number_utils.limit_fraction_places(max(extremal_values), machine_precision),
474
474
  )
475
475
 
476
476
  def _get_result_register(self) -> RegisterArithmeticInfo:
@@ -482,6 +482,13 @@ class Multiplier(BinaryOpWithFloatInputs):
482
482
  self.right_arg, self.machine_precision
483
483
  )
484
484
  bounds = self._get_bounds((left_arg, right_arg), self.machine_precision)
485
+ if self.output_size:
486
+ if fraction_places:
487
+ raise ValueError(MODULO_WITH_FRACTION_PLACES_ERROR_MSG)
488
+ max_bounds = RegisterArithmeticInfo.get_maximal_bounds(
489
+ size=self.output_size, is_signed=False, fraction_places=0
490
+ )
491
+ bounds = number_utils.bounds_cut(bounds, max_bounds)
485
492
 
486
493
  return RegisterArithmeticInfo(
487
494
  size=self.output_size
@@ -4,9 +4,10 @@ from typing import Any, Dict, Iterable
4
4
  import pydantic
5
5
 
6
6
  from classiq.interface.exceptions import ClassiqValueError
7
- from classiq.interface.generator.arith import argument_utils
7
+ from classiq.interface.generator.arith import argument_utils, number_utils
8
8
  from classiq.interface.generator.arith.argument_utils import RegisterOrConst
9
9
  from classiq.interface.generator.arith.arithmetic_operations import (
10
+ MODULO_WITH_FRACTION_PLACES_ERROR_MSG,
10
11
  ArithmeticOperationParams,
11
12
  )
12
13
  from classiq.interface.generator.arith.binary_ops import (
@@ -108,6 +109,13 @@ class Extremum(ArithmeticOperationParams):
108
109
  argument_utils.upper_bound(eff_right_arg),
109
110
  ),
110
111
  )
112
+ if self.output_size:
113
+ if fraction_places:
114
+ raise ValueError(MODULO_WITH_FRACTION_PLACES_ERROR_MSG)
115
+ max_bounds = RegisterArithmeticInfo.get_maximal_bounds(
116
+ size=self.output_size, is_signed=False, fraction_places=0
117
+ )
118
+ bounds = number_utils.bounds_cut(bounds, max_bounds)
111
119
  return RegisterArithmeticInfo(
112
120
  size=self.output_size or required_size,
113
121
  fraction_places=fraction_places,
@@ -110,3 +110,9 @@ def limit_fraction_places(number: float, machine_precision: int) -> float:
110
110
  fraction_part_size=orig_fractions - removed_fractions,
111
111
  is_signed=number < 0,
112
112
  )
113
+
114
+
115
+ def bounds_cut(
116
+ bounds1: Tuple[float, float], bounds2: Tuple[float, float]
117
+ ) -> Tuple[float, float]:
118
+ return max(min(bounds1), min(bounds2)), min(max(bounds1), max(bounds2))