amplify-quantum 1.0.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 (48) hide show
  1. amplify_quantum/__init__.py +92 -0
  2. amplify_quantum/__version__.py +24 -0
  3. amplify_quantum/algo/__init__.py +4 -0
  4. amplify_quantum/algo/base.py +76 -0
  5. amplify_quantum/algo/qaoa/__init__.py +42 -0
  6. amplify_quantum/algo/qaoa/components.py +397 -0
  7. amplify_quantum/algo/qaoa/impls/__init__.py +10 -0
  8. amplify_quantum/algo/qaoa/impls/qaoa_auto.py +40 -0
  9. amplify_quantum/algo/qaoa/impls/qaoa_n_hot.py +45 -0
  10. amplify_quantum/algo/qaoa/impls/qaoa_original.py +45 -0
  11. amplify_quantum/algo/qaoa/interface.py +213 -0
  12. amplify_quantum/algo/qaoa/run.py +216 -0
  13. amplify_quantum/algo/qaoa/type.py +282 -0
  14. amplify_quantum/algo/rqaoa/__init__.py +23 -0
  15. amplify_quantum/algo/rqaoa/interface.py +224 -0
  16. amplify_quantum/algo/rqaoa/run.py +314 -0
  17. amplify_quantum/algo/rqaoa/type.py +116 -0
  18. amplify_quantum/algo/utility.py +159 -0
  19. amplify_quantum/circuit/__init__.py +4 -0
  20. amplify_quantum/circuit/base.py +127 -0
  21. amplify_quantum/circuit/qiskit.py +235 -0
  22. amplify_quantum/circuit/qulacs.py +227 -0
  23. amplify_quantum/client/__init__.py +4 -0
  24. amplify_quantum/client/aer.py +213 -0
  25. amplify_quantum/client/aqt.py +132 -0
  26. amplify_quantum/client/base.py +160 -0
  27. amplify_quantum/client/braketsimulator.py +144 -0
  28. amplify_quantum/client/ibm.py +188 -0
  29. amplify_quantum/client/ionq.py +133 -0
  30. amplify_quantum/client/iqm.py +133 -0
  31. amplify_quantum/client/qulacs.py +45 -0
  32. amplify_quantum/client/rigetti.py +133 -0
  33. amplify_quantum/minimize/__init__.py +4 -0
  34. amplify_quantum/minimize/base.py +57 -0
  35. amplify_quantum/minimize/no_op.py +80 -0
  36. amplify_quantum/minimize/scipy.py +168 -0
  37. amplify_quantum/sampler/__init__.py +4 -0
  38. amplify_quantum/sampler/base.py +92 -0
  39. amplify_quantum/sampler/braket.py +560 -0
  40. amplify_quantum/sampler/qiskit.py +652 -0
  41. amplify_quantum/sampler/qulacs.py +119 -0
  42. amplify_quantum/utils/__init__.py +4 -0
  43. amplify_quantum/utils/braket.py +110 -0
  44. amplify_quantum/utils/qiskit.py +48 -0
  45. amplify_quantum-1.0.0.dist-info/METADATA +45 -0
  46. amplify_quantum-1.0.0.dist-info/RECORD +48 -0
  47. amplify_quantum-1.0.0.dist-info/WHEEL +4 -0
  48. amplify_quantum-1.0.0.dist-info/licenses/LICENSE.txt +203 -0
@@ -0,0 +1,92 @@
1
+ # Copyright (c) Fixstars Corporation and Fixstars Amplify Corporation.
2
+ #
3
+ # This source code is licensed under the Apache2.0 license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+ try:
6
+ # ruff: disable[F401]
7
+ import qiskit
8
+ import qiskit_aer
9
+ import qiskit_ibm_runtime
10
+ import qulacs
11
+ import scipy
12
+ import typing_extensions
13
+ # ruff: enable[F401]
14
+ except ImportError:
15
+ pass
16
+ else:
17
+ from .algo.base import QuantumAlgoProtocol
18
+ from .algo.qaoa import (
19
+ QAOA,
20
+ QAOADurations,
21
+ QAOAHistoryItem,
22
+ QAOAResult,
23
+ QAOAType,
24
+ )
25
+ from .algo.rqaoa import (
26
+ RQAOA,
27
+ NormalElimination,
28
+ RQAOADurations,
29
+ RQAOAHistoryItem,
30
+ RQAOAResult,
31
+ RQAOAType,
32
+ UnintentionalElimination,
33
+ )
34
+ from .circuit.qiskit import QiskitCircuit
35
+ from .circuit.qulacs import QulacsCircuit
36
+ from .client.aer import AerClient
37
+ from .client.aqt import AQTClient
38
+ from .client.base import QuantumBaseClient
39
+ from .client.braketsimulator import BraketSimulatorClient
40
+ from .client.ibm import IBMClient
41
+ from .client.ionq import IonQClient
42
+ from .client.iqm import IQMClient
43
+ from .client.qulacs import QulacsClient
44
+ from .client.rigetti import RigettiClient
45
+ from .minimize.base import MinimizeProtocol, MinimizeResult
46
+ from .minimize.no_op import NoOpMinimize, NoOpMinimizeResult
47
+ from .minimize.scipy import ScipyMinimize, ScipyMinimizeOptions, ScipyMinimizeResult
48
+ from .sampler.base import IsingSeqFreqList, SamplerProtocol, SamplingDurations
49
+ from .sampler.braket import BraketJobMeta
50
+ from .sampler.qiskit import AerDeviceType, QiskitJobMeta
51
+ from .sampler.qulacs import QulacsJobMeta
52
+
53
+ __all__ = [
54
+ "QAOA",
55
+ "RQAOA",
56
+ "AQTClient",
57
+ "AerClient",
58
+ "AerDeviceType",
59
+ "BraketJobMeta",
60
+ "BraketSimulatorClient",
61
+ "IBMClient",
62
+ "IQMClient",
63
+ "IonQClient",
64
+ "IsingSeqFreqList",
65
+ "MinimizeProtocol",
66
+ "MinimizeResult",
67
+ "NoOpMinimize",
68
+ "NoOpMinimizeResult",
69
+ "NormalElimination",
70
+ "QAOADurations",
71
+ "QAOAHistoryItem",
72
+ "QAOAResult",
73
+ "QAOAType",
74
+ "QiskitCircuit",
75
+ "QiskitJobMeta",
76
+ "QuantumAlgoProtocol",
77
+ "QuantumBaseClient",
78
+ "QulacsCircuit",
79
+ "QulacsClient",
80
+ "QulacsJobMeta",
81
+ "RQAOADurations",
82
+ "RQAOAHistoryItem",
83
+ "RQAOAResult",
84
+ "RQAOAType",
85
+ "RigettiClient",
86
+ "SamplerProtocol",
87
+ "SamplingDurations",
88
+ "ScipyMinimize",
89
+ "ScipyMinimizeOptions",
90
+ "ScipyMinimizeResult",
91
+ "UnintentionalElimination",
92
+ ]
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '1.0.0'
22
+ __version_tuple__ = version_tuple = (1, 0, 0)
23
+
24
+ __commit_id__ = commit_id = None
@@ -0,0 +1,4 @@
1
+ # Copyright (c) Fixstars Corporation and Fixstars Amplify Corporation.
2
+ #
3
+ # This source code is licensed under the Apache2.0 license found in the
4
+ # LICENSE file in the root directory of this source tree.
@@ -0,0 +1,76 @@
1
+ # Copyright (c) Fixstars Corporation and Fixstars Amplify Corporation.
2
+ #
3
+ # This source code is licensed under the Apache2.0 license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import TYPE_CHECKING, Any, Final, Literal, Protocol, overload
9
+
10
+ if TYPE_CHECKING:
11
+ from collections.abc import Callable
12
+
13
+ from amplify import AcceptableDegrees, CustomClientResultProtocol, Model
14
+
15
+ from amplify_quantum.sampler.base import SamplerProtocol, SamplingMeta_co
16
+
17
+
18
+ class QuantumAlgoProtocol(Protocol):
19
+ """Protocol for quantum optimization algorithms.
20
+
21
+ Implement this protocol to define a custom algorithm compatible with any
22
+ :class:`~amplify.QuantumBaseClient` subclass. A conforming class
23
+ must declare :attr:`acceptable_degrees`, a :attr:`Parameters` class, and a
24
+ static :meth:`run` method.
25
+ """
26
+
27
+ Parameters: Final[type]
28
+ """Configuration class for the algorithm. Instances are passed to :meth:`run`
29
+ and to :attr:`acceptable_degrees` when it is a callable."""
30
+
31
+ acceptable_degrees: Final[
32
+ AcceptableDegrees
33
+ | Callable[
34
+ [
35
+ Any # QuantumAlgoProtocol.Parameters
36
+ ],
37
+ AcceptableDegrees,
38
+ ]
39
+ ]
40
+ """Polynomial degrees accepted by the algorithm's objective function.
41
+ Either a static :class:`~amplify.AcceptableDegrees` value, or a callable
42
+ that takes a :attr:`Parameters` instance and returns one."""
43
+
44
+ @overload
45
+ @staticmethod
46
+ def run(
47
+ sampler: SamplerProtocol[SamplingMeta_co, Any],
48
+ model: Model,
49
+ parameters: Any, # noqa: ANN401
50
+ dry_run: Literal[False],
51
+ ) -> CustomClientResultProtocol: ...
52
+ @overload
53
+ @staticmethod
54
+ def run(
55
+ sampler: SamplerProtocol[SamplingMeta_co, Any],
56
+ model: Model,
57
+ parameters: Any, # noqa: ANN401
58
+ dry_run: Literal[True],
59
+ ) -> None: ...
60
+
61
+ @staticmethod
62
+ def run(
63
+ sampler: SamplerProtocol[SamplingMeta_co, Any], model: Model, parameters: Any, dry_run: bool = False
64
+ ) -> CustomClientResultProtocol | None:
65
+ """Run the algorithm on the given model.
66
+
67
+ Args:
68
+ sampler (SamplerProtocol[SamplingMeta_co, Any]): Backend sampler that executes quantum circuits.
69
+ model (Model): The optimization model to solve.
70
+ parameters (Any): Algorithm-specific configuration (e.g., :class:`QAOA.Parameters`).
71
+ dry_run (bool): If ``True``, validate inputs without executing circuits and return ``None``.
72
+
73
+ Returns:
74
+ CustomClientResultProtocol | None: The algorithm result, or ``None`` if *dry_run* is ``True``.
75
+ """
76
+ ...
@@ -0,0 +1,42 @@
1
+ # Copyright (c) Fixstars Corporation and Fixstars Amplify Corporation.
2
+ #
3
+ # This source code is licensed under the Apache2.0 license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+
6
+
7
+ from .components import (
8
+ CostPhaseSeparator,
9
+ IdentityPhaseSeparator,
10
+ UnconstrainedQAOAProblem,
11
+ UniformSuperpositionInitialState,
12
+ XMixer,
13
+ )
14
+ from .interface import QAOA, QAOAType
15
+ from .type import (
16
+ InitialStateProtocol,
17
+ MixerProtocol,
18
+ PhaseSeparatorProtocol,
19
+ QAOADurations,
20
+ QAOAHistoryItem,
21
+ QAOAImplProtocol,
22
+ QAOAProblemProtocol,
23
+ QAOAResult,
24
+ )
25
+
26
+ __all__ = [
27
+ "QAOA",
28
+ "CostPhaseSeparator",
29
+ "IdentityPhaseSeparator",
30
+ "InitialStateProtocol",
31
+ "MixerProtocol",
32
+ "PhaseSeparatorProtocol",
33
+ "QAOADurations",
34
+ "QAOAHistoryItem",
35
+ "QAOAImplProtocol",
36
+ "QAOAProblemProtocol",
37
+ "QAOAResult",
38
+ "QAOAType",
39
+ "UnconstrainedQAOAProblem",
40
+ "UniformSuperpositionInitialState",
41
+ "XMixer",
42
+ ]
@@ -0,0 +1,397 @@
1
+ # Copyright (c) Fixstars Corporation and Fixstars Amplify Corporation.
2
+ #
3
+ # This source code is licensed under the Apache2.0 license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+
6
+ from __future__ import annotations
7
+
8
+ from itertools import islice
9
+ from typing import TYPE_CHECKING
10
+
11
+ from amplify import Degree, Matrix, Model, VariableType
12
+
13
+ from amplify_quantum.algo.utility import (
14
+ gather_disjoint_n_hot_greedy,
15
+ is_satisfied_group_constraints,
16
+ raise_subclass_error,
17
+ raise_type_error,
18
+ validate_poly,
19
+ )
20
+ from amplify_quantum.circuit.base import SupportsAnsatz, SupportsAnsatzObservable, SupportsCAnsatz, SupportsHam
21
+
22
+ if TYPE_CHECKING:
23
+ from collections.abc import Iterable, Iterator, Sequence
24
+
25
+ from amplify import Poly
26
+
27
+ from amplify_quantum.algo.qaoa.type import QAOAProblemProtocol
28
+ from amplify_quantum.circuit.base import CircuitProtocol
29
+
30
+
31
+ def _generate_x_observable(
32
+ obs_class: type[SupportsAnsatzObservable], num_qubits: int, active_qubits: Iterable[int]
33
+ ) -> SupportsAnsatzObservable:
34
+ observable = obs_class(num_qubits)
35
+ for qubit in active_qubits:
36
+ observable.add_pauli_x(num_qubits, qubit, 1.0)
37
+ return observable
38
+
39
+
40
+ def _apply_x_rotation_to_qubits(circuit: SupportsAnsatz, num_qubits: int, qubits: Iterable[int], angle: float) -> None:
41
+ qubit_list = list(qubits)
42
+ if not qubit_list:
43
+ return
44
+
45
+ observable_class = type(circuit).get_observable_class()
46
+ observable = _generate_x_observable(observable_class, num_qubits, qubit_list)
47
+ circuit.add_observable_rotation_gate(observable, angle, num_qubits)
48
+
49
+
50
+ def _generate_hamiltonian(obs_class: type[SupportsHam], ising_poly: Poly, num_qubits: int) -> SupportsHam:
51
+ hamiltonian = obs_class(num_qubits)
52
+ for key, value in ising_poly:
53
+ if key:
54
+ hamiltonian.add_pauli_z(num_qubits, (i.id for i in key), value)
55
+ return hamiltonian
56
+
57
+
58
+ class UniformSuperpositionInitialState:
59
+ """Initial state that places all active qubits in an equal superposition.
60
+
61
+ Applies a Hadamard gate to each qubit listed in ``active_qubits``.
62
+ Requires no variational parameters.
63
+ """
64
+
65
+ def num_parameters(self, problem: QAOAProblemProtocol) -> int:
66
+ """Return 0 — this component takes no variational parameters."""
67
+ _ = problem
68
+ return 0
69
+
70
+ def apply(self, circuit: CircuitProtocol, problem: QAOAProblemProtocol, parameters: Iterator[float]) -> None:
71
+ """Apply a Hadamard gate to each active qubit.
72
+
73
+ Args:
74
+ circuit (CircuitProtocol): The circuit to modify in place.
75
+ problem (QAOAProblemProtocol): Provides ``active_qubits``.
76
+ parameters (Iterator[float]): Unused; no parameters are consumed.
77
+ """
78
+ _ = parameters
79
+ if not isinstance(circuit, SupportsAnsatz):
80
+ raise_type_error("'circuit'", "SupportsAnsatz", circuit)
81
+
82
+ for qubit in problem.active_qubits:
83
+ circuit.add_h_gate(qubit)
84
+
85
+
86
+ class CostPhaseSeparator:
87
+ """Phase separator that applies a rotation generated by the cost Hamiltonian.
88
+
89
+ Consumes one variational parameter per layer (the phase angle *gamma*).
90
+ """
91
+
92
+ def num_parameters_per_layer(self, problem: QAOAProblemProtocol, layer_index: int) -> int:
93
+ """Return 1 — one phase angle per layer."""
94
+ _ = problem
95
+ _ = layer_index
96
+ return 1
97
+
98
+ def apply_layer(
99
+ self, circuit: CircuitProtocol, problem: QAOAProblemProtocol, parameters: Iterator[float], layer_index: int
100
+ ) -> None:
101
+ """Apply a cost Hamiltonian rotation gate to *circuit*.
102
+
103
+ Args:
104
+ circuit (CircuitProtocol): The circuit to modify in place.
105
+ problem (QAOAProblemProtocol): Provides the Ising polynomial for the cost Hamiltonian.
106
+ parameters (Iterator[float]): Supplies the phase angle *gamma*.
107
+ layer_index (int): Zero-based index of the current QAOA layer (unused).
108
+ """
109
+ _ = layer_index
110
+ p = next(parameters, None)
111
+ if p is None:
112
+ raise ValueError("CostPhaseSeparator expects exactly one parameter per layer.")
113
+
114
+ if not isinstance(circuit, SupportsAnsatz):
115
+ raise_type_error("'circuit'", "SupportsAnsatz", circuit)
116
+
117
+ observable_class = type(circuit).get_observable_class()
118
+
119
+ if not issubclass(observable_class, SupportsHam): # pyright: ignore[reportUnnecessaryIsInstance]
120
+ raise_subclass_error("observable class of 'circuit'", "SupportsHam", observable_class)
121
+ if not issubclass(observable_class, SupportsAnsatzObservable): # pyright: ignore[reportUnnecessaryIsInstance]
122
+ raise_subclass_error("observable class of 'circuit'", "SupportsAnsatzObservable", observable_class)
123
+
124
+ hamiltonian = _generate_hamiltonian(observable_class, problem.ising_poly, problem.num_qubits)
125
+
126
+ if not isinstance(hamiltonian, SupportsAnsatzObservable):
127
+ raise_type_error("observable class of 'circuit'", "SupportsAnsatzObservable", circuit)
128
+
129
+ circuit.add_observable_rotation_gate(hamiltonian, p, problem.num_qubits)
130
+
131
+
132
+ class IdentityPhaseSeparator:
133
+ """Phase separator that applies no gates (identity operation).
134
+
135
+ Used when the phase-separation step is handled externally or should be skipped.
136
+ Requires no variational parameters.
137
+ """
138
+
139
+ def num_parameters_per_layer(self, problem: QAOAProblemProtocol, layer_index: int) -> int:
140
+ """Return 0 — this component takes no variational parameters."""
141
+ _ = problem
142
+ _ = layer_index
143
+ return 0
144
+
145
+ def apply_layer(
146
+ self, circuit: CircuitProtocol, problem: QAOAProblemProtocol, parameters: Iterator[float], layer_index: int
147
+ ) -> None:
148
+ """Do nothing — identity operation.
149
+
150
+ Args:
151
+ circuit (CircuitProtocol): Unused.
152
+ problem (QAOAProblemProtocol): Unused.
153
+ parameters (Iterator[float]): Unused; no parameters are consumed.
154
+ layer_index (int): Unused.
155
+ """
156
+ _ = circuit
157
+ _ = problem
158
+ _ = layer_index
159
+ _ = parameters
160
+
161
+
162
+ class XMixer:
163
+ """Mixer that applies a transverse-field (X) rotation to all active qubits.
164
+
165
+ Consumes one variational parameter per layer (the mixing angle *beta*).
166
+ """
167
+
168
+ def num_parameters_per_layer(self, problem: QAOAProblemProtocol, layer_index: int) -> int:
169
+ """Return 1 — one mixing angle per layer."""
170
+ _ = problem
171
+ _ = layer_index
172
+ return 1
173
+
174
+ def apply_layer(
175
+ self, circuit: CircuitProtocol, problem: QAOAProblemProtocol, parameters: Iterator[float], layer_index: int
176
+ ) -> None:
177
+ """Apply an X-rotation to each active qubit.
178
+
179
+ Args:
180
+ circuit (CircuitProtocol): The circuit to modify in place.
181
+ problem (QAOAProblemProtocol): Provides ``active_qubits``.
182
+ parameters (Iterator[float]): Supplies the mixing angle *beta*.
183
+ layer_index (int): Zero-based index of the current QAOA layer (unused).
184
+ """
185
+ _ = layer_index
186
+
187
+ p = next(parameters, None)
188
+ if p is None:
189
+ raise ValueError("XMixer expects exactly one parameter per layer.")
190
+
191
+ if not isinstance(circuit, SupportsAnsatz):
192
+ raise_type_error("'circuit'", "SupportsAnsatz", circuit)
193
+
194
+ _apply_x_rotation_to_qubits(circuit, problem.num_qubits, problem.active_qubits, p)
195
+
196
+
197
+ class UnconstrainedQAOAProblem:
198
+ """Problem representation for standard (unconstrained) QAOA.
199
+
200
+ Converts an :class:`~amplify.Model` into an Ising polynomial and exposes
201
+ the qubit indices and objective evaluation needed by the QAOA circuit
202
+ construction. Used internally by the standard QAOA implementation.
203
+ """
204
+
205
+ def __init__(self, model: Model) -> None:
206
+ """Initialize from an optimization model.
207
+
208
+ Args:
209
+ model (Model): The optimization model to solve. Constraints are
210
+ dropped; only the objective is converted to an Ising polynomial.
211
+ """
212
+ self.ising_poly = model.to_unconstrained_poly()
213
+ self.active_qubits = frozenset(i.id for i in self.ising_poly.variables)
214
+ self.num_qubits = max(self.active_qubits) + 1 if len(self.active_qubits) > 0 else 0
215
+
216
+ validate_poly(self.ising_poly, {VariableType.Ising: Degree.HighOrder})
217
+
218
+ def compute_objective_value(self, sol: Sequence[int]) -> float | None:
219
+ """Evaluate the Ising cost function at the given spin assignment.
220
+
221
+ Args:
222
+ sol (Sequence[int]): Spin values (``+1`` or ``-1``) indexed by qubit id.
223
+
224
+ Returns:
225
+ float | None: The objective value.
226
+ """
227
+ val = self.ising_poly.substitute({i: sol[i.id] for i in self.ising_poly.variables})
228
+ if not val.is_number():
229
+ raise RuntimeError(f"Expected the substituted value to be a number, but got {val}.")
230
+ return float(val)
231
+
232
+
233
+ class NHOTQAOAProblem:
234
+ """Problem representation for equality-constrained QAOA.
235
+
236
+ Converts an :class:`~amplify.Model` into an Ising polynomial while
237
+ separating disjoint n-hot (equality) constraints from the objective.
238
+ Constraints not recognized as n-hot are added as penalty terms.
239
+ """
240
+
241
+ def __init__(self, model: Model) -> None:
242
+ """Initialize from an optimization model.
243
+
244
+ Args:
245
+ model (Model): The optimization model to solve. Disjoint n-hot
246
+ constraints are separated; unrecognized constraints are added
247
+ as penalty terms.
248
+ """
249
+ group_list, unused_constraint = gather_disjoint_n_hot_greedy(model.constraints)
250
+ # objective function
251
+ poly = model.objective
252
+ if isinstance(poly, Matrix):
253
+ poly = poly.to_poly()
254
+ # penalty terms for constraints that are not in the shape of n-hot
255
+ for c in unused_constraint:
256
+ poly += c.penalty
257
+
258
+ self.ising_poly = poly
259
+ self.n_hot_constraints = group_list
260
+
261
+ self.constrained_indices = frozenset(i.id for group, _ in group_list for i in group)
262
+ self.unconstrained_indices = frozenset(
263
+ i.id for i in self.ising_poly.variables if i.id not in self.constrained_indices
264
+ )
265
+ self.active_qubits = self.unconstrained_indices | self.constrained_indices
266
+ self.num_qubits = max(
267
+ max(self.unconstrained_indices) + 1 if len(self.unconstrained_indices) > 0 else 0,
268
+ max(self.constrained_indices) + 1 if len(self.constrained_indices) > 0 else 0,
269
+ )
270
+
271
+ # TODO: Poly から変数の種類ごと (Ising, Binary など) の degree を取れるようになったら # noqa: FIX002
272
+ # ここではなく algo.qaoa._details._generate_hamiltonian で validate するようにする
273
+ validate_poly(self.ising_poly, {VariableType.Ising: Degree.HighOrder})
274
+
275
+ def compute_objective_value(self, sol: Sequence[int]) -> float | None:
276
+ """Evaluate the Ising cost function at the given spin assignment.
277
+
278
+ Returns ``None`` if the assignment violates any n-hot constraint.
279
+
280
+ Args:
281
+ sol (Sequence[int]): Spin values (``+1`` or ``-1``) indexed by qubit id.
282
+
283
+ Returns:
284
+ float | None: The objective value, or ``None`` if the solution is infeasible.
285
+ """
286
+ if not is_satisfied_group_constraints(sol, self.n_hot_constraints):
287
+ return None
288
+ val = self.ising_poly.substitute({i: sol[i.id] for i in self.ising_poly.variables})
289
+ if not val.is_number():
290
+ raise RuntimeError(f"Expected the substituted value to be a number, but got {val}.")
291
+ return float(val)
292
+
293
+
294
+ class FeasibleInitialState:
295
+ """Initial state that prepares a feasible state satisfying n-hot constraints.
296
+
297
+ Applies X gates to place constrained qubits into a feasible configuration
298
+ and Rx rotations to unconstrained qubits.
299
+ """
300
+
301
+ def num_parameters(self, problem: NHOTQAOAProblem) -> int:
302
+ """Return the number of variational parameters (one per unconstrained qubit)."""
303
+ return len(problem.unconstrained_indices)
304
+
305
+ def apply(self, circuit: CircuitProtocol, problem: NHOTQAOAProblem, parameters: Iterator[float]) -> None:
306
+ """Prepare a feasible initial state.
307
+
308
+ Args:
309
+ circuit (CircuitProtocol): The circuit to modify in place.
310
+ problem (NHOTQAOAProblem): Provides n-hot constraints and unconstrained qubit indices.
311
+ parameters (Iterator[float]): Supplies Rx rotation angles for unconstrained qubits.
312
+ """
313
+ num_parameters = self.num_parameters(problem)
314
+ ps = list(islice(parameters, num_parameters))
315
+ if len(ps) != num_parameters:
316
+ raise ValueError(f"FeasibleInitialState expects exactly {num_parameters} parameters per layer.")
317
+
318
+ if not isinstance(circuit, SupportsCAnsatz):
319
+ raise_type_error("'circuit'", "SupportsCAnsatz", circuit)
320
+
321
+ for group, num_flips in problem.n_hot_constraints:
322
+ for i in range(num_flips):
323
+ circuit.add_x_gate(group[i].id)
324
+
325
+ for param_index, i in enumerate(problem.unconstrained_indices):
326
+ circuit.add_rx_gate(i, ps[param_index])
327
+
328
+
329
+ def _apply_xy_rotation(circuit: SupportsCAnsatz, first: int, second: int, angle: float) -> None:
330
+ circuit.add_cnot_gate(first, second)
331
+ circuit.add_ry_gate(first, angle)
332
+ circuit.add_cnot_gate(second, first)
333
+ circuit.add_ry_gate(first, -angle)
334
+ circuit.add_cnot_gate(first, second)
335
+
336
+
337
+ class ConstraintPreservingMixer:
338
+ """Mixer that preserves n-hot constraints using XY-rotation gates.
339
+
340
+ Applies pairwise XY rotations within each constraint group so that
341
+ the quantum state remains in the feasible subspace throughout the circuit.
342
+ """
343
+
344
+ def num_parameters_per_layer(self, problem: NHOTQAOAProblem, layer_index: int) -> int:
345
+ """Return the number of variational parameters per layer."""
346
+ _ = layer_index
347
+ num_parameters = 0
348
+ for group, _ in problem.n_hot_constraints:
349
+ m = len(group)
350
+ num_parameters += m // 2 + (m - 1) // 2
351
+ return num_parameters
352
+
353
+ def apply_layer(
354
+ self, circuit: CircuitProtocol, problem: NHOTQAOAProblem, parameters: Iterator[float], layer_index: int
355
+ ) -> None:
356
+ """Apply pairwise XY rotations within each constraint group.
357
+
358
+ Args:
359
+ circuit (CircuitProtocol): The circuit to modify in place.
360
+ problem (NHOTQAOAProblem): Provides n-hot constraint groups.
361
+ parameters (Iterator[float]): Supplies rotation angles for XY gates.
362
+ layer_index (int): Zero-based index of the current QAOA layer (unused).
363
+ """
364
+ _ = layer_index
365
+ num_parameters = self.num_parameters_per_layer(problem, layer_index)
366
+ ps = list(islice(parameters, num_parameters))
367
+ if len(ps) != num_parameters:
368
+ raise ValueError(f"ConstraintPreservingMixer expects exactly {num_parameters} parameters per layer.")
369
+
370
+ if not isinstance(circuit, SupportsCAnsatz):
371
+ raise_type_error("'circuit'", "SupportsCAnsatz", circuit)
372
+
373
+ param_iter = iter(ps)
374
+
375
+ for group, _ in problem.n_hot_constraints:
376
+ m = len(group)
377
+ for index in range(0, m - 1, 2):
378
+ _apply_xy_rotation(circuit, group[index].id, group[index + 1].id, next(param_iter))
379
+ for index in range(1, m - 1, 2):
380
+ _apply_xy_rotation(circuit, group[index].id, group[index + 1].id, next(param_iter))
381
+
382
+
383
+ if TYPE_CHECKING:
384
+ from amplify_quantum.algo.qaoa.type import (
385
+ InitialStateProtocol,
386
+ MixerProtocol,
387
+ PhaseSeparatorProtocol,
388
+ )
389
+
390
+ _0: type[InitialStateProtocol] = UniformSuperpositionInitialState
391
+ _0 = FeasibleInitialState # pyright: ignore[reportConstantRedefinition]
392
+ _1: type[PhaseSeparatorProtocol] = CostPhaseSeparator
393
+ _1 = IdentityPhaseSeparator # pyright: ignore[reportConstantRedefinition]
394
+ _2: type[MixerProtocol] = XMixer
395
+ _2 = ConstraintPreservingMixer # pyright: ignore[reportConstantRedefinition]
396
+ _3: type[QAOAProblemProtocol] = UnconstrainedQAOAProblem
397
+ _3 = NHOTQAOAProblem # pyright: ignore[reportConstantRedefinition]
@@ -0,0 +1,10 @@
1
+ # Copyright (c) Fixstars Corporation and Fixstars Amplify Corporation.
2
+ #
3
+ # This source code is licensed under the Apache2.0 license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+
6
+ from .qaoa_auto import AutoQAOAImpl
7
+ from .qaoa_n_hot import NHOTQAOAImpl
8
+ from .qaoa_original import OriginalQAOAImpl
9
+
10
+ __all__ = ["AutoQAOAImpl", "NHOTQAOAImpl", "OriginalQAOAImpl"]
@@ -0,0 +1,40 @@
1
+ # Copyright (c) Fixstars Corporation and Fixstars Amplify Corporation.
2
+ #
3
+ # This source code is licensed under the Apache2.0 license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import TYPE_CHECKING
9
+
10
+ from amplify_quantum.algo.utility import gather_disjoint_n_hot_greedy
11
+
12
+ from .qaoa_n_hot import NHOTQAOAImpl
13
+ from .qaoa_original import OriginalQAOAImpl
14
+
15
+ if TYPE_CHECKING:
16
+ from amplify import Model
17
+
18
+
19
+ class AutoQAOAImpl:
20
+ """QAOA implementation that automatically selects the best strategy.
21
+
22
+ Returns :class:`NHOTQAOAImpl` when the model contains
23
+ disjoint n-hot constraints, otherwise :class:`OriginalQAOAImpl`.
24
+ """
25
+
26
+ def __new__(cls, model: Model, reps: int) -> NHOTQAOAImpl | OriginalQAOAImpl:
27
+ """Select and instantiate the best QAOA implementation for the given model.
28
+
29
+ Args:
30
+ model (Model): The optimization model to solve.
31
+ reps (int): Number of QAOA layers (circuit depth *p*).
32
+
33
+ Returns:
34
+ NHOTQAOAImpl | OriginalQAOAImpl: The selected implementation instance.
35
+ """
36
+ group_list, _ = gather_disjoint_n_hot_greedy(model.constraints)
37
+
38
+ if len(group_list) > 0:
39
+ return NHOTQAOAImpl(model, reps)
40
+ return OriginalQAOAImpl(model, reps)