amplify-quantum 1.0.0rc3__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 (46) hide show
  1. amplify_quantum/__init__.py +113 -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 +399 -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 +215 -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/details.py +312 -0
  16. amplify_quantum/algo/rqaoa/interface.py +221 -0
  17. amplify_quantum/algo/rqaoa/type.py +113 -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 +214 -0
  25. amplify_quantum/client/aqt.py +153 -0
  26. amplify_quantum/client/base.py +152 -0
  27. amplify_quantum/client/braketsimulator.py +109 -0
  28. amplify_quantum/client/ibm.py +175 -0
  29. amplify_quantum/client/ionq.py +154 -0
  30. amplify_quantum/client/iqm.py +154 -0
  31. amplify_quantum/client/qulacs.py +45 -0
  32. amplify_quantum/client/rigetti.py +154 -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 +557 -0
  40. amplify_quantum/sampler/qiskit.py +654 -0
  41. amplify_quantum/sampler/qulacs.py +119 -0
  42. amplify_quantum/utils/braket.py +110 -0
  43. amplify_quantum-1.0.0rc3.dist-info/METADATA +38 -0
  44. amplify_quantum-1.0.0rc3.dist-info/RECORD +46 -0
  45. amplify_quantum-1.0.0rc3.dist-info/WHEEL +4 -0
  46. amplify_quantum-1.0.0rc3.dist-info/licenses/LICENSE.txt +203 -0
@@ -0,0 +1,113 @@
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
+ CostPhaseSeparator,
21
+ IdentityPhaseSeparator,
22
+ InitialStateProtocol,
23
+ MixerProtocol,
24
+ PhaseSeparatorProtocol,
25
+ QAOADuration,
26
+ QAOAHistoryItem,
27
+ QAOAImplProtocol,
28
+ QAOAProblemProtocol,
29
+ QAOAResult,
30
+ QAOAType,
31
+ UnconstrainedQAOAProblem,
32
+ UniformSuperpositionInitialState,
33
+ XMixer,
34
+ )
35
+ from .algo.rqaoa import (
36
+ RQAOA,
37
+ NormalElimination,
38
+ RQAOADuration,
39
+ RQAOAHistoryItem,
40
+ RQAOAResult,
41
+ RQAOAType,
42
+ UnintentionalElimination,
43
+ )
44
+ from .circuit.qiskit import QiskitCircuit
45
+ from .circuit.qulacs import QulacsCircuit
46
+ from .client.aer import AerClient
47
+ from .client.aqt import AQTClient
48
+ from .client.base import QuantumBaseClient
49
+ from .client.braketsimulator import BraketSimulatorClient
50
+ from .client.ibm import IBMClient
51
+ from .client.ionq import IonQClient
52
+ from .client.iqm import IQMClient
53
+ from .client.qulacs import QulacsClient
54
+ from .client.rigetti import RigettiClient
55
+ from .minimize.base import MinimizeProto, MinimizeResult
56
+ from .minimize.no_op import NoOpMinimize, NoOpMinimizeResult
57
+ from .minimize.scipy import ScipyMinimize, ScipyMinimizeOptions, ScipyMinimizeResult
58
+ from .sampler.base import IsingSeqFreqList, SamplerProtocol, SamplingDuration
59
+ from .sampler.qiskit import AerSampler, IBMSampler, QiskitDeviceType, QiskitJobMeta
60
+ from .sampler.qulacs import QulacsJobMeta, QulacsSampler
61
+
62
+ __all__ = [
63
+ "QAOA",
64
+ "RQAOA",
65
+ "AQTClient",
66
+ "AerClient",
67
+ "AerSampler",
68
+ "BraketSimulatorClient",
69
+ "CostPhaseSeparator",
70
+ "IBMClient",
71
+ "IBMSampler",
72
+ "IQMClient",
73
+ "IdentityPhaseSeparator",
74
+ "InitialStateProtocol",
75
+ "IonQClient",
76
+ "IsingSeqFreqList",
77
+ "MinimizeProto",
78
+ "MinimizeResult",
79
+ "MixerProtocol",
80
+ "NoOpMinimize",
81
+ "NoOpMinimizeResult",
82
+ "NormalElimination",
83
+ "PhaseSeparatorProtocol",
84
+ "QAOADuration",
85
+ "QAOAHistoryItem",
86
+ "QAOAImplProtocol",
87
+ "QAOAProblemProtocol",
88
+ "QAOAResult",
89
+ "QAOAType",
90
+ "QiskitCircuit",
91
+ "QiskitDeviceType",
92
+ "QiskitJobMeta",
93
+ "QuantumAlgoProtocol",
94
+ "QuantumBaseClient",
95
+ "QulacsCircuit",
96
+ "QulacsClient",
97
+ "QulacsJobMeta",
98
+ "QulacsSampler",
99
+ "RQAOADuration",
100
+ "RQAOAHistoryItem",
101
+ "RQAOAResult",
102
+ "RQAOAType",
103
+ "RigettiClient",
104
+ "SamplerProtocol",
105
+ "SamplingDuration",
106
+ "ScipyMinimize",
107
+ "ScipyMinimizeOptions",
108
+ "ScipyMinimizeResult",
109
+ "UnconstrainedQAOAProblem",
110
+ "UniformSuperpositionInitialState",
111
+ "UnintentionalElimination",
112
+ "XMixer",
113
+ ]
@@ -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.0rc3'
22
+ __version_tuple__ = version_tuple = (1, 0, 0, 'rc3')
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
+ UniformSuperpositionInitialState,
11
+ XMixer,
12
+ )
13
+ from .impls.qaoa_original import UnconstrainedQAOAProblem
14
+ from .interface import QAOA, QAOAType
15
+ from .type import (
16
+ InitialStateProtocol,
17
+ MixerProtocol,
18
+ PhaseSeparatorProtocol,
19
+ QAOADuration,
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
+ "QAOADuration",
34
+ "QAOAHistoryItem",
35
+ "QAOAImplProtocol",
36
+ "QAOAProblemProtocol",
37
+ "QAOAResult",
38
+ "QAOAType",
39
+ "UnconstrainedQAOAProblem",
40
+ "UniformSuperpositionInitialState",
41
+ "XMixer",
42
+ ]
@@ -0,0 +1,399 @@
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
+ # TODO: Poly から変数の種類ごと (Ising, Binary など) の degree を取れるようになったら # noqa: FIX002
217
+ # ここではなく algo.qaoa._details._generate_hamiltonian で validate するようにする
218
+ validate_poly(self.ising_poly, {VariableType.Ising: Degree.HighOrder})
219
+
220
+ def compute_objective_value(self, sol: Sequence[int]) -> float | None:
221
+ """Evaluate the Ising cost function at the given spin assignment.
222
+
223
+ Args:
224
+ sol (Sequence[int]): Spin values (``+1`` or ``-1``) indexed by qubit id.
225
+
226
+ Returns:
227
+ float | None: The objective value.
228
+ """
229
+ val = self.ising_poly.substitute({i: sol[i.id] for i in self.ising_poly.variables})
230
+ if not val.is_number():
231
+ raise RuntimeError(f"Expected the substituted value to be a number, but got {val}.")
232
+ return float(val)
233
+
234
+
235
+ class EQConstrainedQAOAProblem:
236
+ """Problem representation for equality-constrained QAOA.
237
+
238
+ Converts an :class:`~amplify.Model` into an Ising polynomial while
239
+ separating disjoint n-hot (equality) constraints from the objective.
240
+ Constraints not recognized as n-hot are added as penalty terms.
241
+ """
242
+
243
+ def __init__(self, model: Model) -> None:
244
+ """Initialize from an optimization model.
245
+
246
+ Args:
247
+ model (Model): The optimization model to solve. Disjoint n-hot
248
+ constraints are separated; unrecognized constraints are added
249
+ as penalty terms.
250
+ """
251
+ group_list, unused_constraint = gather_disjoint_n_hot_greedy(model.constraints)
252
+ # objective function
253
+ poly = model.objective
254
+ if isinstance(poly, Matrix):
255
+ poly = poly.to_poly()
256
+ # penalty terms for constraints that are not in the shape of n-hot
257
+ for c in unused_constraint:
258
+ poly += c.penalty
259
+
260
+ self.ising_poly = poly
261
+ self.n_hot_constraints = group_list
262
+
263
+ self.constrained_indices = frozenset(i.id for group, _ in group_list for i in group)
264
+ self.unconstrained_indices = frozenset(
265
+ i.id for i in self.ising_poly.variables if i.id not in self.constrained_indices
266
+ )
267
+ self.active_qubits = self.unconstrained_indices | self.constrained_indices
268
+ self.num_qubits = max(
269
+ max(self.unconstrained_indices) + 1 if len(self.unconstrained_indices) > 0 else 0,
270
+ max(self.constrained_indices) + 1 if len(self.constrained_indices) > 0 else 0,
271
+ )
272
+
273
+ # TODO: Poly から変数の種類ごと (Ising, Binary など) の degree を取れるようになったら # noqa: FIX002
274
+ # ここではなく algo.qaoa._details._generate_hamiltonian で validate するようにする
275
+ validate_poly(self.ising_poly, {VariableType.Ising: Degree.HighOrder})
276
+
277
+ def compute_objective_value(self, sol: Sequence[int]) -> float | None:
278
+ """Evaluate the Ising cost function at the given spin assignment.
279
+
280
+ Returns ``None`` if the assignment violates any n-hot constraint.
281
+
282
+ Args:
283
+ sol (Sequence[int]): Spin values (``+1`` or ``-1``) indexed by qubit id.
284
+
285
+ Returns:
286
+ float | None: The objective value, or ``None`` if the solution is infeasible.
287
+ """
288
+ if not is_satisfied_group_constraints(sol, self.n_hot_constraints):
289
+ return None
290
+ val = self.ising_poly.substitute({i: sol[i.id] for i in self.ising_poly.variables})
291
+ if not val.is_number():
292
+ raise RuntimeError(f"Expected the substituted value to be a number, but got {val}.")
293
+ return float(val)
294
+
295
+
296
+ class FeasibleInitialState:
297
+ """Initial state that prepares a feasible state satisfying n-hot constraints.
298
+
299
+ Applies X gates to place constrained qubits into a feasible configuration
300
+ and Rx rotations to unconstrained qubits.
301
+ """
302
+
303
+ def num_parameters(self, problem: EQConstrainedQAOAProblem) -> int:
304
+ """Return the number of variational parameters (one per unconstrained qubit)."""
305
+ return len(problem.unconstrained_indices)
306
+
307
+ def apply(self, circuit: CircuitProtocol, problem: EQConstrainedQAOAProblem, parameters: Iterator[float]) -> None:
308
+ """Prepare a feasible initial state.
309
+
310
+ Args:
311
+ circuit (CircuitProtocol): The circuit to modify in place.
312
+ problem (EQConstrainedQAOAProblem): Provides n-hot constraints and unconstrained qubit indices.
313
+ parameters (Iterator[float]): Supplies Rx rotation angles for unconstrained qubits.
314
+ """
315
+ num_parameters = self.num_parameters(problem)
316
+ ps = list(islice(parameters, num_parameters))
317
+ if len(ps) != num_parameters:
318
+ raise ValueError(f"FeasibleInitialState expects exactly {num_parameters} parameters per layer.")
319
+
320
+ if not isinstance(circuit, SupportsCAnsatz):
321
+ raise_type_error("'circuit'", "SupportsCAnsatz", circuit)
322
+
323
+ for group, num_flips in problem.n_hot_constraints:
324
+ for i in range(num_flips):
325
+ circuit.add_x_gate(group[i].id)
326
+
327
+ for param_index, i in enumerate(problem.unconstrained_indices):
328
+ circuit.add_rx_gate(i, ps[param_index])
329
+
330
+
331
+ def _apply_xy_rotation(circuit: SupportsCAnsatz, first: int, second: int, angle: float) -> None:
332
+ circuit.add_cnot_gate(first, second)
333
+ circuit.add_ry_gate(first, angle)
334
+ circuit.add_cnot_gate(second, first)
335
+ circuit.add_ry_gate(first, -angle)
336
+ circuit.add_cnot_gate(first, second)
337
+
338
+
339
+ class ConstraintPreservingMixer:
340
+ """Mixer that preserves n-hot constraints using XY-rotation gates.
341
+
342
+ Applies pairwise XY rotations within each constraint group so that
343
+ the quantum state remains in the feasible subspace throughout the circuit.
344
+ """
345
+
346
+ def num_parameters_per_layer(self, problem: EQConstrainedQAOAProblem, layer_index: int) -> int:
347
+ """Return the number of variational parameters per layer."""
348
+ _ = layer_index
349
+ num_parameters = 0
350
+ for group, _ in problem.n_hot_constraints:
351
+ m = len(group)
352
+ num_parameters += m // 2 + (m - 1) // 2
353
+ return num_parameters
354
+
355
+ def apply_layer(
356
+ self, circuit: CircuitProtocol, problem: EQConstrainedQAOAProblem, parameters: Iterator[float], layer_index: int
357
+ ) -> None:
358
+ """Apply pairwise XY rotations within each constraint group.
359
+
360
+ Args:
361
+ circuit (CircuitProtocol): The circuit to modify in place.
362
+ problem (EQConstrainedQAOAProblem): Provides n-hot constraint groups.
363
+ parameters (Iterator[float]): Supplies rotation angles for XY gates.
364
+ layer_index (int): Zero-based index of the current QAOA layer (unused).
365
+ """
366
+ _ = layer_index
367
+ num_parameters = self.num_parameters_per_layer(problem, layer_index)
368
+ ps = list(islice(parameters, num_parameters))
369
+ if len(ps) != num_parameters:
370
+ raise ValueError(f"ConstraintPreservingMixer expects exactly {num_parameters} parameters per layer.")
371
+
372
+ if not isinstance(circuit, SupportsCAnsatz):
373
+ raise_type_error("'circuit'", "SupportsCAnsatz", circuit)
374
+
375
+ param_iter = iter(ps)
376
+
377
+ for group, _ in problem.n_hot_constraints:
378
+ m = len(group)
379
+ for index in range(0, m - 1, 2):
380
+ _apply_xy_rotation(circuit, group[index].id, group[index + 1].id, next(param_iter))
381
+ for index in range(1, m - 1, 2):
382
+ _apply_xy_rotation(circuit, group[index].id, group[index + 1].id, next(param_iter))
383
+
384
+
385
+ if TYPE_CHECKING:
386
+ from amplify_quantum.algo.qaoa.type import (
387
+ InitialStateProtocol,
388
+ MixerProtocol,
389
+ PhaseSeparatorProtocol,
390
+ )
391
+
392
+ _0: type[InitialStateProtocol] = UniformSuperpositionInitialState
393
+ _0 = FeasibleInitialState # pyright: ignore[reportConstantRedefinition]
394
+ _1: type[PhaseSeparatorProtocol] = CostPhaseSeparator
395
+ _1 = IdentityPhaseSeparator # pyright: ignore[reportConstantRedefinition]
396
+ _2: type[MixerProtocol] = XMixer
397
+ _2 = ConstraintPreservingMixer # pyright: ignore[reportConstantRedefinition]
398
+ _3: type[QAOAProblemProtocol] = UnconstrainedQAOAProblem
399
+ _3 = EQConstrainedQAOAProblem # 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 EQConstrainedQAOAImpl
8
+ from .qaoa_original import OriginalQAOAImpl
9
+
10
+ __all__ = ["AutoQAOAImpl", "EQConstrainedQAOAImpl", "OriginalQAOAImpl"]