westquant-qoolqit 1.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- westquant_qoolqit/__init__.py +27 -0
- westquant_qoolqit/benchmarks.py +150 -0
- westquant_qoolqit/combined.py +544 -0
- westquant_qoolqit/common/__init__.py +38 -0
- westquant_qoolqit/common/anova.py +140 -0
- westquant_qoolqit/common/equivalence.py +202 -0
- westquant_qoolqit/common/exact_solver.py +89 -0
- westquant_qoolqit/common/metrics.py +223 -0
- westquant_qoolqit/common/pareto.py +78 -0
- westquant_qoolqit/common/qoolqit_adapter.py +542 -0
- westquant_qoolqit/common/reproducibility.py +81 -0
- westquant_qoolqit/common/serialization.py +46 -0
- westquant_qoolqit/common/types.py +190 -0
- westquant_qoolqit/generate_notebooks.py +404 -0
- westquant_qoolqit/generate_slides.py +303 -0
- westquant_qoolqit/hamiltonian_explorer/__init__.py +22 -0
- westquant_qoolqit/hamiltonian_explorer/explorer.py +98 -0
- westquant_qoolqit/hamiltonian_explorer/generator.py +104 -0
- westquant_qoolqit/hamiltonian_explorer/realizability.py +77 -0
- westquant_qoolqit/hamiltonian_explorer/result.py +50 -0
- westquant_qoolqit/hamiltonian_explorer/scoring.py +44 -0
- westquant_qoolqit/hamiltonian_explorer/transforms.py +262 -0
- westquant_qoolqit/representation_scheduler/__init__.py +23 -0
- westquant_qoolqit/representation_scheduler/candidates.py +32 -0
- westquant_qoolqit/representation_scheduler/embedders.py +126 -0
- westquant_qoolqit/representation_scheduler/proposer.py +100 -0
- westquant_qoolqit/representation_scheduler/result.py +74 -0
- westquant_qoolqit/representation_scheduler/robustness.py +35 -0
- westquant_qoolqit/representation_scheduler/scheduler.py +204 -0
- westquant_qoolqit/representation_scheduler/scoring.py +82 -0
- westquant_qoolqit/representation_scheduler/successive_halving.py +40 -0
- westquant_qoolqit/run_benchmarks.py +425 -0
- westquant_qoolqit/validate_submission.py +350 -0
- westquant_qoolqit-1.2.0.dist-info/METADATA +564 -0
- westquant_qoolqit-1.2.0.dist-info/RECORD +39 -0
- westquant_qoolqit-1.2.0.dist-info/WHEEL +5 -0
- westquant_qoolqit-1.2.0.dist-info/licenses/AUTHORS.md +30 -0
- westquant_qoolqit-1.2.0.dist-info/licenses/LICENSE +201 -0
- westquant_qoolqit-1.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""WestQuant Representation Stack for QoolQit.
|
|
2
|
+
|
|
3
|
+
Two composable open-source contributions for analog neutral-atom quantum
|
|
4
|
+
programming, built on Pasqal's QoolQit framework:
|
|
5
|
+
|
|
6
|
+
* WestQuant Representation Scheduler (Project A) -- search over physical
|
|
7
|
+
embeddings/realizations for a fixed logical Hamiltonian.
|
|
8
|
+
* WestQuant Hamiltonian Representation Explorer (Project B) -- search over
|
|
9
|
+
mathematically valid Hamiltonian representations of the same problem.
|
|
10
|
+
|
|
11
|
+
Verified against qoolqit 1.4.0.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .common import (
|
|
15
|
+
BinaryQuadraticHamiltonian,
|
|
16
|
+
QOOLQIT_VERSION,
|
|
17
|
+
capture_environment,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__version__ = "1.2.0"
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"BinaryQuadraticHamiltonian",
|
|
24
|
+
"QOOLQIT_VERSION",
|
|
25
|
+
"capture_environment",
|
|
26
|
+
"__version__",
|
|
27
|
+
]
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Benchmark problem generators: QUBO, MWIS, and a small industrial example."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from .common.types import BinaryQuadraticHamiltonian
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# --------------------------------------------------------------------------- #
|
|
11
|
+
# QUBO
|
|
12
|
+
# --------------------------------------------------------------------------- #
|
|
13
|
+
def synthetic_qubo(n: int, density: float = 0.5, seed: int = 0,
|
|
14
|
+
value_range: tuple[float, float] = (-2.0, 2.0)) -> BinaryQuadraticHamiltonian:
|
|
15
|
+
"""Random symmetric QUBO with given density of nonzero off-diagonal entries."""
|
|
16
|
+
rng = np.random.default_rng(seed)
|
|
17
|
+
Q = np.zeros((n, n))
|
|
18
|
+
for i in range(n):
|
|
19
|
+
for j in range(i + 1, n):
|
|
20
|
+
if rng.random() < density:
|
|
21
|
+
v = rng.uniform(*value_range)
|
|
22
|
+
Q[i, j] = v
|
|
23
|
+
Q[j, i] = v
|
|
24
|
+
np.fill_diagonal(Q, rng.uniform(*value_range, size=n))
|
|
25
|
+
h = BinaryQuadraticHamiltonian.from_qubo(Q)
|
|
26
|
+
h.metadata = {"type": "qubo", "n": n, "density": density, "seed": seed}
|
|
27
|
+
return h
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# --------------------------------------------------------------------------- #
|
|
31
|
+
# MWIS
|
|
32
|
+
# --------------------------------------------------------------------------- #
|
|
33
|
+
def mwis_from_graph(weights: np.ndarray, edges: list[tuple[int, int]],
|
|
34
|
+
U: float | None = None) -> BinaryQuadraticHamiltonian:
|
|
35
|
+
"""Build the MWIS penalty Hamiltonian E_U = -w.x + U sum x_i x_j."""
|
|
36
|
+
weights = np.asarray(weights, dtype=float).ravel()
|
|
37
|
+
if U is None:
|
|
38
|
+
U = float(weights.max()) + 1.0
|
|
39
|
+
n = weights.shape[0]
|
|
40
|
+
linear = -weights.copy()
|
|
41
|
+
quadratic = np.zeros((n, n))
|
|
42
|
+
for i, j in edges:
|
|
43
|
+
i, j = (i, j) if i < j else (j, i)
|
|
44
|
+
quadratic[i, j] = U
|
|
45
|
+
quadratic[j, i] = U
|
|
46
|
+
h = BinaryQuadraticHamiltonian(linear=linear, quadratic=quadratic, constant=0.0,
|
|
47
|
+
metadata={"type": "mwis", "U": U,
|
|
48
|
+
"weights": weights.tolist(),
|
|
49
|
+
"edges": [tuple(e) for e in edges]})
|
|
50
|
+
return h
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def mwis_path(n: int, weights: np.ndarray | None = None, seed: int = 0,
|
|
54
|
+
U: float | None = None) -> tuple[BinaryQuadraticHamiltonian, dict]:
|
|
55
|
+
weights = weights if weights is not None else np.random.default_rng(seed).uniform(1, 5, size=n)
|
|
56
|
+
edges = [(i, i + 1) for i in range(n - 1)]
|
|
57
|
+
h = mwis_from_graph(weights, edges, U=U)
|
|
58
|
+
return h, {"weights": weights.tolist(), "edges": edges}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def mwis_cycle(n: int, seed: int = 0, U: float | None = None):
|
|
62
|
+
weights = np.random.default_rng(seed).uniform(1, 5, size=n)
|
|
63
|
+
edges = [(i, (i + 1) % n) for i in range(n)]
|
|
64
|
+
return mwis_from_graph(weights, edges, U=U), {"weights": weights.tolist(), "edges": edges}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def mwis_grid(rows: int, cols: int, seed: int = 0, U: float | None = None):
|
|
68
|
+
n = rows * cols
|
|
69
|
+
weights = np.random.default_rng(seed).uniform(1, 5, size=n)
|
|
70
|
+
edges = []
|
|
71
|
+
for r in range(rows):
|
|
72
|
+
for c in range(cols):
|
|
73
|
+
i = r * cols + c
|
|
74
|
+
if c + 1 < cols:
|
|
75
|
+
edges.append((i, i + 1))
|
|
76
|
+
if r + 1 < rows:
|
|
77
|
+
edges.append((i, i + cols))
|
|
78
|
+
return mwis_from_graph(weights, edges, U=U), {"weights": weights.tolist(), "edges": edges}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def mwis_random_geometric(n: int, radius: float = 0.7, seed: int = 0, U: float | None = None):
|
|
82
|
+
rng = np.random.default_rng(seed)
|
|
83
|
+
coords = rng.uniform(0, 1, size=(n, 2))
|
|
84
|
+
edges = []
|
|
85
|
+
for i in range(n):
|
|
86
|
+
for j in range(i + 1, n):
|
|
87
|
+
if np.linalg.norm(coords[i] - coords[j]) < radius:
|
|
88
|
+
edges.append((i, j))
|
|
89
|
+
weights = rng.uniform(1, 5, size=n)
|
|
90
|
+
return mwis_from_graph(weights, edges, U=U), {"weights": weights.tolist(), "edges": edges}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def mwis_erdos_renyi(n: int, p: float = 0.4, seed: int = 0, U: float | None = None):
|
|
94
|
+
rng = np.random.default_rng(seed)
|
|
95
|
+
edges = []
|
|
96
|
+
for i in range(n):
|
|
97
|
+
for j in range(i + 1, n):
|
|
98
|
+
if rng.random() < p:
|
|
99
|
+
edges.append((i, j))
|
|
100
|
+
weights = rng.uniform(1, 5, size=n)
|
|
101
|
+
return mwis_from_graph(weights, edges, U=U), {"weights": weights.tolist(), "edges": edges}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# --------------------------------------------------------------------------- #
|
|
105
|
+
# Small industrial example: shift assignment (scheduling)
|
|
106
|
+
# --------------------------------------------------------------------------- #
|
|
107
|
+
def shift_assignment(n_workers: int = 4, n_shifts: int = 3, seed: int = 0):
|
|
108
|
+
"""Small scheduling QUBO: assign workers to shifts minimizing conflicts.
|
|
109
|
+
|
|
110
|
+
Variables x_{w,s} = 1 if worker w assigned to shift s. Constraints:
|
|
111
|
+
- each worker assigned to exactly one shift (one-hot)
|
|
112
|
+
- minimize a soft cost (random preferences).
|
|
113
|
+
|
|
114
|
+
The one-hot penalty is P * (sum_s x_{w,s} - 1)^2 which expands to:
|
|
115
|
+
P * (sum_s x_{w,s}^2 + 2*sum_{s<s'} x_{w,s} x_{w,s'} - 2*sum_s x_{w,s} + 1)
|
|
116
|
+
|
|
117
|
+
Since x^2 = x for binary variables, the diagonal contribution is:
|
|
118
|
+
P * (1 - 2) = -P per variable (from the -2*sum term)
|
|
119
|
+
The off-diagonal contribution is:
|
|
120
|
+
2*P per pair (from the 2*sum_{s<s'} term)
|
|
121
|
+
The constant P is dropped (doesn't affect optimization).
|
|
122
|
+
|
|
123
|
+
Encoded as a QUBO small enough for exact enumeration.
|
|
124
|
+
"""
|
|
125
|
+
rng = np.random.default_rng(seed)
|
|
126
|
+
n = n_workers * n_shifts
|
|
127
|
+
Q = np.zeros((n, n))
|
|
128
|
+
P = 5.0
|
|
129
|
+
for w in range(n_workers):
|
|
130
|
+
idxs = [w * n_shifts + s for s in range(n_shifts)]
|
|
131
|
+
# One-hot penalty: P * (sum - 1)^2
|
|
132
|
+
for a in idxs:
|
|
133
|
+
Q[a, a] += -2 * P # from -2*P*sum term
|
|
134
|
+
for a in idxs:
|
|
135
|
+
for b in idxs:
|
|
136
|
+
if a != b:
|
|
137
|
+
Q[a, b] += P # from P*sum_{s<s'} term (symmetric, so each pair gets P)
|
|
138
|
+
# Soft preference cost on diagonal
|
|
139
|
+
for a in idxs:
|
|
140
|
+
Q[a, a] += rng.uniform(-1, 1)
|
|
141
|
+
h = BinaryQuadraticHamiltonian.from_qubo(Q)
|
|
142
|
+
h.metadata = {"type": "scheduling", "n_workers": n_workers,
|
|
143
|
+
"n_shifts": n_shifts, "seed": seed}
|
|
144
|
+
return h
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
__all__ = [
|
|
148
|
+
"synthetic_qubo", "mwis_from_graph", "mwis_path", "mwis_cycle", "mwis_grid",
|
|
149
|
+
"mwis_random_geometric", "mwis_erdos_renyi", "shift_assignment",
|
|
150
|
+
]
|