ascent-science-sqa 0.1.0__tar.gz

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.
@@ -0,0 +1,68 @@
1
+ Metadata-Version: 2.4
2
+ Name: ascent-science-sqa
3
+ Version: 0.1.0
4
+ Summary: Path-integral simulated quantum annealing for the Ascent Science SDK
5
+ Author: Ascent Science
6
+ License-Expression: LicenseRef-Proprietary
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3 :: Only
9
+ Classifier: Typing :: Typed
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: ascent-science<0.10,>=0.9.4
13
+
14
+ # Ascent Science SQA plugin
15
+
16
+ This package implements simulated quantum annealing (SQA) with the standard
17
+ Suzuki--Trotter path-integral mapping and single-spin Metropolis updates. It is
18
+ a classical simulation of a transverse-field Ising model; it does not use a
19
+ quantum processor.
20
+
21
+ For development from this repository, install the core SDK and plugin in editable mode:
22
+
23
+ ```bash
24
+ python -m pip install -e ./sdk-python -e ./sdk-plugins/sqa
25
+ ```
26
+
27
+ After the `0.1.0` package is published, it can instead be installed with
28
+ `python -m pip install ascent-science-sqa==0.1.0`.
29
+
30
+ ```python
31
+ import ascent
32
+
33
+ solver = ascent.quantum.SimulatedQuantumAnnealing(
34
+ trotter_slices=32,
35
+ sweeps=500,
36
+ beta=5.0,
37
+ gamma_start=5.0,
38
+ gamma_end=0.01,
39
+ seed=42,
40
+ )
41
+
42
+ # Minimize x.T Q x for x in {0, 1}^n.
43
+ result = solver.solve_qubo([
44
+ [-2.0, 1.0],
45
+ [1.0, -2.0],
46
+ ])
47
+ print(result.best_solution, result.best_value)
48
+ ```
49
+
50
+ For an Ising model, `solve_ising(J, fields=h)` minimizes
51
+ `s.T @ J @ s + h.T @ s + offset`, with spins in `{-1, +1}`. For a canonical
52
+ problem object use `solver.solve(QuboProblem(...))` or
53
+ `solver.solve(IsingProblem(...))`.
54
+
55
+ SQA is stochastic and does not prove optimality. Results therefore have status
56
+ `FEASIBLE` (or `TIME_LIMIT` if a supplied `SolveOptions` limit is reached).
57
+
58
+ ## Build a release
59
+
60
+ Use the same release flow as the ACO plugin:
61
+
62
+ ```bash
63
+ sh scripts/build-release.sh
64
+ ```
65
+
66
+ The wheel, source archive, and `SHA256SUMS` are written to `dist/`. If the
67
+ default Python does not provide a recent setuptools/build installation, select
68
+ another interpreter with `ASCENT_SQA_BUILD_PYTHON=/path/to/python`.
@@ -0,0 +1,55 @@
1
+ # Ascent Science SQA plugin
2
+
3
+ This package implements simulated quantum annealing (SQA) with the standard
4
+ Suzuki--Trotter path-integral mapping and single-spin Metropolis updates. It is
5
+ a classical simulation of a transverse-field Ising model; it does not use a
6
+ quantum processor.
7
+
8
+ For development from this repository, install the core SDK and plugin in editable mode:
9
+
10
+ ```bash
11
+ python -m pip install -e ./sdk-python -e ./sdk-plugins/sqa
12
+ ```
13
+
14
+ After the `0.1.0` package is published, it can instead be installed with
15
+ `python -m pip install ascent-science-sqa==0.1.0`.
16
+
17
+ ```python
18
+ import ascent
19
+
20
+ solver = ascent.quantum.SimulatedQuantumAnnealing(
21
+ trotter_slices=32,
22
+ sweeps=500,
23
+ beta=5.0,
24
+ gamma_start=5.0,
25
+ gamma_end=0.01,
26
+ seed=42,
27
+ )
28
+
29
+ # Minimize x.T Q x for x in {0, 1}^n.
30
+ result = solver.solve_qubo([
31
+ [-2.0, 1.0],
32
+ [1.0, -2.0],
33
+ ])
34
+ print(result.best_solution, result.best_value)
35
+ ```
36
+
37
+ For an Ising model, `solve_ising(J, fields=h)` minimizes
38
+ `s.T @ J @ s + h.T @ s + offset`, with spins in `{-1, +1}`. For a canonical
39
+ problem object use `solver.solve(QuboProblem(...))` or
40
+ `solver.solve(IsingProblem(...))`.
41
+
42
+ SQA is stochastic and does not prove optimality. Results therefore have status
43
+ `FEASIBLE` (or `TIME_LIMIT` if a supplied `SolveOptions` limit is reached).
44
+
45
+ ## Build a release
46
+
47
+ Use the same release flow as the ACO plugin:
48
+
49
+ ```bash
50
+ sh scripts/build-release.sh
51
+ ```
52
+
53
+ The wheel, source archive, and `SHA256SUMS` are written to `dist/`. If the
54
+ default Python does not provide a recent setuptools/build installation, select
55
+ another interpreter with `ASCENT_SQA_BUILD_PYTHON=/path/to/python`.
@@ -0,0 +1,25 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ascent-science-sqa"
7
+ version = "0.1.0"
8
+ description = "Path-integral simulated quantum annealing for the Ascent Science SDK"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "LicenseRef-Proprietary"
12
+ dependencies = ["ascent-science>=0.9.4,<0.10"]
13
+ authors = [{name = "Ascent Science"}]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3 :: Only",
17
+ "Typing :: Typed",
18
+ ]
19
+
20
+ [project.entry-points."ascent.algorithms"]
21
+ sqa = "ascent_science_sqa:register"
22
+
23
+ [tool.setuptools.packages.find]
24
+ where = ["src"]
25
+ include = ["ascent_science_sqa*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,293 @@
1
+ """Suzuki--Trotter path-integral simulated quantum annealing.
2
+
3
+ This is a classical Monte Carlo simulation of a transverse-field Ising model.
4
+ It does not submit work to quantum hardware.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import math
10
+ import random
11
+ import time
12
+
13
+ from ascent.core import (
14
+ AlgorithmSpec,
15
+ ExecutionInfo,
16
+ IsingProblem,
17
+ QuboProblem,
18
+ SolveOptions,
19
+ SolveResult,
20
+ SolveStatus,
21
+ SolverIdentity,
22
+ qubo_to_ising,
23
+ symmetrize,
24
+ )
25
+ from ascent.plugin_api import register_algorithm, validated_license
26
+ from ascent.usage import tracked_call
27
+
28
+ __version__ = "0.1.0"
29
+
30
+
31
+ def _finite_vector(values, size, name):
32
+ try:
33
+ vector = [float(value) for value in values]
34
+ except (TypeError, ValueError) as error:
35
+ raise ValueError(f"{name} must contain finite numbers") from error
36
+ if len(vector) != size or any(not math.isfinite(value) for value in vector):
37
+ raise ValueError(f"{name} must contain one finite value per variable")
38
+ return vector
39
+
40
+
41
+ class SimulatedQuantumAnnealing:
42
+ """Minimize Ising or QUBO energy using standard path-integral SQA.
43
+
44
+ ``trotter_slices`` controls the Suzuki--Trotter discretization. ``beta`` is
45
+ the inverse physical temperature and ``gamma_start``/``gamma_end`` define
46
+ the transverse-field schedule. A result is heuristic and is therefore
47
+ reported as ``FEASIBLE``, never as a certified optimum.
48
+ """
49
+
50
+ def __init__(
51
+ self,
52
+ *,
53
+ trotter_slices=16,
54
+ sweeps=200,
55
+ beta=5.0,
56
+ gamma_start=5.0,
57
+ gamma_end=0.01,
58
+ schedule="geometric",
59
+ seed=None,
60
+ user_id=None,
61
+ sdk_code=None,
62
+ endpoint=None,
63
+ license_path=None,
64
+ timeout=30.0,
65
+ report_usage=True,
66
+ validation_mode="cached",
67
+ **unused,
68
+ ):
69
+ if unused:
70
+ raise TypeError("unsupported SQA parameters: %s" % ", ".join(sorted(unused)))
71
+ self.trotter_slices = int(trotter_slices)
72
+ self.sweeps = int(sweeps)
73
+ self.beta = float(beta)
74
+ self.gamma_start = float(gamma_start)
75
+ self.gamma_end = float(gamma_end)
76
+ self.schedule = str(schedule).lower()
77
+ self.seed = seed
78
+ self._validate_parameters()
79
+ self.endpoint, self.license_user_id, self._usage_reporter = validated_license(
80
+ endpoint, user_id, sdk_code, license_path, timeout, report_usage, validation_mode
81
+ )
82
+
83
+ def _validate_parameters(self):
84
+ if self.trotter_slices < 2:
85
+ raise ValueError("trotter_slices must be at least 2")
86
+ if self.sweeps < 1:
87
+ raise ValueError("sweeps must be a positive integer")
88
+ if not math.isfinite(self.beta) or self.beta <= 0:
89
+ raise ValueError("beta must be a finite positive number")
90
+ if not math.isfinite(self.gamma_start) or self.gamma_start <= 0:
91
+ raise ValueError("gamma_start must be a finite positive number")
92
+ if not math.isfinite(self.gamma_end) or self.gamma_end <= 0:
93
+ raise ValueError("gamma_end must be a finite positive number")
94
+ if self.gamma_start < self.gamma_end:
95
+ raise ValueError("gamma_start must be greater than or equal to gamma_end")
96
+ if self.schedule not in {"linear", "geometric"}:
97
+ raise ValueError("schedule must be 'linear' or 'geometric'")
98
+
99
+ def solve(self, problem, *, options=None):
100
+ if options is not None and not isinstance(options, SolveOptions):
101
+ raise TypeError("options must be a SolveOptions instance")
102
+ if isinstance(problem, QuboProblem):
103
+ return self.solve_qubo(
104
+ problem.matrix,
105
+ offset=float(problem.metadata.get("offset", 0.0)),
106
+ options=options,
107
+ )
108
+ if isinstance(problem, IsingProblem):
109
+ return self.solve_ising(
110
+ problem.matrix,
111
+ fields=problem.metadata.get("fields"),
112
+ offset=float(problem.metadata.get("offset", 0.0)),
113
+ options=options,
114
+ )
115
+ raise TypeError("problem must be a QuboProblem or IsingProblem")
116
+
117
+ def solve_qubo(self, qubo, *, offset=0.0, initial_state=None, options=None):
118
+ coefficients = qubo_to_ising(qubo)
119
+ initial_spins = None
120
+ if initial_state is not None:
121
+ try:
122
+ bits = list(initial_state)
123
+ except TypeError as error:
124
+ raise ValueError("initial_state must contain one 0 or 1 value per variable") from error
125
+ if any(bit not in (0, 1) for bit in bits):
126
+ raise ValueError("initial_state must contain one 0 or 1 value per variable")
127
+ initial_spins = [2 * int(bit) - 1 for bit in bits]
128
+
129
+ def execute():
130
+ result = self._solve_ising(
131
+ coefficients.couplings,
132
+ coefficients.fields,
133
+ float(offset) + coefficients.offset,
134
+ initial_state=initial_spins,
135
+ options=options,
136
+ )
137
+ spins = result.best_solution
138
+ bits = [(spin + 1) // 2 for spin in spins]
139
+ metrics = dict(result.metrics)
140
+ metrics["best_spins"] = spins
141
+ return SolveResult(
142
+ status=result.status,
143
+ best_solution=bits,
144
+ best_value=result.best_value,
145
+ solver=result.solver,
146
+ runtime_seconds=result.runtime_seconds,
147
+ history=result.history,
148
+ metrics=metrics,
149
+ execution=result.execution,
150
+ )
151
+
152
+ return tracked_call(self._usage_reporter, "sqa", "local", execute)
153
+
154
+ def solve_ising(self, couplings, *, fields=None, offset=0.0, initial_state=None, options=None):
155
+ return tracked_call(
156
+ self._usage_reporter,
157
+ "sqa",
158
+ "local",
159
+ lambda: self._solve_ising(couplings, fields, offset, initial_state=initial_state, options=options),
160
+ )
161
+
162
+ def _gamma(self, step):
163
+ if self.sweeps == 1:
164
+ return self.gamma_end
165
+ fraction = step / (self.sweeps - 1)
166
+ if self.schedule == "linear":
167
+ return self.gamma_start + fraction * (self.gamma_end - self.gamma_start)
168
+ return self.gamma_start * (self.gamma_end / self.gamma_start) ** fraction
169
+
170
+ def _solve_ising(self, couplings, fields, offset, *, initial_state, options):
171
+ if options is not None and not isinstance(options, SolveOptions):
172
+ raise TypeError("options must be a SolveOptions instance")
173
+ started = time.perf_counter()
174
+ matrix = symmetrize(couplings)
175
+ size = len(matrix)
176
+ linear = [0.0] * size if fields is None else _finite_vector(fields, size, "fields")
177
+ offset = float(offset)
178
+ if not math.isfinite(offset):
179
+ raise ValueError("offset must be finite")
180
+ if initial_state is not None:
181
+ initial_state = [int(value) for value in initial_state]
182
+ if len(initial_state) != size or any(value not in {-1, 1} for value in initial_state):
183
+ raise ValueError("initial_state must contain one -1 or +1 value per variable")
184
+
185
+ seed = self.seed if options is None or options.random_seed is None else options.random_seed
186
+ rng = random.Random(seed)
187
+ replicas = [
188
+ list(initial_state) if initial_state is not None else [rng.choice((-1, 1)) for _ in range(size)]
189
+ for _ in range(self.trotter_slices)
190
+ ]
191
+
192
+ def classical_energy(spins):
193
+ return offset + sum(linear[i] * spins[i] for i in range(size)) + sum(
194
+ matrix[i][j] * spins[i] * spins[j]
195
+ for i in range(size)
196
+ for j in range(size)
197
+ )
198
+
199
+ energies = [classical_energy(replica) for replica in replicas]
200
+ best_index = min(range(self.trotter_slices), key=energies.__getitem__)
201
+ best_spins = replicas[best_index][:]
202
+ best_value = energies[best_index]
203
+ history = [best_value]
204
+ accepted = attempted = 0
205
+ deadline = None
206
+ if options is not None and options.time_limit_seconds is not None:
207
+ deadline = started + options.time_limit_seconds
208
+
209
+ completed_sweeps = 0
210
+ timed_out = False
211
+ for step in range(self.sweeps):
212
+ gamma = self._gamma(step)
213
+ argument = self.beta * gamma / self.trotter_slices
214
+ temporal_coupling = -0.5 * math.log(max(math.tanh(argument), 1e-300))
215
+ updates = [(slice_index, spin_index) for slice_index in range(self.trotter_slices) for spin_index in range(size)]
216
+ rng.shuffle(updates)
217
+ for slice_index, spin_index in updates:
218
+ if deadline is not None and time.perf_counter() >= deadline:
219
+ timed_out = True
220
+ break
221
+ spins = replicas[slice_index]
222
+ spin = spins[spin_index]
223
+ local_field = linear[spin_index] + 2.0 * sum(
224
+ matrix[spin_index][other] * spins[other]
225
+ for other in range(size)
226
+ if other != spin_index
227
+ )
228
+ delta_energy = -2.0 * spin * local_field
229
+ previous_spin = replicas[(slice_index - 1) % self.trotter_slices][spin_index]
230
+ next_spin = replicas[(slice_index + 1) % self.trotter_slices][spin_index]
231
+ delta_action = (
232
+ self.beta * delta_energy / self.trotter_slices
233
+ + 2.0 * temporal_coupling * spin * (previous_spin + next_spin)
234
+ )
235
+ attempted += 1
236
+ if delta_action <= 0.0 or rng.random() < math.exp(-min(delta_action, 745.0)):
237
+ spins[spin_index] = -spin
238
+ energies[slice_index] += delta_energy
239
+ accepted += 1
240
+ if energies[slice_index] < best_value:
241
+ best_value = energies[slice_index]
242
+ best_spins = spins[:]
243
+ if timed_out:
244
+ break
245
+ completed_sweeps += 1
246
+ history.append(best_value)
247
+
248
+ return SolveResult(
249
+ status=SolveStatus.TIME_LIMIT if timed_out else SolveStatus.FEASIBLE,
250
+ best_solution=best_spins,
251
+ best_value=float(best_value),
252
+ solver=SolverIdentity(
253
+ "SimulatedQuantumAnnealing",
254
+ f"ascent-science-sqa {__version__}",
255
+ "Suzuki-Trotter Monte Carlo",
256
+ ),
257
+ runtime_seconds=time.perf_counter() - started,
258
+ history=tuple(history),
259
+ metrics={
260
+ "trotter_slices": self.trotter_slices,
261
+ "completed_sweeps": completed_sweeps,
262
+ "beta": self.beta,
263
+ "gamma_start": self.gamma_start,
264
+ "gamma_end": self.gamma_end,
265
+ "schedule": self.schedule,
266
+ "accepted_moves": accepted,
267
+ "attempted_moves": attempted,
268
+ "acceptance_rate": accepted / attempted if attempted else 0.0,
269
+ },
270
+ execution=ExecutionInfo(location="local", backend="path-integral-monte-carlo"),
271
+ )
272
+
273
+
274
+ def register():
275
+ return register_algorithm(
276
+ category="quantum",
277
+ exports={"SimulatedQuantumAnnealing": SimulatedQuantumAnnealing},
278
+ specification=AlgorithmSpec(
279
+ name="sqa",
280
+ family="quantum",
281
+ problem_types=("qubo", "ising"),
282
+ input_kind="qubo_or_ising",
283
+ output_kind="binary_or_spin",
284
+ factory=SimulatedQuantumAnnealing,
285
+ deterministic=False,
286
+ supports_initial_solution=True,
287
+ aliases=("simulated_quantum_annealing",),
288
+ description="Suzuki-Trotter path-integral simulated quantum annealing.",
289
+ ),
290
+ )
291
+
292
+
293
+ __all__ = ["SimulatedQuantumAnnealing", "register"]
@@ -0,0 +1,68 @@
1
+ Metadata-Version: 2.4
2
+ Name: ascent-science-sqa
3
+ Version: 0.1.0
4
+ Summary: Path-integral simulated quantum annealing for the Ascent Science SDK
5
+ Author: Ascent Science
6
+ License-Expression: LicenseRef-Proprietary
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3 :: Only
9
+ Classifier: Typing :: Typed
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: ascent-science<0.10,>=0.9.4
13
+
14
+ # Ascent Science SQA plugin
15
+
16
+ This package implements simulated quantum annealing (SQA) with the standard
17
+ Suzuki--Trotter path-integral mapping and single-spin Metropolis updates. It is
18
+ a classical simulation of a transverse-field Ising model; it does not use a
19
+ quantum processor.
20
+
21
+ For development from this repository, install the core SDK and plugin in editable mode:
22
+
23
+ ```bash
24
+ python -m pip install -e ./sdk-python -e ./sdk-plugins/sqa
25
+ ```
26
+
27
+ After the `0.1.0` package is published, it can instead be installed with
28
+ `python -m pip install ascent-science-sqa==0.1.0`.
29
+
30
+ ```python
31
+ import ascent
32
+
33
+ solver = ascent.quantum.SimulatedQuantumAnnealing(
34
+ trotter_slices=32,
35
+ sweeps=500,
36
+ beta=5.0,
37
+ gamma_start=5.0,
38
+ gamma_end=0.01,
39
+ seed=42,
40
+ )
41
+
42
+ # Minimize x.T Q x for x in {0, 1}^n.
43
+ result = solver.solve_qubo([
44
+ [-2.0, 1.0],
45
+ [1.0, -2.0],
46
+ ])
47
+ print(result.best_solution, result.best_value)
48
+ ```
49
+
50
+ For an Ising model, `solve_ising(J, fields=h)` minimizes
51
+ `s.T @ J @ s + h.T @ s + offset`, with spins in `{-1, +1}`. For a canonical
52
+ problem object use `solver.solve(QuboProblem(...))` or
53
+ `solver.solve(IsingProblem(...))`.
54
+
55
+ SQA is stochastic and does not prove optimality. Results therefore have status
56
+ `FEASIBLE` (or `TIME_LIMIT` if a supplied `SolveOptions` limit is reached).
57
+
58
+ ## Build a release
59
+
60
+ Use the same release flow as the ACO plugin:
61
+
62
+ ```bash
63
+ sh scripts/build-release.sh
64
+ ```
65
+
66
+ The wheel, source archive, and `SHA256SUMS` are written to `dist/`. If the
67
+ default Python does not provide a recent setuptools/build installation, select
68
+ another interpreter with `ASCENT_SQA_BUILD_PYTHON=/path/to/python`.
@@ -0,0 +1,9 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/ascent_science_sqa/__init__.py
4
+ src/ascent_science_sqa.egg-info/PKG-INFO
5
+ src/ascent_science_sqa.egg-info/SOURCES.txt
6
+ src/ascent_science_sqa.egg-info/dependency_links.txt
7
+ src/ascent_science_sqa.egg-info/entry_points.txt
8
+ src/ascent_science_sqa.egg-info/requires.txt
9
+ src/ascent_science_sqa.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [ascent.algorithms]
2
+ sqa = ascent_science_sqa:register
@@ -0,0 +1 @@
1
+ ascent-science<0.10,>=0.9.4
@@ -0,0 +1 @@
1
+ ascent_science_sqa