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.
- classiq/_internals/api_wrapper.py +27 -0
- classiq/applications/chemistry/chemistry_model_constructor.py +0 -2
- classiq/applications/chemistry/hartree_fock.py +68 -0
- classiq/applications/chemistry/mapping.py +85 -0
- classiq/applications/chemistry/op_utils.py +79 -0
- classiq/applications/chemistry/problems.py +195 -0
- classiq/applications/chemistry/ucc.py +109 -0
- classiq/applications/chemistry/z2_symmetries.py +368 -0
- classiq/applications/combinatorial_helpers/pauli_helpers/pauli_utils.py +30 -1
- classiq/applications/combinatorial_optimization/combinatorial_problem.py +20 -42
- classiq/{model_expansions/evaluators → evaluators}/arg_type_match.py +12 -4
- classiq/{model_expansions/evaluators → evaluators}/argument_types.py +1 -1
- classiq/evaluators/classical_expression.py +53 -0
- classiq/{model_expansions/evaluators → evaluators}/classical_type_inference.py +3 -4
- classiq/{model_expansions/evaluators → evaluators}/parameter_types.py +17 -15
- classiq/execution/__init__.py +12 -1
- classiq/execution/execution_session.py +238 -49
- classiq/execution/jobs.py +26 -1
- classiq/execution/qnn.py +2 -2
- classiq/execution/user_budgets.py +39 -0
- classiq/interface/_version.py +1 -1
- classiq/interface/constants.py +1 -0
- classiq/interface/debug_info/debug_info.py +0 -4
- classiq/interface/execution/primitives.py +29 -1
- classiq/interface/executor/estimate_cost.py +35 -0
- classiq/interface/executor/execution_result.py +13 -0
- classiq/interface/executor/result.py +116 -1
- classiq/interface/executor/user_budget.py +26 -33
- classiq/interface/generator/expressions/atomic_expression_functions.py +10 -1
- classiq/interface/generator/expressions/proxies/classical/any_classical_value.py +0 -6
- classiq/interface/generator/functions/builtins/internal_operators.py +2 -0
- classiq/interface/generator/functions/classical_type.py +2 -35
- classiq/interface/generator/functions/concrete_types.py +20 -3
- classiq/interface/generator/functions/type_modifier.py +0 -19
- classiq/interface/generator/generated_circuit_data.py +5 -18
- classiq/interface/generator/types/compilation_metadata.py +0 -3
- classiq/interface/ide/operation_registry.py +45 -0
- classiq/interface/ide/visual_model.py +68 -3
- classiq/interface/model/bounds.py +12 -2
- classiq/interface/model/model.py +12 -7
- classiq/interface/model/port_declaration.py +2 -24
- classiq/interface/model/quantum_expressions/arithmetic_operation.py +7 -4
- classiq/interface/model/variable_declaration_statement.py +33 -6
- classiq/interface/pretty_print/__init__.py +0 -0
- classiq/{qmod/native → interface/pretty_print}/expression_to_qmod.py +18 -11
- classiq/interface/server/routes.py +4 -0
- classiq/model_expansions/atomic_expression_functions_defs.py +47 -6
- classiq/model_expansions/function_builder.py +4 -1
- classiq/model_expansions/interpreters/base_interpreter.py +3 -3
- classiq/model_expansions/interpreters/generative_interpreter.py +16 -1
- classiq/model_expansions/quantum_operations/allocate.py +1 -1
- classiq/model_expansions/quantum_operations/assignment_result_processor.py +64 -22
- classiq/model_expansions/quantum_operations/bind.py +2 -2
- classiq/model_expansions/quantum_operations/bounds.py +7 -1
- classiq/model_expansions/quantum_operations/call_emitter.py +26 -20
- classiq/model_expansions/quantum_operations/classical_var_emitter.py +16 -0
- classiq/model_expansions/quantum_operations/variable_decleration.py +31 -11
- classiq/model_expansions/scope.py +7 -0
- classiq/model_expansions/scope_initialization.py +3 -3
- classiq/model_expansions/transformers/model_renamer.py +6 -4
- classiq/model_expansions/transformers/type_modifier_inference.py +81 -43
- classiq/model_expansions/transformers/var_splitter.py +1 -1
- classiq/model_expansions/visitors/symbolic_param_inference.py +2 -3
- classiq/open_library/functions/__init__.py +3 -2
- classiq/open_library/functions/amplitude_amplification.py +10 -18
- classiq/open_library/functions/discrete_sine_cosine_transform.py +5 -5
- classiq/open_library/functions/grover.py +14 -6
- classiq/open_library/functions/modular_exponentiation.py +22 -20
- classiq/open_library/functions/qaoa_penalty.py +8 -1
- classiq/open_library/functions/state_preparation.py +18 -32
- classiq/qmod/__init__.py +2 -0
- classiq/qmod/builtins/enums.py +23 -0
- classiq/qmod/builtins/functions/__init__.py +2 -0
- classiq/qmod/builtins/functions/exponentiation.py +32 -4
- classiq/qmod/builtins/operations.py +65 -1
- classiq/qmod/builtins/structs.py +55 -3
- classiq/qmod/classical_variable.py +74 -0
- classiq/qmod/declaration_inferrer.py +3 -2
- classiq/qmod/native/pretty_printer.py +20 -20
- classiq/qmod/pretty_print/expression_to_python.py +2 -1
- classiq/qmod/pretty_print/pretty_printer.py +35 -21
- classiq/qmod/python_classical_type.py +12 -5
- classiq/qmod/qfunc.py +2 -19
- classiq/qmod/qmod_constant.py +2 -5
- classiq/qmod/qmod_parameter.py +2 -5
- classiq/qmod/qmod_variable.py +61 -23
- classiq/qmod/quantum_expandable.py +5 -3
- classiq/qmod/quantum_function.py +49 -4
- classiq/qmod/semantics/annotation/qstruct_annotator.py +1 -1
- classiq/qmod/semantics/validation/main_validation.py +1 -9
- classiq/qmod/symbolic_type.py +2 -1
- classiq/qmod/utilities.py +0 -2
- classiq/qmod/write_qmod.py +1 -1
- {classiq-0.83.0.dist-info → classiq-0.85.0.dist-info}/METADATA +4 -1
- {classiq-0.83.0.dist-info → classiq-0.85.0.dist-info}/RECORD +101 -90
- classiq/interface/model/quantum_variable_declaration.py +0 -7
- classiq/model_expansions/evaluators/classical_expression.py +0 -36
- /classiq/{model_expansions/evaluators → evaluators}/__init__.py +0 -0
- /classiq/{model_expansions/evaluators → evaluators}/control.py +0 -0
- /classiq/{model_expansions → evaluators}/expression_evaluator.py +0 -0
- /classiq/{model_expansions/evaluators → evaluators}/quantum_type_utils.py +0 -0
- /classiq/{model_expansions/evaluators → evaluators}/type_type_match.py +0 -0
- {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
|