catalyst-q 0.1.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.
@@ -0,0 +1,18 @@
1
+ from .client import CatalystClient, CatalystQClient, PreparedRequest, execute_prepared_request
2
+ from .cloudflare import CloudflareDeploymentSpec, RenderedDeployment
3
+ from .hmk import HypervectorMemoryKey
4
+ from .identity import InstallIdentity
5
+ from .licensing import FreemiumLimitError, UsagePolicy
6
+
7
+ __all__ = [
8
+ "CatalystClient",
9
+ "CatalystQClient",
10
+ "CloudflareDeploymentSpec",
11
+ "FreemiumLimitError",
12
+ "HypervectorMemoryKey",
13
+ "InstallIdentity",
14
+ "PreparedRequest",
15
+ "RenderedDeployment",
16
+ "UsagePolicy",
17
+ "execute_prepared_request",
18
+ ]
catalyst_hmk/client.py ADDED
@@ -0,0 +1,440 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import re
6
+ import time
7
+ from dataclasses import dataclass
8
+ from typing import Any, Callable, Dict, List, Optional, Tuple, Union
9
+ from urllib import error, request
10
+
11
+ from .hmk import HypervectorMemoryKey
12
+ from .identity import InstallIdentity
13
+ from .licensing import UsagePolicy
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class PreparedRequest:
18
+ method: str
19
+ url: str
20
+ headers: Dict[str, str]
21
+ json: Dict[str, Any]
22
+
23
+
24
+ Transport = Callable[[PreparedRequest, float], Tuple[int, Dict[str, str], bytes]]
25
+
26
+
27
+ class CatalystClient:
28
+ def __init__(
29
+ self,
30
+ api_key: Optional[str] = None,
31
+ base_url: str = "https://api.strategic-innovations.ai/v3turbo",
32
+ control_plane_url: str = "https://catalyst-q-sdk.strategic-innovations.workers.dev",
33
+ policy: Optional[UsagePolicy] = None,
34
+ install_identity: Optional[InstallIdentity] = None,
35
+ ) -> None:
36
+ self.api_key = api_key
37
+ self.base_url = base_url.rstrip("/")
38
+ self.control_plane_url = control_plane_url.rstrip("/")
39
+ self.policy = policy or UsagePolicy.free()
40
+ self.install_identity = install_identity
41
+
42
+ def prepare_execute_request(
43
+ self,
44
+ circuit: List[Dict[str, Any]],
45
+ hmk: HypervectorMemoryKey,
46
+ calls_this_month: int,
47
+ ) -> PreparedRequest:
48
+ return self.prepare_execute(circuit, hmk=hmk, calls_this_month=calls_this_month)
49
+
50
+ def prepare_execute(
51
+ self,
52
+ circuit: Union[List[Dict[str, Any]], Any],
53
+ hmk: HypervectorMemoryKey,
54
+ calls_this_month: int,
55
+ qubits: Optional[int] = None,
56
+ shots: Optional[int] = None,
57
+ compute_units_this_month: int = 0,
58
+ production: bool = False,
59
+ ) -> PreparedRequest:
60
+ circuit_payload = _circuit_payload(circuit)
61
+ resolved_qubits = qubits or circuit_payload.get("qubits") or _infer_qubits(circuit_payload["circuit"])
62
+ gate_count = max(1, len(circuit_payload["circuit"]))
63
+ requested_compute_units = _circuit_compute_units(resolved_qubits, gate_count, shots)
64
+ body: Dict[str, Any] = {
65
+ "qubits": resolved_qubits,
66
+ "circuit": circuit_payload["circuit"],
67
+ "hmk": hmk.to_wire(),
68
+ "state_mode": "hmk_state_in_payload",
69
+ "registration": self._identity().to_wire(),
70
+ "billing_estimate": {
71
+ "circuit_runs": 1,
72
+ "qubits": resolved_qubits,
73
+ "gate_count": gate_count,
74
+ "shots": shots or 1,
75
+ "compute_units": requested_compute_units,
76
+ },
77
+ }
78
+ if shots is not None:
79
+ body["shots"] = shots
80
+ self.policy.check_circuit_run(
81
+ "execute",
82
+ circuit_runs_this_month=calls_this_month,
83
+ compute_units_this_month=compute_units_this_month,
84
+ qubits=resolved_qubits,
85
+ requested_compute_units=requested_compute_units,
86
+ production=production,
87
+ )
88
+ return self._request("/execute", body)
89
+
90
+ def prepare_qasm(
91
+ self,
92
+ qasm: str,
93
+ hmk: HypervectorMemoryKey,
94
+ calls_this_month: int,
95
+ qubits: Optional[int] = None,
96
+ shots: Optional[int] = None,
97
+ output_format: Optional[str] = None,
98
+ compute_units_this_month: int = 0,
99
+ production: bool = False,
100
+ ) -> PreparedRequest:
101
+ resolved_qubits = qubits or _infer_qasm_qubits(qasm)
102
+ gate_count = max(1, _infer_qasm_gate_count(qasm))
103
+ requested_compute_units = _circuit_compute_units(resolved_qubits, gate_count, shots)
104
+ body: Dict[str, Any] = {
105
+ "qasm": qasm,
106
+ "hmk": hmk.to_wire(),
107
+ "state_mode": "hmk_state_in_payload",
108
+ "registration": self._identity().to_wire(),
109
+ "billing_estimate": {
110
+ "circuit_runs": 1,
111
+ "qubits": resolved_qubits,
112
+ "gate_count": gate_count,
113
+ "shots": shots or 1,
114
+ "compute_units": requested_compute_units,
115
+ },
116
+ }
117
+ if shots is not None:
118
+ body["shots"] = shots
119
+ if output_format is not None:
120
+ body["output_format"] = output_format
121
+ self.policy.check_circuit_run(
122
+ "execute_qasm",
123
+ circuit_runs_this_month=calls_this_month,
124
+ compute_units_this_month=compute_units_this_month,
125
+ qubits=resolved_qubits,
126
+ requested_compute_units=requested_compute_units,
127
+ production=production,
128
+ )
129
+ return self._request("/execute/qasm", body)
130
+
131
+ def prepare_solver(
132
+ self,
133
+ kind: str,
134
+ payload: Dict[str, Any],
135
+ hmk: HypervectorMemoryKey,
136
+ solver_runs_this_month: int,
137
+ compute_units_this_month: int = 0,
138
+ problem_size: Optional[int] = None,
139
+ requested_compute_units: Optional[int] = None,
140
+ production: bool = False,
141
+ ) -> PreparedRequest:
142
+ allowed = {"sat", "tsp", "knapsack", "portfolio"}
143
+ if kind not in allowed:
144
+ raise ValueError(f"solver kind must be one of {sorted(allowed)}")
145
+ resolved_problem_size = problem_size or _infer_problem_size(kind, payload)
146
+ resolved_compute_units = requested_compute_units or _infer_solver_compute_units(
147
+ kind, payload, resolved_problem_size
148
+ )
149
+ body = {
150
+ **payload,
151
+ "hmk": hmk.to_wire(),
152
+ "state_mode": "hmk_state_in_payload",
153
+ "registration": self._identity().to_wire(),
154
+ "billing_estimate": {
155
+ "solver_runs": 1,
156
+ "problem_size": resolved_problem_size,
157
+ "compute_units": resolved_compute_units,
158
+ },
159
+ }
160
+ self.policy.check_solver_run(
161
+ f"solve_{kind}",
162
+ solver_runs_this_month=solver_runs_this_month,
163
+ compute_units_this_month=compute_units_this_month,
164
+ problem_size=resolved_problem_size,
165
+ requested_compute_units=resolved_compute_units,
166
+ production=production,
167
+ )
168
+ return self._request(f"/solve/{kind}", body)
169
+
170
+ def prepare_activation_request(self) -> PreparedRequest:
171
+ identity = self._identity()
172
+ return self._control_request(
173
+ "/v1/installs/activate",
174
+ {
175
+ "install_id": identity.install_id,
176
+ "tier": identity.tier,
177
+ "mode": identity.mode,
178
+ "register_on": identity.register_on,
179
+ "sdk_version": "0.1.0",
180
+ "package": "catalyst-q",
181
+ },
182
+ )
183
+
184
+ def prepare_usage_check_request(
185
+ self,
186
+ operation: str,
187
+ route: str,
188
+ billing_estimate: Dict[str, Any],
189
+ production: bool = False,
190
+ ) -> PreparedRequest:
191
+ identity = self._identity()
192
+ return self._control_request(
193
+ "/v1/usage/check",
194
+ {
195
+ "install_id": identity.install_id,
196
+ "operation": operation,
197
+ "route": route,
198
+ "billing_estimate": billing_estimate,
199
+ "production": production,
200
+ "sdk_version": "0.1.0",
201
+ },
202
+ )
203
+
204
+ def prepare_sat(
205
+ self,
206
+ problem: Any,
207
+ hmk: HypervectorMemoryKey,
208
+ solver_runs_this_month: int,
209
+ compute_units_this_month: int = 0,
210
+ production: bool = False,
211
+ ) -> PreparedRequest:
212
+ return self._prepare_problem("sat", problem, hmk, solver_runs_this_month, compute_units_this_month, production)
213
+
214
+ def prepare_tsp(
215
+ self,
216
+ problem: Any,
217
+ hmk: HypervectorMemoryKey,
218
+ solver_runs_this_month: int,
219
+ compute_units_this_month: int = 0,
220
+ production: bool = False,
221
+ ) -> PreparedRequest:
222
+ return self._prepare_problem("tsp", problem, hmk, solver_runs_this_month, compute_units_this_month, production)
223
+
224
+ def prepare_knapsack(
225
+ self,
226
+ problem: Any,
227
+ hmk: HypervectorMemoryKey,
228
+ solver_runs_this_month: int,
229
+ compute_units_this_month: int = 0,
230
+ production: bool = False,
231
+ ) -> PreparedRequest:
232
+ return self._prepare_problem(
233
+ "knapsack", problem, hmk, solver_runs_this_month, compute_units_this_month, production
234
+ )
235
+
236
+ def prepare_portfolio(
237
+ self,
238
+ problem: Any,
239
+ hmk: HypervectorMemoryKey,
240
+ solver_runs_this_month: int,
241
+ compute_units_this_month: int = 0,
242
+ production: bool = False,
243
+ ) -> PreparedRequest:
244
+ return self._prepare_problem(
245
+ "portfolio", problem, hmk, solver_runs_this_month, compute_units_this_month, production
246
+ )
247
+
248
+ def _prepare_problem(
249
+ self,
250
+ kind: str,
251
+ problem: Any,
252
+ hmk: HypervectorMemoryKey,
253
+ solver_runs_this_month: int,
254
+ compute_units_this_month: int,
255
+ production: bool,
256
+ ) -> PreparedRequest:
257
+ payload = _problem_payload(problem)
258
+ return self.prepare_solver(
259
+ kind,
260
+ payload,
261
+ hmk=hmk,
262
+ solver_runs_this_month=solver_runs_this_month,
263
+ compute_units_this_month=compute_units_this_month,
264
+ problem_size=getattr(problem, "problem_size", None),
265
+ requested_compute_units=getattr(problem, "compute_units", None),
266
+ production=production,
267
+ )
268
+
269
+ def _request(self, path: str, body: Dict[str, Any]) -> PreparedRequest:
270
+ identity = self._identity()
271
+ return PreparedRequest(
272
+ method="POST",
273
+ url=f"{self.base_url}{path}",
274
+ headers={
275
+ "Authorization": f"Bearer {self.api_key or 'catalyst-free-anonymous'}",
276
+ "Content-Type": "application/json",
277
+ "User-Agent": "catalyst-q/0.1.0",
278
+ "X-Catalyst-SDK": "catalyst-q/0.1.0",
279
+ "X-Catalyst-API": "catalyst-q",
280
+ "X-Catalyst-Install-ID": identity.install_id,
281
+ "X-Catalyst-Install-Tier": identity.tier,
282
+ "X-Catalyst-Account-Mode": identity.mode,
283
+ },
284
+ json=body,
285
+ )
286
+
287
+ def _control_request(self, path: str, body: Dict[str, Any]) -> PreparedRequest:
288
+ identity = self._identity()
289
+ return PreparedRequest(
290
+ method="POST",
291
+ url=f"{self.control_plane_url}{path}",
292
+ headers={
293
+ "Authorization": f"Bearer {self.api_key or 'catalyst-free-anonymous'}",
294
+ "Content-Type": "application/json",
295
+ "User-Agent": "catalyst-q/0.1.0",
296
+ "X-Catalyst-SDK": "catalyst-q/0.1.0",
297
+ "X-Catalyst-API": "catalyst-q",
298
+ "X-Catalyst-Install-ID": identity.install_id,
299
+ "X-Catalyst-Install-Tier": identity.tier,
300
+ "X-Catalyst-Account-Mode": identity.mode,
301
+ },
302
+ json=body,
303
+ )
304
+
305
+ def _identity(self) -> InstallIdentity:
306
+ if self.install_identity is None:
307
+ self.install_identity = InstallIdentity.load()
308
+ return self.install_identity
309
+
310
+
311
+ CatalystQClient = CatalystClient
312
+
313
+
314
+ def execute_prepared_request(
315
+ prepared: PreparedRequest,
316
+ timeout: float = 30.0,
317
+ transport: Optional[Transport] = None,
318
+ ) -> Dict[str, Any]:
319
+ started = time.perf_counter()
320
+ try:
321
+ status_code, headers, body = (transport or _urllib_transport)(prepared, timeout)
322
+ except Exception as exc: # noqa: BLE001 - benchmark artifacts should capture transport failures.
323
+ latency_ms = (time.perf_counter() - started) * 1000.0
324
+ return {
325
+ "ok": False,
326
+ "status_code": 0,
327
+ "latency_ms": round(latency_ms, 3),
328
+ "response_bytes": 0,
329
+ "response_sha256": hashlib.sha256(b"").hexdigest(),
330
+ "headers": {},
331
+ "error_type": exc.__class__.__name__,
332
+ "error": str(exc),
333
+ }
334
+ latency_ms = (time.perf_counter() - started) * 1000.0
335
+ result: Dict[str, Any] = {
336
+ "ok": 200 <= status_code < 300,
337
+ "status_code": status_code,
338
+ "latency_ms": round(latency_ms, 3),
339
+ "response_bytes": len(body),
340
+ "response_sha256": hashlib.sha256(body).hexdigest(),
341
+ "headers": dict(headers),
342
+ }
343
+ parsed = _try_parse_json(body)
344
+ if parsed is not None:
345
+ result["json"] = parsed
346
+ else:
347
+ result["text_preview"] = body[:512].decode("utf-8", errors="replace")
348
+ return result
349
+
350
+
351
+ def _urllib_transport(prepared: PreparedRequest, timeout: float) -> Tuple[int, Dict[str, str], bytes]:
352
+ encoded = json.dumps(prepared.json, separators=(",", ":")).encode("utf-8")
353
+ req = request.Request(prepared.url, data=encoded, method=prepared.method, headers=prepared.headers)
354
+ try:
355
+ with request.urlopen(req, timeout=timeout) as response:
356
+ return response.status, dict(response.headers.items()), response.read()
357
+ except error.HTTPError as exc:
358
+ return exc.code, dict(exc.headers.items()), exc.read()
359
+
360
+
361
+ def _try_parse_json(body: bytes) -> Optional[Any]:
362
+ try:
363
+ return json.loads(body.decode("utf-8"))
364
+ except (UnicodeDecodeError, json.JSONDecodeError):
365
+ return None
366
+
367
+
368
+ def _circuit_payload(circuit: Union[List[Dict[str, Any]], Any]) -> Dict[str, Any]:
369
+ if hasattr(circuit, "to_payload"):
370
+ payload = circuit.to_payload()
371
+ if not isinstance(payload, dict) or "circuit" not in payload:
372
+ raise ValueError("circuit.to_payload() must return a dict with a circuit key")
373
+ return payload
374
+ return {"circuit": circuit}
375
+
376
+
377
+ def _problem_payload(problem: Any) -> Dict[str, Any]:
378
+ if hasattr(problem, "to_payload"):
379
+ payload = problem.to_payload()
380
+ if not isinstance(payload, dict):
381
+ raise ValueError("problem.to_payload() must return a dict")
382
+ return payload
383
+ if not isinstance(problem, dict):
384
+ raise ValueError("solver problem must be a problem dataclass or payload dict")
385
+ return problem
386
+
387
+
388
+ def _infer_qubits(circuit: List[Dict[str, Any]]) -> int:
389
+ maximum = 0
390
+ for gate in circuit:
391
+ for key in ("target", "control"):
392
+ if key in gate and isinstance(gate[key], int):
393
+ maximum = max(maximum, gate[key] + 1)
394
+ for key in ("targets", "controls"):
395
+ values = gate.get(key)
396
+ if isinstance(values, list) and values:
397
+ maximum = max(maximum, *[int(value) + 1 for value in values])
398
+ return max(1, maximum)
399
+
400
+
401
+ def _infer_qasm_qubits(qasm: str) -> int:
402
+ matches = [int(match.group(1)) for match in re.finditer(r"\bqreg\s+\w+\[(\d+)\]", qasm)]
403
+ return max(matches) if matches else 1
404
+
405
+
406
+ def _infer_qasm_gate_count(qasm: str) -> int:
407
+ ignored = ("OPENQASM", "include", "qreg", "creg", "//")
408
+ count = 0
409
+ for raw_line in qasm.splitlines():
410
+ line = raw_line.strip()
411
+ if not line or line.startswith(ignored):
412
+ continue
413
+ count += 1
414
+ return count
415
+
416
+
417
+ def _circuit_compute_units(qubits: int, gate_count: int, shots: Optional[int]) -> int:
418
+ return max(1, qubits) * max(1, gate_count) * max(1, shots or 1)
419
+
420
+
421
+ def _infer_problem_size(kind: str, payload: Dict[str, Any]) -> int:
422
+ if kind == "sat":
423
+ return int(payload.get("variables", 1))
424
+ if kind == "tsp":
425
+ return len(payload.get("distances", [])) or len(payload.get("cities", [])) or 1
426
+ if kind == "knapsack":
427
+ return len(payload.get("weights", [])) or 1
428
+ if kind == "portfolio":
429
+ return len(payload.get("returns", [])) or 1
430
+ return 1
431
+
432
+
433
+ def _infer_solver_compute_units(kind: str, payload: Dict[str, Any], problem_size: int) -> int:
434
+ if kind == "sat":
435
+ return max(1, problem_size * len(payload.get("clauses", [])))
436
+ if kind in {"tsp", "portfolio"}:
437
+ return max(1, problem_size * problem_size)
438
+ if kind == "knapsack":
439
+ return max(1, problem_size * max(1, int(payload.get("capacity", 1))))
440
+ return max(1, problem_size)
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Dict, List
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class RenderedDeployment:
9
+ files: Dict[str, str]
10
+ deploy_command: List[str]
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class CloudflareDeploymentSpec:
15
+ worker_name: str
16
+ route_prefix: str = "/v3turbo"
17
+ compatibility_date: str = "2026-05-11"
18
+
19
+ def render(self) -> RenderedDeployment:
20
+ wrangler = "\n".join(
21
+ [
22
+ f'name = "{self.worker_name}"',
23
+ 'main = "src/worker.ts"',
24
+ f'compatibility_date = "{self.compatibility_date}"',
25
+ "",
26
+ "[vars]",
27
+ 'CATALYST_STATE_MODE = "hmk_state_in_payload"',
28
+ 'CATALYST_SHARDING = "nonlinear"',
29
+ "",
30
+ ]
31
+ )
32
+ worker = "\n".join(
33
+ [
34
+ "export default {",
35
+ " async fetch(request: Request): Promise<Response> {",
36
+ " const url = new URL(request.url);",
37
+ f' if (!url.pathname.startsWith("{self.route_prefix}")) {{',
38
+ ' return new Response("not found", { status: 404 });',
39
+ " }",
40
+ " return Response.json({",
41
+ ' status: "ready",',
42
+ ' state_mode: "hmk_state_in_payload",',
43
+ ' sharding: "nonlinear",',
44
+ " });",
45
+ " },",
46
+ "};",
47
+ "",
48
+ ]
49
+ )
50
+ return RenderedDeployment(
51
+ files={
52
+ "wrangler.toml": wrangler,
53
+ "src/worker.ts": worker,
54
+ },
55
+ deploy_command=["npx", "wrangler", "deploy"],
56
+ )