classiq 0.83.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 (103) 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/applications/combinatorial_optimization/combinatorial_problem.py +20 -42
  11. classiq/{model_expansions/evaluators → evaluators}/arg_type_match.py +12 -4
  12. classiq/{model_expansions/evaluators → evaluators}/argument_types.py +1 -1
  13. classiq/evaluators/classical_expression.py +53 -0
  14. classiq/{model_expansions/evaluators → evaluators}/classical_type_inference.py +3 -4
  15. classiq/{model_expansions/evaluators → evaluators}/parameter_types.py +17 -15
  16. classiq/execution/__init__.py +12 -1
  17. classiq/execution/execution_session.py +238 -49
  18. classiq/execution/jobs.py +26 -1
  19. classiq/execution/qnn.py +2 -2
  20. classiq/execution/user_budgets.py +39 -0
  21. classiq/interface/_version.py +1 -1
  22. classiq/interface/constants.py +1 -0
  23. classiq/interface/debug_info/debug_info.py +0 -4
  24. classiq/interface/execution/primitives.py +29 -1
  25. classiq/interface/executor/estimate_cost.py +35 -0
  26. classiq/interface/executor/execution_result.py +13 -0
  27. classiq/interface/executor/result.py +116 -1
  28. classiq/interface/executor/user_budget.py +26 -33
  29. classiq/interface/generator/expressions/atomic_expression_functions.py +10 -1
  30. classiq/interface/generator/expressions/proxies/classical/any_classical_value.py +0 -6
  31. classiq/interface/generator/functions/builtins/internal_operators.py +2 -0
  32. classiq/interface/generator/functions/classical_type.py +2 -35
  33. classiq/interface/generator/functions/concrete_types.py +20 -3
  34. classiq/interface/generator/functions/type_modifier.py +0 -19
  35. classiq/interface/generator/generated_circuit_data.py +5 -18
  36. classiq/interface/generator/types/compilation_metadata.py +0 -3
  37. classiq/interface/ide/operation_registry.py +45 -0
  38. classiq/interface/ide/visual_model.py +68 -3
  39. classiq/interface/model/bounds.py +12 -2
  40. classiq/interface/model/model.py +12 -7
  41. classiq/interface/model/port_declaration.py +2 -24
  42. classiq/interface/model/quantum_expressions/arithmetic_operation.py +7 -4
  43. classiq/interface/model/variable_declaration_statement.py +33 -6
  44. classiq/interface/pretty_print/__init__.py +0 -0
  45. classiq/{qmod/native → interface/pretty_print}/expression_to_qmod.py +18 -11
  46. classiq/interface/server/routes.py +4 -0
  47. classiq/model_expansions/atomic_expression_functions_defs.py +47 -6
  48. classiq/model_expansions/function_builder.py +4 -1
  49. classiq/model_expansions/interpreters/base_interpreter.py +3 -3
  50. classiq/model_expansions/interpreters/generative_interpreter.py +16 -1
  51. classiq/model_expansions/quantum_operations/allocate.py +1 -1
  52. classiq/model_expansions/quantum_operations/assignment_result_processor.py +64 -22
  53. classiq/model_expansions/quantum_operations/bind.py +2 -2
  54. classiq/model_expansions/quantum_operations/bounds.py +7 -1
  55. classiq/model_expansions/quantum_operations/call_emitter.py +26 -20
  56. classiq/model_expansions/quantum_operations/classical_var_emitter.py +16 -0
  57. classiq/model_expansions/quantum_operations/variable_decleration.py +31 -11
  58. classiq/model_expansions/scope.py +7 -0
  59. classiq/model_expansions/scope_initialization.py +3 -3
  60. classiq/model_expansions/transformers/model_renamer.py +6 -4
  61. classiq/model_expansions/transformers/type_modifier_inference.py +81 -43
  62. classiq/model_expansions/transformers/var_splitter.py +1 -1
  63. classiq/model_expansions/visitors/symbolic_param_inference.py +2 -3
  64. classiq/open_library/functions/__init__.py +3 -2
  65. classiq/open_library/functions/amplitude_amplification.py +10 -18
  66. classiq/open_library/functions/discrete_sine_cosine_transform.py +5 -5
  67. classiq/open_library/functions/grover.py +14 -6
  68. classiq/open_library/functions/modular_exponentiation.py +22 -20
  69. classiq/open_library/functions/qaoa_penalty.py +8 -1
  70. classiq/open_library/functions/state_preparation.py +18 -32
  71. classiq/qmod/__init__.py +2 -0
  72. classiq/qmod/builtins/enums.py +23 -0
  73. classiq/qmod/builtins/functions/__init__.py +2 -0
  74. classiq/qmod/builtins/functions/exponentiation.py +32 -4
  75. classiq/qmod/builtins/operations.py +65 -1
  76. classiq/qmod/builtins/structs.py +55 -3
  77. classiq/qmod/classical_variable.py +74 -0
  78. classiq/qmod/declaration_inferrer.py +3 -2
  79. classiq/qmod/native/pretty_printer.py +20 -20
  80. classiq/qmod/pretty_print/expression_to_python.py +2 -1
  81. classiq/qmod/pretty_print/pretty_printer.py +35 -21
  82. classiq/qmod/python_classical_type.py +12 -5
  83. classiq/qmod/qfunc.py +2 -19
  84. classiq/qmod/qmod_constant.py +2 -5
  85. classiq/qmod/qmod_parameter.py +2 -5
  86. classiq/qmod/qmod_variable.py +61 -23
  87. classiq/qmod/quantum_expandable.py +5 -3
  88. classiq/qmod/quantum_function.py +49 -4
  89. classiq/qmod/semantics/annotation/qstruct_annotator.py +1 -1
  90. classiq/qmod/semantics/validation/main_validation.py +1 -9
  91. classiq/qmod/symbolic_type.py +2 -1
  92. classiq/qmod/utilities.py +0 -2
  93. classiq/qmod/write_qmod.py +1 -1
  94. {classiq-0.83.0.dist-info → classiq-0.85.0.dist-info}/METADATA +4 -1
  95. {classiq-0.83.0.dist-info → classiq-0.85.0.dist-info}/RECORD +101 -90
  96. classiq/interface/model/quantum_variable_declaration.py +0 -7
  97. classiq/model_expansions/evaluators/classical_expression.py +0 -36
  98. /classiq/{model_expansions/evaluators → evaluators}/__init__.py +0 -0
  99. /classiq/{model_expansions/evaluators → evaluators}/control.py +0 -0
  100. /classiq/{model_expansions → evaluators}/expression_evaluator.py +0 -0
  101. /classiq/{model_expansions/evaluators → evaluators}/quantum_type_utils.py +0 -0
  102. /classiq/{model_expansions/evaluators → evaluators}/type_type_match.py +0 -0
  103. {classiq-0.83.0.dist-info → classiq-0.85.0.dist-info}/WHEEL +0 -0
@@ -600,3 +600,30 @@ class ApiWrapper:
600
600
  )
601
601
 
602
602
  return [UserBudget.model_validate(info) for info in data]
603
+
604
+ @classmethod
605
+ async def call_set_budget_limit(
606
+ cls, provider: str, budget_limit: float
607
+ ) -> UserBudget:
608
+ data = await client().call_api(
609
+ http_method=HTTPMethod.PATCH,
610
+ url=routes.USER_BUDGET_SET_LIMIT_FULL_PATH,
611
+ body={
612
+ "provider": provider,
613
+ "budget_limit": budget_limit,
614
+ },
615
+ )
616
+
617
+ return UserBudget.model_validate(data)
618
+
619
+ @classmethod
620
+ async def call_clear_budget_limit(cls, provider: str) -> UserBudget:
621
+ data = await client().call_api(
622
+ http_method=HTTPMethod.PATCH,
623
+ url=routes.USER_BUDGET_CLEAR_LIMIT_FULL_PATH,
624
+ body={
625
+ "provider": provider,
626
+ },
627
+ )
628
+
629
+ return UserBudget.model_validate(data)
@@ -254,7 +254,6 @@ def _summed_fermionic_operator_to_qmod_lader_terms(
254
254
  ) -> str:
255
255
  return "\t\t".join(
256
256
  [
257
- # fmt: off
258
257
  f"""
259
258
  struct_literal(LadderTerm,
260
259
  coefficient={fermionic_operator[1]},
@@ -263,7 +262,6 @@ def _summed_fermionic_operator_to_qmod_lader_terms(
263
262
  ]
264
263
  ),"""
265
264
  for fermionic_operator in hamiltonian.op_list
266
- # fmt: on
267
265
  ]
268
266
  )[:-1]
269
267
 
@@ -0,0 +1,68 @@
1
+ from collections.abc import Sequence
2
+
3
+ from openfermion.ops import FermionOperator
4
+ from openfermion.ops.operators.qubit_operator import QubitOperator
5
+
6
+ from classiq.applications.chemistry.mapping import FermionToQubitMapper
7
+ from classiq.applications.chemistry.problems import FermionHamiltonianProblem
8
+
9
+
10
+ def get_hf_fermion_op(problem: FermionHamiltonianProblem) -> FermionOperator:
11
+ """
12
+ Constructs a fermion operator that creates the Hartree-Fock reference state in
13
+ block-spin ordering.
14
+
15
+ Args:
16
+ problem (FermionHamiltonianProblem): The fermion problem. The Hartree-Fock
17
+ fermion operator depends only on the number of spatial orbitals and the
18
+ number of alpha and beta particles.
19
+
20
+ Returns:
21
+ The Hartree-Fock fermion operator.
22
+ """
23
+ return FermionOperator(" ".join(f"{i}^" for i in problem.occupied))
24
+
25
+
26
+ def get_hf_state(
27
+ problem: FermionHamiltonianProblem, mapper: FermionToQubitMapper
28
+ ) -> list[bool]:
29
+ """
30
+ Computes the qubits state after applying the Hartree-Fock operator defined by the
31
+ given problem and mapper.
32
+
33
+ The Qmod function `prepare_basis_state` can be used on the returned value to
34
+ allocate and initialize the qubits array.
35
+
36
+ Args:
37
+ problem (FermionHamiltonianProblem): The fermion problem.
38
+ mapper (FermionToQubitMapper): The mapper from fermion operator to qubits
39
+ operator.
40
+
41
+ Returns:
42
+ The qubits state, given as a list of boolean values for each qubit.
43
+ """
44
+ hf_qubit_op = _get_hf_qubit_op(problem, mapper)
45
+ num_qubits = mapper.get_num_qubits(problem)
46
+ if not hf_qubit_op.terms:
47
+ return [False] * num_qubits
48
+
49
+ # All terms map the zero state to the same basis state
50
+ first_term = next(iter(hf_qubit_op.terms.keys()))
51
+ return _apply_term_on_zero_state(first_term, num_qubits)
52
+
53
+
54
+ def _get_hf_qubit_op(
55
+ problem: FermionHamiltonianProblem, mapper: FermionToQubitMapper
56
+ ) -> QubitOperator:
57
+ hf_fermion_op = get_hf_fermion_op(problem)
58
+ return mapper.map(hf_fermion_op)
59
+
60
+
61
+ def _apply_term_on_zero_state(
62
+ term: Sequence[tuple[int, str]], num_qubits: int
63
+ ) -> list[bool]:
64
+ state = [False] * num_qubits
65
+ for qubit, pauli in term:
66
+ if pauli in ("X", "Y"):
67
+ state[qubit] = True
68
+ return state
@@ -0,0 +1,85 @@
1
+ from typing import Any, NoReturn
2
+
3
+ from openfermion.ops import FermionOperator, QubitOperator
4
+ from openfermion.transforms import (
5
+ bravyi_kitaev,
6
+ jordan_wigner,
7
+ )
8
+
9
+ from classiq.interface.enum_utils import StrEnum
10
+ from classiq.interface.exceptions import ClassiqValueError
11
+
12
+ from classiq.applications.chemistry.problems import FermionHamiltonianProblem
13
+
14
+
15
+ class MappingMethod(StrEnum):
16
+ """
17
+ Mapping methods from fermionic operators to qubits operators.
18
+ """
19
+
20
+ JORDAN_WIGNER = "jw"
21
+ BRAVYI_KITAEV = "bk"
22
+
23
+
24
+ class FermionToQubitMapper:
25
+ """
26
+ Mapper between fermionic operators to qubits operators, using one of the supported
27
+ mapping methods (see `MappingMethod`).
28
+
29
+ Attributes:
30
+ method (MappingMethod): The mapping method.
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ method: MappingMethod = MappingMethod.JORDAN_WIGNER,
36
+ ) -> None:
37
+ """
38
+ Initializes a `FermionToQubitMapper` object using the specified method.
39
+
40
+ Args:
41
+ method (MappingMethod): The mapping method.
42
+ """
43
+ self.method = method
44
+
45
+ if self.method is MappingMethod.JORDAN_WIGNER:
46
+ self._mapper = jordan_wigner
47
+ elif self.method is MappingMethod.BRAVYI_KITAEV:
48
+ self._mapper = bravyi_kitaev
49
+ else:
50
+ _raise_invalid_method(method)
51
+
52
+ def map(
53
+ self, fermion_op: FermionOperator, *args: Any, **kwargs: Any
54
+ ) -> QubitOperator:
55
+ """
56
+ Maps the given fermionic operator to a qubits operator using the mapper's
57
+ configuration.
58
+
59
+ Args:
60
+ fermion_op (FermionOperator): A fermionic operator.
61
+ *args: Extra parameters which are ignored, may be used in subclasses.
62
+ **kwargs: Extra parameters which are ignored, may be used in subclasses.
63
+
64
+ Returns:
65
+ The mapped qubits operator.
66
+ """
67
+ return self._mapper(fermion_op)
68
+
69
+ def get_num_qubits(self, problem: FermionHamiltonianProblem) -> int:
70
+ """
71
+ Gets the number of qubits after mapping the given problem into qubits space.
72
+
73
+ Args:
74
+ problem (FermionHamiltonianProblem): The fermion problem.
75
+
76
+ Returns:
77
+ The number of qubits.
78
+ """
79
+ return 2 * problem.n_orbitals
80
+
81
+
82
+ # statically validate that we have exhaustively searched all methods by defining its type
83
+ # as `NoReturn`, while dynamically raising an indicative error
84
+ def _raise_invalid_method(method: NoReturn) -> NoReturn:
85
+ raise ClassiqValueError(f"Invalid mapping method: {method}")
@@ -0,0 +1,79 @@
1
+ from typing import Optional, cast
2
+
3
+ import numpy as np
4
+ from openfermion.ops.operators.qubit_operator import QubitOperator
5
+ from openfermion.utils.operator_utils import count_qubits
6
+
7
+ from classiq.interface.exceptions import ClassiqValueError
8
+
9
+ from classiq.qmod.builtins.enums import Pauli
10
+ from classiq.qmod.builtins.structs import IndexedPauli, SparsePauliOp, SparsePauliTerm
11
+
12
+
13
+ def _get_n_qubits(qubit_op: QubitOperator, n_qubits: Optional[int]) -> int:
14
+ min_n_qubits = cast(int, count_qubits(qubit_op))
15
+ if n_qubits is None:
16
+ return min_n_qubits
17
+
18
+ if n_qubits < min_n_qubits:
19
+ raise ClassiqValueError(
20
+ f"The operator acts on {min_n_qubits} and cannot be cast to a PauliTerm on {n_qubits}"
21
+ )
22
+ return n_qubits
23
+
24
+
25
+ def qubit_op_to_pauli_terms(
26
+ qubit_op: QubitOperator, n_qubits: Optional[int] = None
27
+ ) -> SparsePauliOp:
28
+ n_qubits = _get_n_qubits(qubit_op, n_qubits)
29
+ return SparsePauliOp(
30
+ terms=[ # type:ignore[arg-type]
31
+ SparsePauliTerm(
32
+ paulis=[ # type:ignore[arg-type]
33
+ IndexedPauli(
34
+ pauli=getattr(Pauli, pauli),
35
+ index=n_qubits - qubit - 1,
36
+ )
37
+ for qubit, pauli in term[::-1]
38
+ ],
39
+ coefficient=coeff,
40
+ )
41
+ for term, coeff in qubit_op.terms.items()
42
+ ],
43
+ num_qubits=n_qubits, # type:ignore[arg-type]
44
+ )
45
+
46
+
47
+ _PAULIS_TO_XZ = {"I": (0, 0), "X": (1, 0), "Z": (0, 1), "Y": (1, 1)}
48
+ _XZ_TO_PAULIS = {(0, 0): "I", (1, 0): "X", (0, 1): "Z", (1, 1): "Y"}
49
+
50
+
51
+ def qubit_op_to_xz_matrix(
52
+ qubit_op: QubitOperator, n_qubits: Optional[int] = None
53
+ ) -> np.ndarray:
54
+ n_qubits = _get_n_qubits(qubit_op, n_qubits)
55
+ xz_mat = np.zeros((len(qubit_op.terms), 2 * n_qubits), dtype=np.int8)
56
+
57
+ for row, (term, _) in zip(xz_mat, qubit_op.terms.items()):
58
+ for qubit, pauli in term:
59
+ row[qubit], row[n_qubits + qubit] = _PAULIS_TO_XZ[pauli]
60
+
61
+ return xz_mat
62
+
63
+
64
+ def xz_matrix_to_qubit_op(xz_mat: np.ndarray) -> QubitOperator:
65
+ if len(xz_mat.shape) == 1:
66
+ xz_mat = np.array([xz_mat])
67
+
68
+ qubit_op = QubitOperator()
69
+ n_qubits = xz_mat.shape[1] // 2
70
+ for row in xz_mat:
71
+ op = tuple(
72
+ (qubit, pauli)
73
+ for qubit in range(n_qubits)
74
+ if (pauli := _XZ_TO_PAULIS[(row[qubit], row[n_qubits + qubit])]) != "I"
75
+ )
76
+ if op:
77
+ qubit_op += QubitOperator(op, 1)
78
+
79
+ return qubit_op
@@ -0,0 +1,195 @@
1
+ from collections.abc import Sequence
2
+ from typing import Optional, cast
3
+
4
+ from openfermion import MolecularData
5
+ from openfermion.ops import FermionOperator
6
+ from openfermion.transforms import (
7
+ get_fermion_operator,
8
+ reorder,
9
+ )
10
+ from openfermion.utils import count_qubits
11
+
12
+ from classiq.interface.exceptions import ClassiqValueError
13
+
14
+
15
+ class FermionHamiltonianProblem:
16
+ """
17
+ Defines an electronic-structure problem using a Fermionic operator and electron count.
18
+ Can also be constructed from a `MolecularData` object using the `from_molecule`
19
+ method.
20
+
21
+ Attributes:
22
+ fermion_hamiltonian (FermionOperator): The fermionic hamiltonian of the problem.
23
+ Assumed to be in the block-spin labeling.
24
+ n_orbitals (int): Number of spatial orbitlas.
25
+ n_alpha (int): Number of alpha particles.
26
+ n_beta (int): Number of beta particles.
27
+ n_particles (tuple[int, int]): Number of alpha and beta particles.
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ fermion_hamiltonian: FermionOperator,
33
+ n_particles: tuple[int, int],
34
+ n_orbitals: Optional[int] = None,
35
+ ) -> None:
36
+ """
37
+ Initializes a `FermionHamiltonianProblem` from the fermion hamiltonian, number
38
+ of alpha and beta particles, and optionally the number of orbitals.
39
+
40
+ Args:
41
+ fermion_hamiltonian (FermionHamiltonianProblem): The fermionic hamiltonian
42
+ of the problem. Assumed to be in the block-spin labeling.
43
+ n_particles (tuple[int, int]): Number of alpha and beta particles.
44
+ n_orbitals (int, optional): Number of spatial orbitals. If not specified,
45
+ the number is inferred from `fermion_hamiltonian`.
46
+ """
47
+ self.fermion_hamiltonian = fermion_hamiltonian
48
+ self.n_particles = n_particles
49
+ self.n_alpha, self.n_beta = n_particles
50
+
51
+ qubits = cast(int, count_qubits(fermion_hamiltonian))
52
+ min_n_orbitals = (qubits + 1) // 2
53
+ if n_orbitals is None:
54
+ self.n_orbitals = min_n_orbitals
55
+ else:
56
+ if n_orbitals < min_n_orbitals:
57
+ raise ClassiqValueError(
58
+ f"n_orbitals ({n_orbitals}) is less than the minimum number of orbitals {min_n_orbitals} inferred from the hamiltonian"
59
+ )
60
+ self.n_orbitals = n_orbitals
61
+
62
+ if self.n_alpha > self.n_orbitals:
63
+ raise ClassiqValueError(
64
+ f"n_alpha ({self.n_alpha}) exceeds available orbitals ({self.n_orbitals})"
65
+ )
66
+ if self.n_beta > self.n_orbitals:
67
+ raise ClassiqValueError(
68
+ f"n_beta ({self.n_beta}) exceeds available orbitals ({self.n_orbitals})"
69
+ )
70
+
71
+ @property
72
+ def occupied_alpha(self) -> list[int]:
73
+ """
74
+ Indices list of occupied alpha particles.
75
+ """
76
+ return list(range(self.n_alpha))
77
+
78
+ @property
79
+ def virtual_alpha(self) -> list[int]:
80
+ """
81
+ Indices list of virtual alpha particles.
82
+ """
83
+ return list(range(self.n_alpha, self.n_orbitals))
84
+
85
+ @property
86
+ def occupied_beta(self) -> list[int]:
87
+ """
88
+ Indices list of occupied beta particles.
89
+ """
90
+ return list(range(self.n_orbitals, self.n_orbitals + self.n_beta))
91
+
92
+ @property
93
+ def virtual_beta(self) -> list[int]:
94
+ """
95
+ Indices list of virtual beta particles.
96
+ """
97
+ return list(range(self.n_orbitals + self.n_beta, 2 * self.n_orbitals))
98
+
99
+ @property
100
+ def occupied(self) -> list[int]:
101
+ """
102
+ Indices list of occupied alpha and beta particles.
103
+ """
104
+ return self.occupied_alpha + self.occupied_beta
105
+
106
+ @property
107
+ def virtual(self) -> list[int]:
108
+ """
109
+ Indices list of virtual alpha and beta particles.
110
+ """
111
+ return self.virtual_alpha + self.virtual_beta
112
+
113
+ @classmethod
114
+ def from_molecule(
115
+ cls,
116
+ molecule: MolecularData,
117
+ first_active_index: int = 0,
118
+ remove_orbitlas: Optional[Sequence[int]] = None,
119
+ op_compression_tol: float = 1e-13,
120
+ ) -> "FermionHamiltonianProblem":
121
+ """
122
+ Constructs a `FermionHamiltonianProblem` from a molecule data.
123
+
124
+ Args:
125
+ molecule (MolecularData): The molecule data.
126
+ first_active_index (int): The first active index, indicates all prior
127
+ indices are freezed.
128
+ remove_orbitlas (Sequence[int], optional): Active indices to be removed.
129
+ op_compression_tol (float): Tolerance for trimming the fermion operator.
130
+
131
+ Returns:
132
+ The fermion hamiltonian problem.
133
+ """
134
+ if molecule.n_orbitals is None:
135
+ raise ClassiqValueError(
136
+ "The molecular data is not populated. Hint: call `run_pyscf` with the molecule."
137
+ )
138
+
139
+ if first_active_index >= molecule.n_orbitals:
140
+ raise ClassiqValueError(
141
+ f"Invalid active space: got first_active_index={first_active_index} "
142
+ f", while the number of orbitals is {molecule.n_orbitals}."
143
+ f" Active space must be non-empty."
144
+ )
145
+
146
+ freezed_indices = list(range(first_active_index))
147
+ active_indices = list(range(first_active_index, molecule.n_orbitals))
148
+ if remove_orbitlas:
149
+ active_indices = list(set(active_indices) - set(remove_orbitlas))
150
+
151
+ molecular_hamiltonian = molecule.get_molecular_hamiltonian(
152
+ occupied_indices=freezed_indices,
153
+ active_indices=active_indices,
154
+ )
155
+
156
+ n_freezed_orbitals = len(freezed_indices)
157
+ n_orbitals = len(active_indices)
158
+ n_alpha, n_beta = (
159
+ molecule.get_n_alpha_electrons(),
160
+ molecule.get_n_beta_electrons(),
161
+ )
162
+ n_particles = (n_alpha - n_freezed_orbitals, n_beta - n_freezed_orbitals)
163
+
164
+ if n_orbitals <= 0 or min(n_particles) <= 0:
165
+ raise ClassiqValueError(
166
+ f"Degenerate active space: got {n_orbitals} spatial orbitals "
167
+ f"and {n_particles} electrons. "
168
+ f"This can happen if too many orbitals were frozen."
169
+ f"Before freezing number of particle was ({n_alpha, n_beta})."
170
+ f"Consider adjusting `first_active_index` or `remove_orbitlas` "
171
+ f"to ensure the active space is non-empty."
172
+ )
173
+
174
+ fermion_op = get_fermion_operator(molecular_hamiltonian)
175
+ # openfermion returns the operation in alternating-spin labeling, reorder to
176
+ # keep the convention of block-spin labeling.
177
+ fermion_op = _reorder_op_alternating_to_block(fermion_op)
178
+ fermion_op.compress(abs_tol=op_compression_tol)
179
+
180
+ return cls(
181
+ fermion_hamiltonian=fermion_op,
182
+ n_particles=n_particles,
183
+ n_orbitals=n_orbitals,
184
+ )
185
+
186
+
187
+ def _reorder_op_alternating_to_block(op: FermionOperator) -> FermionOperator:
188
+ def _alternating_to_block(idx: int, num_modes: int) -> int:
189
+ """Map an alternating-spin mode index to block-spin order."""
190
+ n = num_modes // 2
191
+ spin = idx % 2 # 0 = alpha, 1 = beta
192
+ orbital = idx // 2
193
+ return orbital + spin * n
194
+
195
+ return reorder(op, _alternating_to_block)
@@ -0,0 +1,109 @@
1
+ from collections.abc import Sequence
2
+ from itertools import chain, combinations, product
3
+ from math import factorial
4
+ from typing import Union
5
+
6
+ from openfermion.ops.operators.fermion_operator import FermionOperator
7
+ from openfermion.ops.operators.qubit_operator import QubitOperator
8
+
9
+ from classiq.applications.chemistry.mapping import FermionToQubitMapper
10
+ from classiq.applications.chemistry.op_utils import qubit_op_to_pauli_terms
11
+ from classiq.applications.chemistry.problems import FermionHamiltonianProblem
12
+ from classiq.qmod.builtins.structs import (
13
+ SparsePauliOp,
14
+ )
15
+
16
+
17
+ def get_ucc_hamiltonians(
18
+ problem: FermionHamiltonianProblem,
19
+ mapper: FermionToQubitMapper,
20
+ excitations: Union[int, Sequence[int]],
21
+ ) -> list[SparsePauliOp]:
22
+ """
23
+ Computes the UCC hamiltonians of the given problem in the desired excitations,
24
+ using the given mapper.
25
+
26
+ Args:
27
+ problem (FermionHamiltonianProblem): The fermion problem.
28
+ mapper (FermionToQubitMapper): The mapper from fermion to qubits operators.
29
+ excitations (int, Sequence[int]): A single desired excitation or an excitations
30
+ list.
31
+
32
+ Returns:
33
+ The UCC hamiltonians.
34
+ """
35
+ if isinstance(excitations, int):
36
+ excitations = [excitations]
37
+
38
+ f_ops = (
39
+ _hamiltonian_from_excitations(
40
+ source, target, 1 / factorial(num_excitations) * 1j
41
+ )
42
+ for num_excitations in excitations
43
+ for source, target in get_excitations(problem, num_excitations)
44
+ )
45
+
46
+ n_qubits = mapper.get_num_qubits(problem)
47
+ return [
48
+ qubit_op_to_pauli_terms(q_op, n_qubits)
49
+ for f_op in f_ops
50
+ if (q_op := mapper.map(f_op))
51
+ not in (
52
+ QubitOperator(),
53
+ QubitOperator((), q_op.constant),
54
+ )
55
+ ]
56
+
57
+
58
+ def _hamiltonian_from_excitations(
59
+ source: tuple[int, ...], target: tuple[int, ...], coeff: complex
60
+ ) -> FermionOperator:
61
+ op_string = " ".join(
62
+ chain(
63
+ (f"{i}^" for i in source),
64
+ (f"{i}" for i in target),
65
+ )
66
+ )
67
+ dagger_op_string = " ".join(
68
+ chain(
69
+ (f"{i}^" for i in reversed(target)),
70
+ (f"{i}" for i in reversed(source)),
71
+ )
72
+ )
73
+ return FermionOperator(op_string, coeff) - FermionOperator(dagger_op_string, coeff)
74
+
75
+
76
+ def get_excitations(
77
+ problem: FermionHamiltonianProblem, num_excitations: int
78
+ ) -> set[tuple[tuple[int, ...], tuple[int, ...]]]:
79
+ """
80
+ Gets all the possible excitations of the given problem according to the
81
+ given number of excitations, preserving the particles spin.
82
+
83
+ Args:
84
+ problem (FermionHamiltonianProblem): The fermion problem.
85
+ num_excitations (int): Number of excitations.
86
+
87
+ Returns:
88
+ A set of all possible excitations, specified as a pair of source and target indices.
89
+ """
90
+ if num_excitations <= 0:
91
+ return set()
92
+
93
+ possible_excitations = chain(
94
+ product(problem.occupied_alpha, problem.virtual_alpha),
95
+ product(problem.occupied_beta, problem.virtual_beta),
96
+ )
97
+ single_excitations = combinations(possible_excitations, r=num_excitations)
98
+
99
+ excitations: set[tuple[tuple[int, ...], tuple[int, ...]]] = set()
100
+ for excitation in single_excitations:
101
+ # the zip converts a sequence of single excitations (e.g. [(0, 1), (2, 3), (4, 5)])
102
+ # to the sequence of combined excitations (e.g. [(0, 2, 4), (1, 3, 5)])
103
+ source, target = (set(gr) for gr in zip(*excitation))
104
+
105
+ # filter out excitations with repetitions in source/target
106
+ if len(source) == num_excitations and len(target) == num_excitations:
107
+ excitations.add((tuple(source), tuple(target)))
108
+
109
+ return excitations