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.

Files changed (69) hide show
  1. divi/__init__.py +1 -2
  2. divi/backends/__init__.py +7 -0
  3. divi/{parallel_simulator.py → backends/_parallel_simulator.py} +4 -3
  4. divi/{qoro_service.py → backends/_qoro_service.py} +27 -15
  5. divi/circuits/__init__.py +5 -0
  6. divi/{circuits.py → circuits/_core.py} +6 -20
  7. divi/{qasm.py → circuits/qasm.py} +2 -2
  8. divi/{exp → extern}/cirq/__init__.py +1 -1
  9. divi/{exp → extern}/cirq/_validator.py +10 -8
  10. divi/qprog/__init__.py +19 -6
  11. divi/qprog/algorithms/__init__.py +14 -0
  12. divi/qprog/algorithms/_ansatze.py +215 -0
  13. divi/qprog/{_qaoa.py → algorithms/_qaoa.py} +16 -26
  14. divi/qprog/{_vqe.py → algorithms/_vqe.py} +35 -133
  15. divi/qprog/batch.py +25 -19
  16. divi/qprog/optimizers.py +170 -45
  17. divi/qprog/quantum_program.py +142 -200
  18. divi/qprog/workflows/__init__.py +10 -0
  19. divi/qprog/{_graph_partitioning.py → workflows/_graph_partitioning.py} +6 -9
  20. divi/qprog/{_qubo_partitioning.py → workflows/_qubo_partitioning.py} +6 -7
  21. divi/qprog/{_vqe_sweep.py → workflows/_vqe_sweep.py} +35 -24
  22. divi/reporting/__init__.py +7 -0
  23. divi/{_pbar.py → reporting/_pbar.py} +13 -14
  24. divi/{qlogger.py → reporting/_qlogger.py} +8 -6
  25. divi/{reporter.py → reporting/_reporter.py} +24 -7
  26. divi/utils.py +14 -6
  27. {qoro_divi-0.3.2b0.dist-info → qoro_divi-0.3.4.dist-info}/METADATA +2 -2
  28. qoro_divi-0.3.4.dist-info/RECORD +68 -0
  29. qoro_divi-0.3.2b0.dist-info/RECORD +0 -62
  30. /divi/{interfaces.py → backends/_circuit_runner.py} +0 -0
  31. /divi/{qpu_system.py → backends/_qpu_system.py} +0 -0
  32. /divi/{qem.py → circuits/qem.py} +0 -0
  33. /divi/{exp → extern}/cirq/_lexer.py +0 -0
  34. /divi/{exp → extern}/cirq/_parser.py +0 -0
  35. /divi/{exp → extern}/cirq/_qasm_export.py +0 -0
  36. /divi/{exp → extern}/cirq/_qasm_import.py +0 -0
  37. /divi/{exp → extern}/cirq/exception.py +0 -0
  38. /divi/{exp → extern}/scipy/_cobyla.py +0 -0
  39. /divi/{exp → extern}/scipy/pyprima/LICENCE.txt +0 -0
  40. /divi/{exp → extern}/scipy/pyprima/__init__.py +0 -0
  41. /divi/{exp → extern}/scipy/pyprima/cobyla/__init__.py +0 -0
  42. /divi/{exp → extern}/scipy/pyprima/cobyla/cobyla.py +0 -0
  43. /divi/{exp → extern}/scipy/pyprima/cobyla/cobylb.py +0 -0
  44. /divi/{exp → extern}/scipy/pyprima/cobyla/geometry.py +0 -0
  45. /divi/{exp → extern}/scipy/pyprima/cobyla/initialize.py +0 -0
  46. /divi/{exp → extern}/scipy/pyprima/cobyla/trustregion.py +0 -0
  47. /divi/{exp → extern}/scipy/pyprima/cobyla/update.py +0 -0
  48. /divi/{exp → extern}/scipy/pyprima/common/__init__.py +0 -0
  49. /divi/{exp → extern}/scipy/pyprima/common/_bounds.py +0 -0
  50. /divi/{exp → extern}/scipy/pyprima/common/_linear_constraints.py +0 -0
  51. /divi/{exp → extern}/scipy/pyprima/common/_nonlinear_constraints.py +0 -0
  52. /divi/{exp → extern}/scipy/pyprima/common/_project.py +0 -0
  53. /divi/{exp → extern}/scipy/pyprima/common/checkbreak.py +0 -0
  54. /divi/{exp → extern}/scipy/pyprima/common/consts.py +0 -0
  55. /divi/{exp → extern}/scipy/pyprima/common/evaluate.py +0 -0
  56. /divi/{exp → extern}/scipy/pyprima/common/history.py +0 -0
  57. /divi/{exp → extern}/scipy/pyprima/common/infos.py +0 -0
  58. /divi/{exp → extern}/scipy/pyprima/common/linalg.py +0 -0
  59. /divi/{exp → extern}/scipy/pyprima/common/message.py +0 -0
  60. /divi/{exp → extern}/scipy/pyprima/common/powalg.py +0 -0
  61. /divi/{exp → extern}/scipy/pyprima/common/preproc.py +0 -0
  62. /divi/{exp → extern}/scipy/pyprima/common/present.py +0 -0
  63. /divi/{exp → extern}/scipy/pyprima/common/ratio.py +0 -0
  64. /divi/{exp → extern}/scipy/pyprima/common/redrho.py +0 -0
  65. /divi/{exp → extern}/scipy/pyprima/common/selectx.py +0 -0
  66. {qoro_divi-0.3.2b0.dist-info → qoro_divi-0.3.4.dist-info}/LICENSE +0 -0
  67. {qoro_divi-0.3.2b0.dist-info → qoro_divi-0.3.4.dist-info}/LICENSES/.license-header +0 -0
  68. {qoro_divi-0.3.2b0.dist-info → qoro_divi-0.3.4.dist-info}/LICENSES/Apache-2.0.txt +0 -0
  69. {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.optimizers import Optimizer
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=VQEAnsatz.HARTREE_FOCK,
53
- optimizer=Optimizer.MONTE_CARLO,
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 (VQEAnsatz): The ansatz to use for the VQE problem
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 = sum(
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("w", (self.n_layers, self.n_params))
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(ansatz, hamiltonian, params):
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._set_ansatz(ansatz, params)
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.ansatz, self.cost_hamiltonian, weights_syms
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._pbar import make_progress_bar
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
- progress_bar.update(
46
- task_id,
47
- advance=msg["progress"],
48
- poll_attempt=msg.get("poll_attempt", 0),
49
- message=msg.get("message", ""),
50
- final_status=msg.get("final_status", ""),
51
- refresh=is_jupyter,
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
- class Optimizer(Enum):
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
- if self in (Optimizer.NELDER_MEAD, Optimizer.L_BFGS_B, Optimizer.COBYLA):
22
- return 1
23
- elif self == Optimizer.MONTE_CARLO:
24
- return 10
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 n_samples(self):
28
- if self == Optimizer.MONTE_CARLO:
29
- return 10
59
+ def n_param_sets(self):
30
60
  return 1
31
61
 
32
- def compute_new_parameters(self, params, iteration, **kwargs):
33
- if self != Optimizer.MONTE_CARLO:
34
- raise NotImplementedError
35
-
36
- rng = kwargs.pop("rng", np.random.default_rng())
37
-
38
- losses = kwargs.pop("losses")
39
- smallest_energy_keys = sorted(losses, key=lambda k: losses[k])[: self.n_samples]
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
- new_params = []
113
+ @property
114
+ def n_param_sets(self):
115
+ return self._n_param_sets
42
116
 
43
- for key in smallest_energy_keys:
44
- new_param_set = [
45
- rng.normal(
46
- params[int(key)],
47
- 1 / (2 * iteration),
48
- size=params[int(key)].shape,
49
- )
50
- for _ in range(self.n_param_sets)
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
- for new_param in new_param_set:
54
- new_param = np.clip(new_param, 0, 2 * np.pi)
169
+ population = np.copy(initial_params)
55
170
 
56
- new_params.extend(new_param_set)
171
+ final_params = None
172
+ final_losses = None
57
173
 
58
- return np.array(new_params)
174
+ for curr_iter in range(max_iterations):
175
+ # Evaluate the entire population once
176
+ losses = cost_fn(population)
59
177
 
60
- def compute_parameter_shift_mask(self, n_params):
61
- if self != Optimizer.L_BFGS_B:
62
- raise NotImplementedError
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
- mask_arr = np.arange(0, 2 * n_params, 2)
65
- mask_arr[0] = 1
183
+ # Store the current best results
184
+ final_params = population[best_indices]
185
+ final_losses = losses[best_indices]
66
186
 
67
- binary_matrix = (
68
- (mask_arr[:, np.newaxis] & (1 << np.arange(n_params))) > 0
69
- ).astype(np.float64)
187
+ if callback_fn:
188
+ callback_fn(OptimizeResult(x=final_params, fun=final_losses))
70
189
 
71
- binary_matrix = binary_matrix.repeat(2, axis=0)
72
- binary_matrix[1::2] *= -1
73
- binary_matrix *= 0.5 * np.pi
190
+ # Generate the next generation of parameters
191
+ population = self._compute_new_parameters(
192
+ population, curr_iter, best_indices, rng
193
+ )
74
194
 
75
- return binary_matrix
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
+ )