qoro-divi 0.3.2b0__py3-none-any.whl → 0.3.4__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.
Potentially problematic release.
This version of qoro-divi might be problematic. Click here for more details.
- divi/__init__.py +1 -2
- divi/backends/__init__.py +7 -0
- divi/{parallel_simulator.py → backends/_parallel_simulator.py} +4 -3
- divi/{qoro_service.py → backends/_qoro_service.py} +27 -15
- divi/circuits/__init__.py +5 -0
- divi/{circuits.py → circuits/_core.py} +6 -20
- divi/{qasm.py → circuits/qasm.py} +2 -2
- divi/{exp → extern}/cirq/__init__.py +1 -1
- divi/{exp → extern}/cirq/_validator.py +10 -8
- divi/qprog/__init__.py +19 -6
- divi/qprog/algorithms/__init__.py +14 -0
- divi/qprog/algorithms/_ansatze.py +215 -0
- divi/qprog/{_qaoa.py → algorithms/_qaoa.py} +16 -26
- divi/qprog/{_vqe.py → algorithms/_vqe.py} +35 -133
- divi/qprog/batch.py +25 -19
- divi/qprog/optimizers.py +170 -45
- divi/qprog/quantum_program.py +142 -200
- divi/qprog/workflows/__init__.py +10 -0
- divi/qprog/{_graph_partitioning.py → workflows/_graph_partitioning.py} +6 -9
- divi/qprog/{_qubo_partitioning.py → workflows/_qubo_partitioning.py} +6 -7
- divi/qprog/{_vqe_sweep.py → workflows/_vqe_sweep.py} +35 -24
- divi/reporting/__init__.py +7 -0
- divi/{_pbar.py → reporting/_pbar.py} +13 -14
- divi/{qlogger.py → reporting/_qlogger.py} +8 -6
- divi/{reporter.py → reporting/_reporter.py} +24 -7
- divi/utils.py +14 -6
- {qoro_divi-0.3.2b0.dist-info → qoro_divi-0.3.4.dist-info}/METADATA +2 -2
- qoro_divi-0.3.4.dist-info/RECORD +68 -0
- qoro_divi-0.3.2b0.dist-info/RECORD +0 -62
- /divi/{interfaces.py → backends/_circuit_runner.py} +0 -0
- /divi/{qpu_system.py → backends/_qpu_system.py} +0 -0
- /divi/{qem.py → circuits/qem.py} +0 -0
- /divi/{exp → extern}/cirq/_lexer.py +0 -0
- /divi/{exp → extern}/cirq/_parser.py +0 -0
- /divi/{exp → extern}/cirq/_qasm_export.py +0 -0
- /divi/{exp → extern}/cirq/_qasm_import.py +0 -0
- /divi/{exp → extern}/cirq/exception.py +0 -0
- /divi/{exp → extern}/scipy/_cobyla.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/LICENCE.txt +0 -0
- /divi/{exp → extern}/scipy/pyprima/__init__.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/cobyla/__init__.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/cobyla/cobyla.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/cobyla/cobylb.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/cobyla/geometry.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/cobyla/initialize.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/cobyla/trustregion.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/cobyla/update.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/__init__.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/_bounds.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/_linear_constraints.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/_nonlinear_constraints.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/_project.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/checkbreak.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/consts.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/evaluate.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/history.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/infos.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/linalg.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/message.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/powalg.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/preproc.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/present.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/ratio.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/redrho.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/selectx.py +0 -0
- {qoro_divi-0.3.2b0.dist-info → qoro_divi-0.3.4.dist-info}/LICENSE +0 -0
- {qoro_divi-0.3.2b0.dist-info → qoro_divi-0.3.4.dist-info}/LICENSES/.license-header +0 -0
- {qoro_divi-0.3.2b0.dist-info → qoro_divi-0.3.4.dist-info}/LICENSES/Apache-2.0.txt +0 -0
- {qoro_divi-0.3.2b0.dist-info → qoro_divi-0.3.4.dist-info}/WHEEL +0 -0
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
#
|
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
|
|
5
|
-
from enum import Enum
|
|
6
5
|
from warnings import warn
|
|
7
6
|
|
|
8
7
|
import pennylane as qml
|
|
@@ -10,36 +9,8 @@ import sympy as sp
|
|
|
10
9
|
|
|
11
10
|
from divi.circuits import MetaCircuit
|
|
12
11
|
from divi.qprog import QuantumProgram
|
|
13
|
-
from divi.qprog.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class VQEAnsatz(Enum):
|
|
17
|
-
UCCSD = "UCCSD"
|
|
18
|
-
RY = "RY"
|
|
19
|
-
RYRZ = "RYRZ"
|
|
20
|
-
HW_EFFICIENT = "HW_EFFICIENT"
|
|
21
|
-
QAOA = "QAOA"
|
|
22
|
-
HARTREE_FOCK = "HF"
|
|
23
|
-
|
|
24
|
-
def describe(self):
|
|
25
|
-
return self.name, self.value
|
|
26
|
-
|
|
27
|
-
def n_params(self, n_qubits, **kwargs):
|
|
28
|
-
if self in (VQEAnsatz.UCCSD, VQEAnsatz.HARTREE_FOCK):
|
|
29
|
-
singles, doubles = qml.qchem.excitations(
|
|
30
|
-
kwargs.pop("n_electrons"), n_qubits
|
|
31
|
-
)
|
|
32
|
-
s_wires, d_wires = qml.qchem.excitations_to_wires(singles, doubles)
|
|
33
|
-
|
|
34
|
-
return len(s_wires) + len(d_wires)
|
|
35
|
-
elif self == VQEAnsatz.RY:
|
|
36
|
-
return n_qubits
|
|
37
|
-
elif self == VQEAnsatz.RYRZ:
|
|
38
|
-
return 2 * n_qubits
|
|
39
|
-
elif self == VQEAnsatz.HW_EFFICIENT:
|
|
40
|
-
raise NotImplementedError
|
|
41
|
-
elif self == VQEAnsatz.QAOA:
|
|
42
|
-
return qml.QAOAEmbedding.shape(n_layers=1, n_wires=n_qubits)[1]
|
|
12
|
+
from divi.qprog.algorithms._ansatze import Ansatz, HartreeFockAnsatz
|
|
13
|
+
from divi.qprog.optimizers import MonteCarloOptimizer, Optimizer
|
|
43
14
|
|
|
44
15
|
|
|
45
16
|
class VQE(QuantumProgram):
|
|
@@ -49,8 +20,8 @@ class VQE(QuantumProgram):
|
|
|
49
20
|
molecule: qml.qchem.Molecule | None = None,
|
|
50
21
|
n_electrons: int | None = None,
|
|
51
22
|
n_layers: int = 1,
|
|
52
|
-
ansatz=
|
|
53
|
-
optimizer=
|
|
23
|
+
ansatz: Ansatz | None = None,
|
|
24
|
+
optimizer: Optimizer | None = None,
|
|
54
25
|
max_iterations=10,
|
|
55
26
|
**kwargs,
|
|
56
27
|
) -> None:
|
|
@@ -62,19 +33,20 @@ class VQE(QuantumProgram):
|
|
|
62
33
|
molecule (pennylane.qchem.Molecule, optional): The molecule representing the problem.
|
|
63
34
|
n_electrons (int, optional): Number of electrons associated with the Hamiltonian.
|
|
64
35
|
Only needs to be provided when a Hamiltonian is given.
|
|
65
|
-
ansatz (
|
|
36
|
+
ansatz (Ansatz): The ansatz to use for the VQE problem
|
|
66
37
|
optimizer (Optimizers): The optimizer to use.
|
|
67
38
|
max_iterations (int): Maximum number of iteration optimizers.
|
|
68
39
|
"""
|
|
69
40
|
|
|
70
41
|
# Local Variables
|
|
42
|
+
self.ansatz = HartreeFockAnsatz() if ansatz is None else ansatz
|
|
71
43
|
self.n_layers = n_layers
|
|
72
44
|
self.results = {}
|
|
73
|
-
self.ansatz = ansatz
|
|
74
|
-
self.optimizer = optimizer
|
|
75
45
|
self.max_iterations = max_iterations
|
|
76
46
|
self.current_iteration = 0
|
|
77
47
|
|
|
48
|
+
self.optimizer = optimizer if optimizer is not None else MonteCarloOptimizer()
|
|
49
|
+
|
|
78
50
|
self._process_problem_input(
|
|
79
51
|
hamiltonian=hamiltonian, molecule=molecule, n_electrons=n_electrons
|
|
80
52
|
)
|
|
@@ -83,6 +55,13 @@ class VQE(QuantumProgram):
|
|
|
83
55
|
|
|
84
56
|
self._meta_circuits = self._create_meta_circuits_dict()
|
|
85
57
|
|
|
58
|
+
@property
|
|
59
|
+
def n_params(self):
|
|
60
|
+
return (
|
|
61
|
+
self.ansatz.n_params_per_layer(self.n_qubits, n_electrons=self.n_electrons)
|
|
62
|
+
* self.n_layers
|
|
63
|
+
)
|
|
64
|
+
|
|
86
65
|
def _process_problem_input(self, hamiltonian, molecule, n_electrons):
|
|
87
66
|
if hamiltonian is None and molecule is None:
|
|
88
67
|
raise ValueError(
|
|
@@ -90,13 +69,8 @@ class VQE(QuantumProgram):
|
|
|
90
69
|
)
|
|
91
70
|
|
|
92
71
|
if hamiltonian is not None:
|
|
93
|
-
if not isinstance(n_electrons, int) or n_electrons < 0:
|
|
94
|
-
raise ValueError(
|
|
95
|
-
f"`n_electrons` is expected to be a non-negative integer. Got {n_electrons}."
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
self.n_electrons = n_electrons
|
|
99
72
|
self.n_qubits = len(hamiltonian.wires)
|
|
73
|
+
self.n_electrons = n_electrons
|
|
100
74
|
|
|
101
75
|
if molecule is not None:
|
|
102
76
|
self.molecule = molecule
|
|
@@ -111,10 +85,6 @@ class VQE(QuantumProgram):
|
|
|
111
85
|
UserWarning,
|
|
112
86
|
)
|
|
113
87
|
|
|
114
|
-
self.n_params = self.ansatz.n_params(
|
|
115
|
-
self.n_qubits, n_electrons=self.n_electrons
|
|
116
|
-
)
|
|
117
|
-
|
|
118
88
|
self.cost_hamiltonian = self._clean_hamiltonian(hamiltonian)
|
|
119
89
|
|
|
120
90
|
def _clean_hamiltonian(
|
|
@@ -137,8 +107,8 @@ class VQE(QuantumProgram):
|
|
|
137
107
|
)
|
|
138
108
|
)
|
|
139
109
|
|
|
140
|
-
self.loss_constant =
|
|
141
|
-
map(lambda x: hamiltonian[x].scalar, constant_terms_idx)
|
|
110
|
+
self.loss_constant = float(
|
|
111
|
+
sum(map(lambda x: hamiltonian[x].scalar, constant_terms_idx))
|
|
142
112
|
)
|
|
143
113
|
|
|
144
114
|
for idx in constant_terms_idx:
|
|
@@ -147,9 +117,17 @@ class VQE(QuantumProgram):
|
|
|
147
117
|
return hamiltonian.simplify()
|
|
148
118
|
|
|
149
119
|
def _create_meta_circuits_dict(self) -> dict[str, MetaCircuit]:
|
|
150
|
-
weights_syms = sp.symarray(
|
|
120
|
+
weights_syms = sp.symarray(
|
|
121
|
+
"w",
|
|
122
|
+
(
|
|
123
|
+
self.n_layers,
|
|
124
|
+
self.ansatz.n_params_per_layer(
|
|
125
|
+
self.n_qubits, n_electrons=self.n_electrons
|
|
126
|
+
),
|
|
127
|
+
),
|
|
128
|
+
)
|
|
151
129
|
|
|
152
|
-
def _prepare_circuit(
|
|
130
|
+
def _prepare_circuit(hamiltonian, params):
|
|
153
131
|
"""
|
|
154
132
|
Prepare the circuit for the VQE problem.
|
|
155
133
|
Args:
|
|
@@ -157,7 +135,12 @@ class VQE(QuantumProgram):
|
|
|
157
135
|
hamiltonian (qml.Hamiltonian): The Hamiltonian to use
|
|
158
136
|
params (list): The parameters to use for the ansatz
|
|
159
137
|
"""
|
|
160
|
-
self.
|
|
138
|
+
self.ansatz.build(
|
|
139
|
+
params,
|
|
140
|
+
n_qubits=self.n_qubits,
|
|
141
|
+
n_layers=self.n_layers,
|
|
142
|
+
n_electrons=self.n_electrons,
|
|
143
|
+
)
|
|
161
144
|
|
|
162
145
|
# Even though in principle we want to sample from a state,
|
|
163
146
|
# we are applying an `expval` operation here to make it compatible
|
|
@@ -168,93 +151,12 @@ class VQE(QuantumProgram):
|
|
|
168
151
|
return {
|
|
169
152
|
"cost_circuit": self._meta_circuit_factory(
|
|
170
153
|
qml.tape.make_qscript(_prepare_circuit)(
|
|
171
|
-
self.
|
|
154
|
+
self.cost_hamiltonian, weights_syms
|
|
172
155
|
),
|
|
173
156
|
symbols=weights_syms.flatten(),
|
|
174
157
|
)
|
|
175
158
|
}
|
|
176
159
|
|
|
177
|
-
def _set_ansatz(self, ansatz: VQEAnsatz, params):
|
|
178
|
-
"""
|
|
179
|
-
Set the ansatz for the VQE problem.
|
|
180
|
-
Args:
|
|
181
|
-
ansatz (Ansatze): The ansatz to use
|
|
182
|
-
params (list): The parameters to use for the ansatz
|
|
183
|
-
n_layers (int): The number of layers to use for the ansatz
|
|
184
|
-
"""
|
|
185
|
-
|
|
186
|
-
def _add_hw_efficient_ansatz(params):
|
|
187
|
-
raise NotImplementedError
|
|
188
|
-
|
|
189
|
-
def _add_qaoa_ansatz(params):
|
|
190
|
-
# This infers layers automatically from the parameters shape
|
|
191
|
-
qml.QAOAEmbedding(
|
|
192
|
-
features=[],
|
|
193
|
-
weights=params.reshape(self.n_layers, -1),
|
|
194
|
-
wires=range(self.n_qubits),
|
|
195
|
-
)
|
|
196
|
-
|
|
197
|
-
def _add_ry_ansatz(params):
|
|
198
|
-
qml.layer(
|
|
199
|
-
qml.AngleEmbedding,
|
|
200
|
-
self.n_layers,
|
|
201
|
-
params.reshape(self.n_layers, -1),
|
|
202
|
-
wires=range(self.n_qubits),
|
|
203
|
-
rotation="Y",
|
|
204
|
-
)
|
|
205
|
-
|
|
206
|
-
def _add_ryrz_ansatz(params):
|
|
207
|
-
def _ryrz(params, wires):
|
|
208
|
-
ry_rots, rz_rots = params.reshape(2, -1)
|
|
209
|
-
qml.AngleEmbedding(ry_rots, wires=wires, rotation="Y")
|
|
210
|
-
qml.AngleEmbedding(rz_rots, wires=wires, rotation="Z")
|
|
211
|
-
|
|
212
|
-
qml.layer(
|
|
213
|
-
_ryrz,
|
|
214
|
-
self.n_layers,
|
|
215
|
-
params.reshape(self.n_layers, -1),
|
|
216
|
-
wires=range(self.n_qubits),
|
|
217
|
-
)
|
|
218
|
-
|
|
219
|
-
def _add_uccsd_ansatz(params):
|
|
220
|
-
hf_state = qml.qchem.hf_state(self.n_electrons, self.n_qubits)
|
|
221
|
-
|
|
222
|
-
singles, doubles = qml.qchem.excitations(self.n_electrons, self.n_qubits)
|
|
223
|
-
s_wires, d_wires = qml.qchem.excitations_to_wires(singles, doubles)
|
|
224
|
-
|
|
225
|
-
qml.UCCSD(
|
|
226
|
-
params.reshape(self.n_layers, -1),
|
|
227
|
-
wires=range(self.n_qubits),
|
|
228
|
-
s_wires=s_wires,
|
|
229
|
-
d_wires=d_wires,
|
|
230
|
-
init_state=hf_state,
|
|
231
|
-
n_repeats=self.n_layers,
|
|
232
|
-
)
|
|
233
|
-
|
|
234
|
-
def _add_hartree_fock_ansatz(params):
|
|
235
|
-
singles, doubles = qml.qchem.excitations(self.n_electrons, self.n_qubits)
|
|
236
|
-
hf_state = qml.qchem.hf_state(self.n_electrons, self.n_qubits)
|
|
237
|
-
|
|
238
|
-
qml.layer(
|
|
239
|
-
qml.AllSinglesDoubles,
|
|
240
|
-
self.n_layers,
|
|
241
|
-
params.reshape(self.n_layers, -1),
|
|
242
|
-
wires=range(self.n_qubits),
|
|
243
|
-
hf_state=hf_state,
|
|
244
|
-
singles=singles,
|
|
245
|
-
doubles=doubles,
|
|
246
|
-
)
|
|
247
|
-
|
|
248
|
-
# Reset the BasisState operations after the first layer
|
|
249
|
-
# for behaviour similar to UCCSD ansatz
|
|
250
|
-
for op in qml.QueuingManager.active_context().queue[1:]:
|
|
251
|
-
op._hyperparameters["hf_state"] = 0
|
|
252
|
-
|
|
253
|
-
if ansatz in VQEAnsatz:
|
|
254
|
-
locals()[f"_add_{ansatz.name.lower()}_ansatz"](params)
|
|
255
|
-
else:
|
|
256
|
-
raise ValueError(f"Invalid Ansatz Value. Got {ansatz}.")
|
|
257
|
-
|
|
258
160
|
def _generate_circuits(self):
|
|
259
161
|
"""
|
|
260
162
|
Generate the circuits for the VQE problem.
|
divi/qprog/batch.py
CHANGED
|
@@ -10,16 +10,15 @@ from multiprocessing import Event, Manager
|
|
|
10
10
|
from multiprocessing.synchronize import Event as EventClass
|
|
11
11
|
from queue import Empty, Queue
|
|
12
12
|
from threading import Lock, Thread
|
|
13
|
+
from typing import Any
|
|
13
14
|
from warnings import warn
|
|
14
15
|
|
|
15
16
|
from rich.console import Console
|
|
16
17
|
from rich.progress import Progress, TaskID
|
|
17
18
|
|
|
18
|
-
from divi.
|
|
19
|
-
from divi.interfaces import CircuitRunner
|
|
20
|
-
from divi.parallel_simulator import ParallelSimulator
|
|
21
|
-
from divi.qlogger import disable_logging
|
|
19
|
+
from divi.backends import CircuitRunner, ParallelSimulator
|
|
22
20
|
from divi.qprog.quantum_program import QuantumProgram
|
|
21
|
+
from divi.reporting import disable_logging, make_progress_bar
|
|
23
22
|
|
|
24
23
|
|
|
25
24
|
def queue_listener(
|
|
@@ -32,7 +31,7 @@ def queue_listener(
|
|
|
32
31
|
):
|
|
33
32
|
while not done_event.is_set():
|
|
34
33
|
try:
|
|
35
|
-
msg = queue.get(timeout=0.1)
|
|
34
|
+
msg: dict[str, Any] = queue.get(timeout=0.1)
|
|
36
35
|
except Empty:
|
|
37
36
|
continue
|
|
38
37
|
except Exception as e:
|
|
@@ -42,14 +41,25 @@ def queue_listener(
|
|
|
42
41
|
with lock:
|
|
43
42
|
task_id = pb_task_map[msg["job_id"]]
|
|
44
43
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
44
|
+
# Prepare update arguments, starting with progress.
|
|
45
|
+
update_args = {"advance": msg["progress"]}
|
|
46
|
+
|
|
47
|
+
if "poll_attempt" in msg:
|
|
48
|
+
update_args["poll_attempt"] = msg.get("poll_attempt", 0)
|
|
49
|
+
if "max_retries" in msg:
|
|
50
|
+
update_args["max_retries"] = msg.get("max_retries")
|
|
51
|
+
if "service_job_id" in msg:
|
|
52
|
+
update_args["service_job_id"] = msg.get("service_job_id")
|
|
53
|
+
if "job_status" in msg:
|
|
54
|
+
update_args["job_status"] = msg.get("job_status")
|
|
55
|
+
if msg.get("message"):
|
|
56
|
+
update_args["message"] = msg.get("message")
|
|
57
|
+
if "final_status" in msg:
|
|
58
|
+
update_args["final_status"] = msg.get("final_status", "")
|
|
59
|
+
|
|
60
|
+
update_args["refresh"] = is_jupyter
|
|
61
|
+
|
|
62
|
+
progress_bar.update(task_id, **update_args)
|
|
53
63
|
|
|
54
64
|
|
|
55
65
|
def _default_task_function(program: QuantumProgram):
|
|
@@ -182,10 +192,7 @@ class ProgramBatch(ABC):
|
|
|
182
192
|
raise RuntimeError("No programs to run.")
|
|
183
193
|
|
|
184
194
|
self._progress_bar = (
|
|
185
|
-
make_progress_bar(
|
|
186
|
-
max_retries=None if self._is_local else self.backend.max_retries,
|
|
187
|
-
is_jupyter=self._is_jupyter,
|
|
188
|
-
)
|
|
195
|
+
make_progress_bar(is_jupyter=self._is_jupyter)
|
|
189
196
|
if hasattr(self, "max_iterations")
|
|
190
197
|
else None
|
|
191
198
|
)
|
|
@@ -217,8 +224,7 @@ class ProgramBatch(ABC):
|
|
|
217
224
|
if not blocking:
|
|
218
225
|
# Arm safety net
|
|
219
226
|
atexit.register(self._atexit_cleanup_hook)
|
|
220
|
-
|
|
221
|
-
if blocking:
|
|
227
|
+
else:
|
|
222
228
|
self.join()
|
|
223
229
|
|
|
224
230
|
return self
|
divi/qprog/optimizers.py
CHANGED
|
@@ -2,74 +2,199 @@
|
|
|
2
2
|
#
|
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from collections.abc import Callable
|
|
5
7
|
from enum import Enum
|
|
6
8
|
|
|
7
9
|
import numpy as np
|
|
10
|
+
from scipy.optimize import OptimizeResult, minimize
|
|
8
11
|
|
|
12
|
+
from divi.extern.scipy._cobyla import _minimize_cobyla as cobyla_fn
|
|
9
13
|
|
|
10
|
-
|
|
14
|
+
|
|
15
|
+
class ScipyMethod(Enum):
|
|
11
16
|
NELDER_MEAD = "Nelder-Mead"
|
|
12
17
|
COBYLA = "COBYLA"
|
|
13
|
-
MONTE_CARLO = "Monte Carlo"
|
|
14
18
|
L_BFGS_B = "L-BFGS-B"
|
|
15
19
|
|
|
16
|
-
def describe(self):
|
|
17
|
-
return self.name, self.value
|
|
18
20
|
|
|
21
|
+
class Optimizer(ABC):
|
|
19
22
|
@property
|
|
23
|
+
@abstractmethod
|
|
20
24
|
def n_param_sets(self):
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
"""
|
|
26
|
+
Returns the number of parameter sets the optimizer can handle per optimization run.
|
|
27
|
+
Returns:
|
|
28
|
+
int: Number of parameter sets.
|
|
29
|
+
"""
|
|
30
|
+
raise NotImplementedError("This method should be implemented by subclasses.")
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def optimize(
|
|
34
|
+
self,
|
|
35
|
+
cost_fn: Callable[[np.ndarray], float],
|
|
36
|
+
initial_params: np.ndarray,
|
|
37
|
+
callback_fn: Callable | None = None,
|
|
38
|
+
**kwargs,
|
|
39
|
+
) -> OptimizeResult:
|
|
40
|
+
"""
|
|
41
|
+
Optimize the given cost function starting from initial parameters.
|
|
42
|
+
|
|
43
|
+
Parameters:
|
|
44
|
+
cost_fn: The cost function to minimize.
|
|
45
|
+
initial_params: Initial parameters for the optimization.
|
|
46
|
+
**kwargs: Additional keyword arguments for the optimizer.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Optimized parameters.
|
|
50
|
+
"""
|
|
51
|
+
raise NotImplementedError("This method should be implemented by subclasses.")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ScipyOptimizer(Optimizer):
|
|
55
|
+
def __init__(self, method: ScipyMethod):
|
|
56
|
+
self.method = method
|
|
25
57
|
|
|
26
58
|
@property
|
|
27
|
-
def
|
|
28
|
-
if self == Optimizer.MONTE_CARLO:
|
|
29
|
-
return 10
|
|
59
|
+
def n_param_sets(self):
|
|
30
60
|
return 1
|
|
31
61
|
|
|
32
|
-
def
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
62
|
+
def optimize(
|
|
63
|
+
self,
|
|
64
|
+
cost_fn: Callable[[np.ndarray], float],
|
|
65
|
+
initial_params: np.ndarray,
|
|
66
|
+
callback_fn: Callable | None = None,
|
|
67
|
+
**kwargs,
|
|
68
|
+
):
|
|
69
|
+
max_iterations = kwargs.pop("maxiter", None)
|
|
70
|
+
|
|
71
|
+
if max_iterations is None or self.method == ScipyMethod.COBYLA:
|
|
72
|
+
# COBYLA perceive maxiter as maxfev so we need
|
|
73
|
+
# to use the callback fn for counting instead.
|
|
74
|
+
maxiter = None
|
|
75
|
+
else:
|
|
76
|
+
# Need to add one more iteration for Nelder-Mead's simplex initialization step
|
|
77
|
+
maxiter = (
|
|
78
|
+
max_iterations + 1
|
|
79
|
+
if self.method == ScipyMethod.NELDER_MEAD
|
|
80
|
+
else max_iterations
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return minimize(
|
|
84
|
+
cost_fn,
|
|
85
|
+
initial_params.squeeze(),
|
|
86
|
+
method=(
|
|
87
|
+
cobyla_fn if self.method == ScipyMethod.COBYLA else self.method.value
|
|
88
|
+
),
|
|
89
|
+
jac=(
|
|
90
|
+
kwargs.pop("jac", None) if self.method == ScipyMethod.L_BFGS_B else None
|
|
91
|
+
),
|
|
92
|
+
callback=callback_fn,
|
|
93
|
+
options={"maxiter": maxiter},
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class MonteCarloOptimizer(Optimizer):
|
|
98
|
+
def __init__(self, n_param_sets: int = 10, n_best_sets: int = 3):
|
|
99
|
+
super().__init__()
|
|
100
|
+
|
|
101
|
+
if n_best_sets > n_param_sets:
|
|
102
|
+
raise ValueError("n_best_sets must be less than or equal to n_param_sets.")
|
|
103
|
+
|
|
104
|
+
self._n_param_sets = n_param_sets
|
|
105
|
+
self._n_best_sets = n_best_sets
|
|
106
|
+
|
|
107
|
+
# Calculate how many times each of the best sets should be repeated
|
|
108
|
+
samples_per_best = self.n_param_sets // self.n_best_sets
|
|
109
|
+
remainder = self.n_param_sets % self.n_best_sets
|
|
110
|
+
self._repeat_counts = np.full(self.n_best_sets, samples_per_best)
|
|
111
|
+
self._repeat_counts[:remainder] += 1
|
|
40
112
|
|
|
41
|
-
|
|
113
|
+
@property
|
|
114
|
+
def n_param_sets(self):
|
|
115
|
+
return self._n_param_sets
|
|
42
116
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
117
|
+
@property
|
|
118
|
+
def n_best_sets(self):
|
|
119
|
+
return self._n_best_sets
|
|
120
|
+
|
|
121
|
+
def _compute_new_parameters(
|
|
122
|
+
self,
|
|
123
|
+
params: np.ndarray,
|
|
124
|
+
curr_iteration: int,
|
|
125
|
+
best_indices: np.ndarray,
|
|
126
|
+
rng: np.random.Generator,
|
|
127
|
+
) -> np.ndarray:
|
|
128
|
+
"""
|
|
129
|
+
Generates a new population of parameters based on the best-performing ones.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
# 1. Select the best parameter sets from the current population
|
|
133
|
+
best_params = params[best_indices]
|
|
134
|
+
|
|
135
|
+
# 2. Prepare the means for sampling by repeating each best parameter set
|
|
136
|
+
# according to its assigned count
|
|
137
|
+
new_means = np.repeat(best_params, self._repeat_counts, axis=0)
|
|
138
|
+
|
|
139
|
+
# 3. Define the standard deviation (scale), which shrinks over iterations
|
|
140
|
+
scale = 1.0 / (2.0 * (curr_iteration + 1.0))
|
|
141
|
+
|
|
142
|
+
# 4. Generate all new parameters in a single vectorized call
|
|
143
|
+
new_params = rng.normal(loc=new_means, scale=scale)
|
|
144
|
+
|
|
145
|
+
# Apply periodic boundary conditions
|
|
146
|
+
return new_params % (2 * np.pi)
|
|
147
|
+
|
|
148
|
+
def optimize(
|
|
149
|
+
self,
|
|
150
|
+
cost_fn: Callable[[np.ndarray], float],
|
|
151
|
+
initial_params: np.ndarray,
|
|
152
|
+
callback_fn: Callable[[OptimizeResult], float | np.ndarray] | None = None,
|
|
153
|
+
**kwargs,
|
|
154
|
+
) -> OptimizeResult:
|
|
155
|
+
"""
|
|
156
|
+
Perform Monte Carlo optimization on the cost function.
|
|
157
|
+
|
|
158
|
+
Parameters:
|
|
159
|
+
cost_fn: The cost function to minimize.
|
|
160
|
+
initial_params: Initial parameters for the optimization.
|
|
161
|
+
callback_fn: Optional callback function to monitor progress.
|
|
162
|
+
**kwargs: Additional keyword arguments for the optimizer.
|
|
163
|
+
Returns:
|
|
164
|
+
Optimized parameters.
|
|
165
|
+
"""
|
|
166
|
+
rng = kwargs.pop("rng", np.random.default_rng())
|
|
167
|
+
max_iterations = kwargs.pop("maxiter", 5)
|
|
52
168
|
|
|
53
|
-
|
|
54
|
-
new_param = np.clip(new_param, 0, 2 * np.pi)
|
|
169
|
+
population = np.copy(initial_params)
|
|
55
170
|
|
|
56
|
-
|
|
171
|
+
final_params = None
|
|
172
|
+
final_losses = None
|
|
57
173
|
|
|
58
|
-
|
|
174
|
+
for curr_iter in range(max_iterations):
|
|
175
|
+
# Evaluate the entire population once
|
|
176
|
+
losses = cost_fn(population)
|
|
59
177
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
178
|
+
# Find the indices of the best-performing parameter sets (only once)
|
|
179
|
+
best_indices = np.argpartition(losses, self.n_best_sets - 1)[
|
|
180
|
+
: self.n_best_sets
|
|
181
|
+
]
|
|
63
182
|
|
|
64
|
-
|
|
65
|
-
|
|
183
|
+
# Store the current best results
|
|
184
|
+
final_params = population[best_indices]
|
|
185
|
+
final_losses = losses[best_indices]
|
|
66
186
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
).astype(np.float64)
|
|
187
|
+
if callback_fn:
|
|
188
|
+
callback_fn(OptimizeResult(x=final_params, fun=final_losses))
|
|
70
189
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
190
|
+
# Generate the next generation of parameters
|
|
191
|
+
population = self._compute_new_parameters(
|
|
192
|
+
population, curr_iter, best_indices, rng
|
|
193
|
+
)
|
|
74
194
|
|
|
75
|
-
|
|
195
|
+
# Return the best results from the LAST EVALUATED population
|
|
196
|
+
return OptimizeResult(
|
|
197
|
+
x=final_params,
|
|
198
|
+
fun=final_losses,
|
|
199
|
+
nit=max_iterations,
|
|
200
|
+
)
|