classiq 0.83.0__py3-none-any.whl → 0.84.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 (82) hide show
  1. classiq/_internals/api_wrapper.py +27 -0
  2. classiq/applications/chemistry/chemistry_model_constructor.py +0 -2
  3. classiq/applications/chemistry/hartree_fock.py +68 -0
  4. classiq/applications/chemistry/mapping.py +85 -0
  5. classiq/applications/chemistry/op_utils.py +79 -0
  6. classiq/applications/chemistry/problems.py +195 -0
  7. classiq/applications/chemistry/ucc.py +109 -0
  8. classiq/applications/chemistry/z2_symmetries.py +368 -0
  9. classiq/applications/combinatorial_helpers/pauli_helpers/pauli_utils.py +30 -1
  10. classiq/{model_expansions/evaluators → evaluators}/arg_type_match.py +12 -4
  11. classiq/{model_expansions/evaluators → evaluators}/argument_types.py +1 -1
  12. classiq/{model_expansions/evaluators → evaluators}/classical_expression.py +1 -1
  13. classiq/{model_expansions/evaluators → evaluators}/classical_type_inference.py +3 -4
  14. classiq/{model_expansions/evaluators → evaluators}/parameter_types.py +17 -15
  15. classiq/execution/__init__.py +12 -1
  16. classiq/execution/execution_session.py +189 -43
  17. classiq/execution/jobs.py +26 -1
  18. classiq/execution/qnn.py +2 -2
  19. classiq/execution/user_budgets.py +39 -0
  20. classiq/interface/_version.py +1 -1
  21. classiq/interface/constants.py +1 -0
  22. classiq/interface/execution/primitives.py +29 -1
  23. classiq/interface/executor/estimate_cost.py +35 -0
  24. classiq/interface/executor/execution_result.py +13 -0
  25. classiq/interface/executor/result.py +116 -1
  26. classiq/interface/executor/user_budget.py +26 -33
  27. classiq/interface/generator/expressions/atomic_expression_functions.py +5 -1
  28. classiq/interface/generator/expressions/proxies/classical/any_classical_value.py +0 -6
  29. classiq/interface/generator/functions/classical_type.py +2 -35
  30. classiq/interface/generator/functions/concrete_types.py +0 -3
  31. classiq/interface/generator/functions/type_modifier.py +0 -19
  32. classiq/interface/generator/generated_circuit_data.py +0 -8
  33. classiq/interface/generator/types/compilation_metadata.py +0 -3
  34. classiq/interface/ide/visual_model.py +6 -2
  35. classiq/interface/model/model.py +12 -7
  36. classiq/interface/model/port_declaration.py +2 -24
  37. classiq/interface/pretty_print/__init__.py +0 -0
  38. classiq/{qmod/native → interface/pretty_print}/expression_to_qmod.py +18 -11
  39. classiq/interface/server/routes.py +4 -0
  40. classiq/model_expansions/atomic_expression_functions_defs.py +42 -5
  41. classiq/model_expansions/interpreters/base_interpreter.py +3 -3
  42. classiq/model_expansions/quantum_operations/allocate.py +1 -1
  43. classiq/model_expansions/quantum_operations/assignment_result_processor.py +1 -1
  44. classiq/model_expansions/quantum_operations/bind.py +2 -2
  45. classiq/model_expansions/quantum_operations/call_emitter.py +26 -20
  46. classiq/model_expansions/quantum_operations/variable_decleration.py +1 -1
  47. classiq/model_expansions/scope_initialization.py +3 -3
  48. classiq/model_expansions/transformers/model_renamer.py +6 -4
  49. classiq/model_expansions/transformers/type_modifier_inference.py +81 -43
  50. classiq/model_expansions/visitors/symbolic_param_inference.py +2 -3
  51. classiq/open_library/functions/__init__.py +3 -0
  52. classiq/open_library/functions/amplitude_amplification.py +10 -18
  53. classiq/open_library/functions/discrete_sine_cosine_transform.py +5 -5
  54. classiq/open_library/functions/grover.py +14 -6
  55. classiq/open_library/functions/modular_exponentiation.py +22 -20
  56. classiq/open_library/functions/state_preparation.py +17 -0
  57. classiq/qmod/builtins/enums.py +23 -0
  58. classiq/qmod/builtins/functions/__init__.py +2 -0
  59. classiq/qmod/builtins/functions/exponentiation.py +32 -4
  60. classiq/qmod/builtins/structs.py +55 -3
  61. classiq/qmod/declaration_inferrer.py +3 -2
  62. classiq/qmod/native/pretty_printer.py +2 -6
  63. classiq/qmod/pretty_print/expression_to_python.py +2 -1
  64. classiq/qmod/pretty_print/pretty_printer.py +1 -6
  65. classiq/qmod/python_classical_type.py +12 -5
  66. classiq/qmod/qmod_constant.py +2 -5
  67. classiq/qmod/qmod_parameter.py +2 -5
  68. classiq/qmod/qmod_variable.py +56 -15
  69. classiq/qmod/quantum_expandable.py +4 -2
  70. classiq/qmod/quantum_function.py +7 -2
  71. classiq/qmod/semantics/annotation/qstruct_annotator.py +1 -1
  72. classiq/qmod/semantics/validation/main_validation.py +1 -9
  73. classiq/qmod/utilities.py +0 -2
  74. classiq/qmod/write_qmod.py +1 -1
  75. {classiq-0.83.0.dist-info → classiq-0.84.0.dist-info}/METADATA +4 -1
  76. {classiq-0.83.0.dist-info → classiq-0.84.0.dist-info}/RECORD +82 -73
  77. /classiq/{model_expansions/evaluators → evaluators}/__init__.py +0 -0
  78. /classiq/{model_expansions/evaluators → evaluators}/control.py +0 -0
  79. /classiq/{model_expansions → evaluators}/expression_evaluator.py +0 -0
  80. /classiq/{model_expansions/evaluators → evaluators}/quantum_type_utils.py +0 -0
  81. /classiq/{model_expansions/evaluators → evaluators}/type_type_match.py +0 -0
  82. {classiq-0.83.0.dist-info → classiq-0.84.0.dist-info}/WHEEL +0 -0
@@ -1,24 +1,36 @@
1
+ import inspect
1
2
  import random
2
3
  from types import TracebackType
3
4
  from typing import Callable, Optional, Union, cast
4
5
 
5
- import numpy as np
6
-
7
6
  from classiq.interface.chemistry.operator import PauliOperator, pauli_integers_to_str
8
7
  from classiq.interface.exceptions import ClassiqError, ClassiqValueError
9
- from classiq.interface.execution.primitives import EstimateInput, PrimitivesInput
8
+ from classiq.interface.execution.primitives import (
9
+ EstimateInput,
10
+ MinimizeClassicalCostInput,
11
+ MinimizeQuantumCostInput,
12
+ PrimitivesInput,
13
+ )
14
+ from classiq.interface.executor.estimate_cost import estimate_cost
10
15
  from classiq.interface.executor.execution_preferences import ExecutionPreferences
16
+ from classiq.interface.executor.execution_result import TaggedMinimizeResult
11
17
  from classiq.interface.executor.result import (
12
18
  EstimationResult,
13
19
  ExecutionDetails,
14
20
  ParsedState,
15
21
  )
16
22
  from classiq.interface.generator.arith import number_utils
23
+ from classiq.interface.generator.expressions.expression import Expression
17
24
  from classiq.interface.generator.functions.qmod_python_interface import QmodPyStruct
18
25
  from classiq.interface.generator.quantum_program import (
19
26
  QuantumProgram,
20
27
  )
21
- from classiq.interface.model.quantum_type import QuantumBit, QuantumNumeric
28
+ from classiq.interface.helpers.custom_pydantic_types import PydanticPauliList
29
+ from classiq.interface.model.quantum_type import (
30
+ QuantumBit,
31
+ QuantumNumeric,
32
+ RegisterQuantumTypeDict,
33
+ )
22
34
 
23
35
  from classiq._internals import async_utils
24
36
  from classiq._internals.api_wrapper import ApiWrapper
@@ -27,38 +39,24 @@ from classiq.applications.combinatorial_helpers.pauli_helpers.pauli_utils import
27
39
  _pauli_dict_to_pauli_terms,
28
40
  )
29
41
  from classiq.execution.jobs import ExecutionJob
30
- from classiq.qmod.builtins import PauliTerm
31
42
  from classiq.qmod.builtins.classical_execution_primitives import (
32
43
  CARRAY_SEPARATOR,
33
44
  ExecutionParams,
34
45
  )
46
+ from classiq.qmod.builtins.structs import PauliTerm, SparsePauliOp
47
+ from classiq.qmod.qmod_variable import (
48
+ QmodExpressionCreator,
49
+ create_qvar_from_quantum_type,
50
+ )
35
51
 
36
- Hamiltonian = Union[list[QmodPyStruct], list[PauliTerm]]
37
- ParsedExecutionParams = dict[str, Union[float, int]]
52
+ Hamiltonian = SparsePauliOp
38
53
  ExecutionParameters = Optional[Union[ExecutionParams, list[ExecutionParams]]]
54
+ ParsedExecutionParams = dict[str, Union[float, int]]
39
55
  ParsedExecutionParameters = Optional[
40
56
  Union[ParsedExecutionParams, list[ParsedExecutionParams]]
41
57
  ]
42
58
 
43
59
 
44
- def hamiltonian_to_pauli_terms(hamiltonian: Hamiltonian) -> list[PauliTerm]:
45
- if isinstance(hamiltonian[0], PauliTerm):
46
- return cast(list[PauliTerm], hamiltonian)
47
- else:
48
- return _pauli_dict_to_pauli_terms(cast(list[QmodPyStruct], hamiltonian))
49
-
50
-
51
- def _hamiltonian_to_pauli_operator(hamiltonian: Hamiltonian) -> PauliOperator:
52
- pauli_list = [
53
- (
54
- pauli_integers_to_str(elem.pauli), # type: ignore[arg-type]
55
- elem.coefficient,
56
- )
57
- for elem in hamiltonian_to_pauli_terms(hamiltonian)
58
- ]
59
- return PauliOperator(pauli_list=pauli_list)
60
-
61
-
62
60
  def parse_params(params: ExecutionParams) -> ParsedExecutionParams:
63
61
  result = {}
64
62
  for key, values in params.items():
@@ -251,7 +249,7 @@ class ExecutionSession:
251
249
  """
252
250
  execution_primitives_input = PrimitivesInput(
253
251
  estimate=EstimateInput(
254
- hamiltonian=_hamiltonian_to_pauli_operator(hamiltonian),
252
+ hamiltonian=self._hamiltonian_to_pauli_operator(hamiltonian),
255
253
  parameters=(
256
254
  [parse_params(parameters)] if parameters is not None else [{}]
257
255
  ),
@@ -293,12 +291,109 @@ class ExecutionSession:
293
291
  """
294
292
  execution_primitives_input = PrimitivesInput(
295
293
  estimate=EstimateInput(
296
- hamiltonian=_hamiltonian_to_pauli_operator(hamiltonian),
294
+ hamiltonian=self._hamiltonian_to_pauli_operator(hamiltonian),
297
295
  parameters=[parse_params(params) for params in parameters],
298
296
  )
299
297
  )
300
298
  return self._execute(execution_primitives_input)
301
299
 
300
+ def minimize(
301
+ self,
302
+ cost_function: Union[Hamiltonian, QmodExpressionCreator],
303
+ initial_params: ExecutionParams,
304
+ max_iteration: int,
305
+ quantile: float = 1.0,
306
+ ) -> list[tuple[float, ExecutionParams]]:
307
+ """
308
+ Minimizes the given cost function using the quantum program.
309
+ Args:
310
+ cost_function: The cost function to minimize. It can be one of the following:
311
+ - A quantum cost function defined by a Hamiltonian.
312
+ - A classical cost function represented as a callable that returns a Qmod expression.
313
+ The callable should accept `QVar`s as arguments and use names matching the Model outputs.
314
+ initial_params: The initial parameters for the minimization.
315
+ Only Models with exactly one execution parameter are supported. This parameter must be of type
316
+ `CReal` or `CArray`. The dictionary must contain a single key-value pair, where:
317
+ - The key is the name of the parameter.
318
+ - The value is either a float or a list of floats.
319
+ max_iteration: The maximum number of iterations for the minimization.
320
+ quantile: The quantile to use for cost estimation.
321
+ Returns:
322
+ A list of tuples, each containing the estimated cost and the corresponding parameters for that iteration.
323
+ `cost` is a float, and `parameters` is a dictionary matching the execution parameter format.
324
+ """
325
+ job = self.submit_minimize(
326
+ cost_function=cost_function,
327
+ initial_params=initial_params,
328
+ max_iteration=max_iteration,
329
+ quantile=quantile,
330
+ )
331
+ result = job.get_minimization_result(_http_client=self._async_client)
332
+
333
+ return self._minimize_result_to_result(
334
+ result=result, initial_params=initial_params
335
+ )
336
+
337
+ def submit_minimize(
338
+ self,
339
+ cost_function: Union[Hamiltonian, QmodExpressionCreator],
340
+ initial_params: ExecutionParams,
341
+ max_iteration: int,
342
+ quantile: float = 1.0,
343
+ ) -> ExecutionJob:
344
+ """
345
+ Initiates an execution job with the `minimize` primitive.
346
+
347
+ This is a non-blocking version of `minimize`: it gets the same parameters and initiates the same execution job, but instead
348
+ of waiting for the result, it returns the job object immediately.
349
+
350
+ Args:
351
+ cost_function: The cost function to minimize. It can be one of the following:
352
+ - A quantum cost function defined by a Hamiltonian.
353
+ - A classical cost function represented as a callable that returns a Qmod expression.
354
+ The callable should accept `QVar`s as arguments and use names matching the Model outputs.
355
+ initial_params: The initial parameters for the minimization.
356
+ Only Models with exactly one execution parameter are supported. This parameter must be of type
357
+ `CReal` or `CArray`. The dictionary must contain a single key-value pair, where:
358
+ - The key is the name of the parameter.
359
+ - The value is either a float or a list of floats.
360
+ max_iteration: The maximum number of iterations for the minimization.
361
+ quantile: The quantile to use for cost estimation.
362
+
363
+ Returns:
364
+ The execution job.
365
+ """
366
+ if len(initial_params) != 1:
367
+ raise ClassiqValueError(
368
+ "The initial parameters must be a dictionary with a single key-value pair."
369
+ )
370
+
371
+ _cost_function: Union[PauliOperator, Expression]
372
+ _initial_params = parse_params(initial_params)
373
+ minimize: Union[MinimizeQuantumCostInput, MinimizeClassicalCostInput]
374
+ if callable(cost_function):
375
+ circuit_output_types = self.program.model.circuit_output_types
376
+ _cost_function = self._create_qmod_expression(
377
+ circuit_output_types, cost_function
378
+ )
379
+ minimize = MinimizeClassicalCostInput(
380
+ cost_function=_cost_function,
381
+ initial_params=_initial_params,
382
+ max_iteration=max_iteration,
383
+ quantile=quantile,
384
+ )
385
+ else:
386
+ _cost_function = self._hamiltonian_to_pauli_operator(cost_function)
387
+ minimize = MinimizeQuantumCostInput(
388
+ cost_function=_cost_function,
389
+ initial_params=_initial_params,
390
+ max_iteration=max_iteration,
391
+ quantile=quantile,
392
+ )
393
+
394
+ execution_primitives_input = PrimitivesInput(minimize=minimize)
395
+ return self._execute(execution_primitives_input)
396
+
302
397
  def estimate_cost(
303
398
  self,
304
399
  cost_func: Callable[[ParsedState], float],
@@ -319,23 +414,8 @@ class ExecutionSession:
319
414
  See Also:
320
415
  sample
321
416
  """
322
- if quantile < 0 or quantile > 1:
323
- raise ClassiqValueError("'quantile' must be between 0 and 1")
324
417
  res = self.sample(parameters)
325
-
326
- counts = np.array(res.parsed_counts)
327
- costs = np.vectorize(lambda sample: cost_func(sample.state))(counts)
328
- shots = np.vectorize(lambda sample: sample.shots)(counts)
329
-
330
- if quantile == 1:
331
- return float(np.average(costs, weights=shots))
332
- costs = np.repeat(costs, shots)
333
- sort_idx = costs.argsort()
334
- sort_idx = sort_idx[: int(quantile * len(costs))]
335
- costs = costs[sort_idx]
336
- if costs.size == 0:
337
- return np.nan
338
- return float(np.average(costs))
418
+ return estimate_cost(cost_func, res.parsed_counts, quantile=quantile)
339
419
 
340
420
  def set_measured_state_filter(
341
421
  self,
@@ -387,3 +467,69 @@ class ExecutionSession:
387
467
  )
388
468
 
389
469
  self.program.model.register_filter_bitstrings[output_name] = legal_bitstrings
470
+
471
+ @staticmethod
472
+ def _hamiltonian_to_pauli_operator(hamiltonian: Hamiltonian) -> PauliOperator:
473
+ pauli_list: PydanticPauliList
474
+ # FIXME: Remove compatibility (CLS-2912)
475
+ if isinstance(hamiltonian, list): # type:ignore[unreachable]
476
+ pauli_list = [ # type:ignore[unreachable]
477
+ (
478
+ pauli_integers_to_str(elem.pauli),
479
+ cast(complex, elem.coefficient),
480
+ )
481
+ for elem in ExecutionSession._hamiltonian_to_pauli_terms(hamiltonian)
482
+ ]
483
+ return PauliOperator(pauli_list=pauli_list)
484
+ pauli_list = []
485
+ for term in cast(list, hamiltonian.terms):
486
+ paulis = ["I"] * cast(int, hamiltonian.num_qubits)
487
+ for indexed_pauli in term.paulis:
488
+ paulis[len(paulis) - indexed_pauli.index - 1] = indexed_pauli.pauli.name
489
+ pauli_list.append(("".join(paulis), term.coefficient))
490
+ return PauliOperator(pauli_list=pauli_list)
491
+
492
+ @staticmethod
493
+ def _create_qmod_expression(
494
+ circuit_output_types: RegisterQuantumTypeDict,
495
+ qmod_expression_creator: QmodExpressionCreator,
496
+ ) -> Expression:
497
+ symbolic_output = {
498
+ name: create_qvar_from_quantum_type(reg.quantum_types, name)
499
+ for name, reg in circuit_output_types.items()
500
+ }
501
+ for name in inspect.signature(qmod_expression_creator).parameters.keys():
502
+ if name not in symbolic_output:
503
+ raise ClassiqValueError(
504
+ f"The provided QVar: {name} does not match the model outputs: {tuple(circuit_output_types.keys())}. "
505
+ )
506
+
507
+ qmod_expression = qmod_expression_creator(**symbolic_output)
508
+ return Expression(expr=str(qmod_expression))
509
+
510
+ @staticmethod
511
+ def _hamiltonian_to_pauli_terms(hamiltonian: list) -> list[PauliTerm]:
512
+ if isinstance(hamiltonian[0], PauliTerm):
513
+ return cast(list[PauliTerm], hamiltonian)
514
+ else:
515
+ return _pauli_dict_to_pauli_terms(cast(list[QmodPyStruct], hamiltonian))
516
+
517
+ @staticmethod
518
+ def _minimize_result_to_result(
519
+ result: TaggedMinimizeResult, initial_params: ExecutionParams
520
+ ) -> list[tuple[float, ExecutionParams]]:
521
+ param_name = next(iter(initial_params.keys()))
522
+ param_value = initial_params[param_name]
523
+ return [
524
+ (
525
+ res.expectation_value,
526
+ {
527
+ param_name: (
528
+ res.parameters[0]
529
+ if isinstance(param_value, (float, int))
530
+ else res.parameters
531
+ )
532
+ },
533
+ )
534
+ for res in result.value
535
+ ]
classiq/execution/jobs.py CHANGED
@@ -10,7 +10,10 @@ from classiq.interface.exceptions import (
10
10
  ClassiqError,
11
11
  )
12
12
  from classiq.interface.executor.execution_request import ExecutionJobDetails, JobCost
13
- from classiq.interface.executor.execution_result import ResultsCollection
13
+ from classiq.interface.executor.execution_result import (
14
+ ResultsCollection,
15
+ TaggedMinimizeResult,
16
+ )
14
17
  from classiq.interface.executor.result import (
15
18
  EstimationResult,
16
19
  EstimationResults,
@@ -244,6 +247,28 @@ class ExecutionJob:
244
247
 
245
248
  raise ClassiqExecutionResultError("batch_estimate")
246
249
 
250
+ def get_minimization_result(
251
+ self, _http_client: Optional[httpx.AsyncClient] = None
252
+ ) -> TaggedMinimizeResult:
253
+ """
254
+ Returns the job's result as a single minimization result after validation. If the result is not yet available, waits for it.
255
+
256
+ Returns:
257
+ The minimization result of the execution job.
258
+
259
+ Raises:
260
+ ClassiqExecutionResultError: In case the result does not contain a single minimization result.
261
+ ClassiqAPIError: In case the job has failed.
262
+ """
263
+ results = self.result(_http_client=_http_client)
264
+ if len(results) != 1:
265
+ raise ClassiqExecutionResultError("minimization")
266
+
267
+ result = results[0]
268
+ if isinstance(result, TaggedMinimizeResult):
269
+ return result
270
+ raise ClassiqExecutionResultError("minimization")
271
+
247
272
  async def poll_async(
248
273
  self,
249
274
  timeout_sec: Optional[float] = None,
classiq/execution/qnn.py CHANGED
@@ -15,7 +15,7 @@ from classiq.interface.executor.quantum_code import Arguments, MultipleArguments
15
15
 
16
16
  from classiq import QuantumProgram
17
17
  from classiq.applications.combinatorial_helpers.pauli_helpers.pauli_utils import (
18
- pauli_operator_to_hamiltonian,
18
+ pauli_operator_to_sparse_hamiltonian,
19
19
  )
20
20
  from classiq.execution.execution_session import ExecutionSession
21
21
 
@@ -27,7 +27,7 @@ def _execute_qnn_estimate(
27
27
  arguments: list[Arguments],
28
28
  observable: PauliOperator,
29
29
  ) -> ResultsCollection:
30
- hamiltonian = pauli_operator_to_hamiltonian(observable.pauli_list)
30
+ hamiltonian = pauli_operator_to_sparse_hamiltonian(observable.pauli_list)
31
31
  return [
32
32
  TaggedEstimationResult(
33
33
  name=DEFAULT_RESULT_NAME,
@@ -36,3 +36,42 @@ async def get_budget_async(
36
36
 
37
37
 
38
38
  get_budget = syncify_function(get_budget_async)
39
+
40
+
41
+ async def set_budget_limit_async(
42
+ provider_vendor: ProviderVendor,
43
+ limit: float,
44
+ ) -> UserBudgets:
45
+ provider = PROVIDER_MAPPER.get(provider_vendor, None)
46
+ if not provider:
47
+ raise ValueError(f"Unsupported provider: {provider_vendor}")
48
+
49
+ budget = get_budget(provider_vendor)
50
+ if budget is None:
51
+ raise ValueError(f"No budget found for provider: {provider_vendor}")
52
+
53
+ if limit <= 0:
54
+ raise ValueError("Budget limit must be greater than zero.")
55
+
56
+ if limit > budget.budgets[0].available_budget:
57
+ print( # noqa: T201
58
+ f"Budget limit {limit} exceeds available budget {budget.budgets[0].available_budget} for provider {provider_vendor}.\n"
59
+ "Setting budget limit to the maximum available budget."
60
+ )
61
+ budgets_list = await ApiWrapper().call_set_budget_limit(provider, limit)
62
+ return UserBudgets(budgets=[budgets_list])
63
+
64
+
65
+ set_budget_limit = syncify_function(set_budget_limit_async)
66
+
67
+
68
+ async def clear_budget_limit_async(provider_vendor: ProviderVendor) -> UserBudgets:
69
+ provider = PROVIDER_MAPPER.get(provider_vendor, None)
70
+ if not provider:
71
+ raise ValueError(f"Unsupported provider: {provider_vendor}")
72
+
73
+ budgets_list = await ApiWrapper().call_clear_budget_limit(provider)
74
+ return UserBudgets(budgets=[budgets_list])
75
+
76
+
77
+ clear_budget_limit = syncify_function(clear_budget_limit_async)
@@ -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.83.0'
6
+ SEMVER_VERSION = '0.84.0'
7
7
  VERSION = str(Version(SEMVER_VERSION))
@@ -0,0 +1 @@
1
+ DEFAULT_DECIMAL_PRECISION = 4
@@ -1,9 +1,10 @@
1
- from typing import Optional
1
+ from typing import Annotated, Literal, Optional, Union
2
2
 
3
3
  from pydantic import BaseModel, Field
4
4
 
5
5
  from classiq.interface.chemistry.operator import PauliOperator
6
6
  from classiq.interface.executor.quantum_code import Arguments
7
+ from classiq.interface.generator.expressions.expression import Expression
7
8
  from classiq.interface.helpers.custom_encoders import CUSTOM_ENCODERS
8
9
 
9
10
 
@@ -12,7 +13,34 @@ class EstimateInput(BaseModel, json_encoders=CUSTOM_ENCODERS):
12
13
  parameters: list[Arguments]
13
14
 
14
15
 
16
+ class MinimizeCostInput(BaseModel, json_encoders=CUSTOM_ENCODERS):
17
+ initial_params: Arguments
18
+ max_iteration: int
19
+ quantile: float
20
+
21
+
22
+ class MinimizeClassicalCostInput(MinimizeCostInput):
23
+ cost_function: Expression
24
+ kind: Literal["MinimizeClassicalCostInput"] = Field(
25
+ default="MinimizeClassicalCostInput"
26
+ )
27
+
28
+
29
+ class MinimizeQuantumCostInput(MinimizeCostInput):
30
+ cost_function: PauliOperator
31
+ kind: Literal["MinimizeQuantumCostInput"] = Field(
32
+ default="MinimizeQuantumCostInput"
33
+ )
34
+
35
+
36
+ ConcreteMinimizeCostInput = Annotated[
37
+ Union[MinimizeQuantumCostInput, MinimizeClassicalCostInput],
38
+ Field(discriminator="kind"),
39
+ ]
40
+
41
+
15
42
  class PrimitivesInput(BaseModel, json_encoders=CUSTOM_ENCODERS):
16
43
  sample: Optional[list[Arguments]] = Field(default=None)
17
44
  estimate: Optional[EstimateInput] = Field(default=None)
45
+ minimize: Optional[ConcreteMinimizeCostInput] = Field(default=None)
18
46
  random_seed: Optional[int] = Field(default=None)
@@ -0,0 +1,35 @@
1
+ from typing import Callable
2
+
3
+ import numpy as np
4
+
5
+ from classiq.interface.exceptions import ClassiqValueError
6
+ from classiq.interface.executor.result import ParsedCounts, ParsedState
7
+
8
+
9
+ def estimate_cost(
10
+ cost_func: Callable[[ParsedState], float],
11
+ parsed_counts: ParsedCounts,
12
+ quantile: float = 1.0,
13
+ ) -> float:
14
+ if quantile < 0 or quantile > 1:
15
+ raise ClassiqValueError("'quantile' must be between 0 and 1")
16
+ costs = np.fromiter((cost_func(sample.state) for sample in parsed_counts), float)
17
+ shots = np.fromiter((sample.shots for sample in parsed_counts), int)
18
+
19
+ if quantile == 1:
20
+ return float(np.average(costs, weights=shots))
21
+ return float(estimate_quantile_cost(costs, shots, quantile=quantile))
22
+
23
+
24
+ def estimate_quantile_cost(
25
+ costs: np.ndarray,
26
+ shots: np.ndarray,
27
+ quantile: float,
28
+ ) -> np.floating:
29
+ repeated_costs = np.repeat(costs, shots)
30
+ sort_idx = repeated_costs.argsort()
31
+ cutoff_idx = sort_idx[: int(quantile * len(repeated_costs))]
32
+ sorted_costs = repeated_costs[cutoff_idx]
33
+ if sorted_costs.size == 0:
34
+ sorted_costs = repeated_costs[sort_idx[0]]
35
+ return np.average(sorted_costs)
@@ -25,6 +25,7 @@ class SavedResultValueType(StrEnum):
25
25
  EstimationResult = "EstimationResult"
26
26
  EstimationResults = "EstimationResults"
27
27
  IQAEResult = "IQAEResult"
28
+ MinimizeResult = "MinimizeResult"
28
29
  Unstructured = "Unstructured"
29
30
 
30
31
 
@@ -88,6 +89,17 @@ class TaggedUnstructured(BaseModel):
88
89
  value: Any = None
89
90
 
90
91
 
92
+ class SingleMinimizeResult(BaseModel):
93
+ expectation_value: float
94
+ parameters: list[float]
95
+
96
+
97
+ class TaggedMinimizeResult(BaseModel):
98
+ value_type: Literal[SavedResultValueType.MinimizeResult]
99
+ name: str
100
+ value: list[SingleMinimizeResult]
101
+
102
+
91
103
  SavedResult = Annotated[
92
104
  Union[
93
105
  TaggedInteger,
@@ -100,6 +112,7 @@ SavedResult = Annotated[
100
112
  TaggedEstimationResults,
101
113
  TaggedIQAEResult,
102
114
  TaggedUnstructured,
115
+ TaggedMinimizeResult,
103
116
  ],
104
117
  Field(..., discriminator="value_type"),
105
118
  ]
@@ -10,11 +10,16 @@ from typing import (
10
10
  Union,
11
11
  )
12
12
 
13
+ import pandas as pd
13
14
  import pydantic
14
15
  from pydantic import BaseModel
15
16
  from typing_extensions import Self, TypeAlias
16
17
 
17
- from classiq.interface.exceptions import ClassiqError
18
+ from classiq.interface.exceptions import (
19
+ ClassiqError,
20
+ ClassiqInternalError,
21
+ ClassiqValueError,
22
+ )
18
23
  from classiq.interface.executor.quantum_code import OutputQubitsMap, Qubits
19
24
  from classiq.interface.generator.arith import number_utils
20
25
  from classiq.interface.generator.complex_type import Complex
@@ -22,6 +27,7 @@ from classiq.interface.generator.functions.classical_type import QmodPyObject
22
27
  from classiq.interface.helpers.custom_pydantic_types import PydanticNonNegIntTuple
23
28
  from classiq.interface.helpers.datastructures import get_sdk_compatible_python_object
24
29
  from classiq.interface.helpers.versioned_model import VersionedModel
30
+ from classiq.interface.model.quantum_type import RegisterQuantumTypeDict
25
31
 
26
32
  _ILLEGAL_QUBIT_ERROR_MSG: str = "Illegal qubit index requested"
27
33
  _REPEATED_QUBIT_ERROR_MSG: str = "Requested a qubit more than once"
@@ -36,6 +42,11 @@ ParsedStates: TypeAlias = Mapping[State, ParsedState]
36
42
  Counts: TypeAlias = dict[State, MeasuredShots]
37
43
  StateVector: TypeAlias = Optional[dict[str, Complex]]
38
44
 
45
+ BITSTRING = "bitstring"
46
+ PROBABILITY = "probability"
47
+ AMPLITUDE = "amplitude"
48
+ COUNT = "count"
49
+
39
50
  if TYPE_CHECKING:
40
51
  DotAccessParsedState = Mapping[Name, Any]
41
52
  else:
@@ -150,6 +161,36 @@ def prepare_parsed_state_vector(
150
161
  return sorted(parsed_state_vector, key=lambda k: abs(k.amplitude), reverse=True)
151
162
 
152
163
 
164
+ def _flatten_columns(df: pd.DataFrame, columns_to_flatten: list[str]) -> pd.DataFrame:
165
+ if len(df.columns) == 0:
166
+ return df
167
+
168
+ flattened_data = {}
169
+
170
+ for col in columns_to_flatten:
171
+ if col not in df.columns:
172
+ continue
173
+
174
+ flat_df = pd.json_normalize(df[col]).add_prefix(f"{col}.")
175
+ flat_df.index = df.index
176
+ flattened_data[col] = flat_df
177
+
178
+ new_df_columns = []
179
+ dfs_to_concat = []
180
+
181
+ for col_name in df.columns:
182
+ if col_name in flattened_data:
183
+ new_df_columns.extend(flattened_data[col_name].columns)
184
+ dfs_to_concat.append(flattened_data[col_name])
185
+ elif col_name not in columns_to_flatten:
186
+ new_df_columns.append(col_name)
187
+ dfs_to_concat.append(df[[col_name]])
188
+
189
+ final_df = pd.concat(dfs_to_concat, axis=1)
190
+ valid_final_columns = [c for c in new_df_columns if c in final_df.columns]
191
+ return final_df[valid_final_columns]
192
+
193
+
153
194
  class ExecutionDetails(BaseModel, QmodPyObject):
154
195
  vendor_format_result: dict[str, Any] = pydantic.Field(
155
196
  ..., description="Result in proprietary vendor format"
@@ -192,6 +233,8 @@ class ExecutionDetails(BaseModel, QmodPyObject):
192
233
  default=None, description="The total number of shots the circuit was executed"
193
234
  )
194
235
 
236
+ output_type_map: RegisterQuantumTypeDict = pydantic.Field(default_factory=dict)
237
+
195
238
  @pydantic.field_validator("counts", mode="after")
196
239
  @classmethod
197
240
  def _clean_spaces_from_counts_keys(cls, v: Counts) -> Counts:
@@ -302,6 +345,78 @@ class ExecutionDetails(BaseModel, QmodPyObject):
302
345
  )[::-1]
303
346
  return number_utils.binary_to_float_or_int(bin_rep=register_binary_string)
304
347
 
348
+ def _counts_df(self) -> pd.DataFrame:
349
+ data: dict[str, Any] = defaultdict(list)
350
+
351
+ for bitstring, count in self.counts.items():
352
+ data[BITSTRING].append(bitstring)
353
+ data[COUNT].append(count)
354
+ if self.probabilities:
355
+ data[PROBABILITY].append(self.probabilities[bitstring])
356
+ elif self.num_shots:
357
+ data[PROBABILITY].append(count / self.num_shots)
358
+
359
+ for name, value in self.parsed_states[bitstring].items():
360
+ data[name].append(value)
361
+
362
+ final_columns = [COUNT, PROBABILITY, BITSTRING]
363
+ columns = [
364
+ col for col in data.keys() if col not in final_columns
365
+ ] + final_columns
366
+
367
+ return pd.DataFrame(data, columns=columns)
368
+
369
+ def _state_vector_df(self) -> pd.DataFrame:
370
+ data: dict[str, Any] = defaultdict(list)
371
+
372
+ if not self.state_vector:
373
+ raise ClassiqInternalError("No state vector")
374
+ if not self.parsed_state_vector_states:
375
+ raise ClassiqInternalError("No parsed state vector states")
376
+
377
+ for bitstring, amplitude in self.state_vector.items():
378
+ data[BITSTRING].append(bitstring)
379
+ data[AMPLITUDE].append(amplitude)
380
+ data[PROBABILITY].append(abs(amplitude) ** 2)
381
+ for name, value in self.parsed_state_vector_states[bitstring].items():
382
+ data[name].append(value)
383
+
384
+ final_columns = [AMPLITUDE, PROBABILITY, BITSTRING]
385
+ columns = [
386
+ col for col in data.keys() if col not in final_columns
387
+ ] + final_columns
388
+
389
+ return pd.DataFrame(data, columns=columns)
390
+
391
+ @functools.cached_property
392
+ def dataframe(self) -> pd.DataFrame:
393
+ reserved_words = frozenset([BITSTRING, PROBABILITY, COUNT, AMPLITUDE])
394
+ _invalid_output_names = reserved_words.intersection(
395
+ self.output_qubits_map.keys()
396
+ )
397
+ if _invalid_output_names:
398
+ raise ClassiqValueError(f"Invalid output names: {_invalid_output_names}")
399
+
400
+ if self.state_vector:
401
+ df = self._state_vector_df()
402
+ else:
403
+ df = self._counts_df()
404
+
405
+ df.sort_values(
406
+ by=[AMPLITUDE if self.state_vector else COUNT, BITSTRING],
407
+ inplace=True,
408
+ ascending=[False, True],
409
+ ignore_index=True,
410
+ )
411
+
412
+ outputs_to_flatten = [
413
+ output
414
+ for output, type in self.output_type_map.items()
415
+ if type.quantum_types.kind == "struct_instance"
416
+ ]
417
+ df = _flatten_columns(df, outputs_to_flatten)
418
+ return df
419
+
305
420
 
306
421
  class MultipleExecutionDetails(VersionedModel):
307
422
  details: list[ExecutionDetails]