From 00f638383a0f471fb9afebd02e1c3609dab26747 Mon Sep 17 00:00:00 2001 From: David Vesterlund Date: Tue, 27 Oct 2026 17:13:33 +0100 Subject: [PATCH] Add verified resumable WQBench data engine foundation --- README.md | 4 + docs/wqbench/AUDIT.md | 19 ++++ docs/wqbench/PLAN_v0_1.md | 33 ++++++ docs/wqbench/README.md | 55 ++++++++++ src/westquant/wqbench.py | 222 ++++++++++++++++++++++++++++++++++++++ tests/test_wqbench.py | 69 ++++++++++++ 6 files changed, 402 insertions(+) create mode 100644 docs/wqbench/AUDIT.md create mode 100644 docs/wqbench/PLAN_v0_1.md create mode 100644 docs/wqbench/README.md create mode 100644 src/westquant/wqbench.py create mode 100644 tests/test_wqbench.py diff --git a/README.md b/README.md index e19ae65..d05fa99 100644 --- a/README.md +++ b/README.md @@ -158,3 +158,7 @@ Apache-2.0 **David Vesterlund** — Vesterlund Ventures / WestQuant Open Source Project - Email: david@vesterlundventures.se - ORCID: [0009-0000-6455-1141](https://orcid.org/0009-0000-6455-1141) + +## WQBench data engine (experimental) + +A stricter, resumable compilation-data foundation is available in `westquant.wqbench`. It records explicit targets, failures, before/after metrics and conservative semantic verification. See [setup and scope](docs/wqbench/README.md), [implementation plan](docs/wqbench/PLAN_v0_1.md) and [legacy data-engine audit](docs/wqbench/AUDIT.md). It is not yet the full WQBench release or a new WQT model evaluation. diff --git a/docs/wqbench/AUDIT.md b/docs/wqbench/AUDIT.md new file mode 100644 index 0000000..10130f4 --- /dev/null +++ b/docs/wqbench/AUDIT.md @@ -0,0 +1,19 @@ +# Revision av öppna datamotorn, 2026-10-27 + +Granskat: `generate.py`, `wqdf.py`, pyproject och README i WestQuantOpen/westquant. Inte granskat: privata WQT50M-koden eller 1.8M ursprungsposter. + +- `_compile_qiskit` tar emot backend/noise_model men skickar varken Target/Backend/coupling map eller brusmodell vidare. Backendnamn i data kan därför inte ensamt tolkas som en faktisk hårdvarukonditionering. +- Fel fångas i hjälpfunktion och toppnivå; WQDFSample har inget compile_success/error/status-fält. Fel kan serialiseras med depth/size/two_qubit_gates=0 och förlorat felmeddelande. +- sample_id beror bara på framework och lokalt index; kollisioner mellan anrop. +- search_space dokumenteras men styr inte genereringslogiken. +- Pulser-adaptern returnerar compile_success=True och nollmått utan att visa faktisk kompilering. +- Det finns ingen före/efter-kretskoppling, explicit semantisk verifiering, targetvalidering eller root-baserad split i dessa moduler. +- Parquet-fallback byter format till JSONL utan ett tydligt fel till anroparen. + +Detta belägger problem i den öppna implementationen. Det belägger inte att samma fel finns i WQT50M:s separata träningspipeline. 15-kretsuppgiften kommer från den bifogade planen; modellkortets fullständiga innehåll gick inte att hämta via den anslutna källan. + +Implementation v0.1 lägger till en separat strikt kärna i stället för att tyst ändra det befintliga API:t. Legacygeneratorn ska inte användas för nya WQBench-publiceringslabels innan en uttrycklig migrering. + +## Kontroll av planens nämnare + +Den bifogade planens nominella fulla produkt 50000 kretsar ×6 backends ×12 actions ×5 mål är 18000000, inte 1800000. Det betyder inte att dataposter saknas: datan kan vara subsamplad, mål kan vara alternativa vyer och kombinationer kan vara otillåtna. Men faktiskt enumererade, försöka, lyckade och exporterade poster måste separeras och förklaras av ett manifest. Nya kärnan räknar varje faktisk kompilering en gång; mål kan senare härledas från råmåtten utan omkompilering. diff --git a/docs/wqbench/PLAN_v0_1.md b/docs/wqbench/PLAN_v0_1.md new file mode 100644 index 0000000..8cfb87f --- /dev/null +++ b/docs/wqbench/PLAN_v0_1.md @@ -0,0 +1,33 @@ +# WQBench foundation: plan före implementation, 2026-10-27 + +## Problem och formalisering + +För varje grundkrets C, explicit Target T, kompilatorkonfiguration a och seed s ska en verklig kompilering ge råmått m samt separat verifierings- och målvaliditetsstatus. Varje försök, även fel, ingår i nämnaren. Olika målvikter är analysvyer över samma kompilering och ska inte skapa påstått oberoende experiment. Familj/grundproblem, inte post, är analysenhet. + +Befintlig öppen motor `src/westquant/generate.py` granskas. Den skiljer sig från den privata WQT50M-pipelinen, som inte har granskats. Uppgifterna om 1.8M poster och 15 testkretsar i användarens bifogade plan behandlas som rapporterade, inte oberoende verifierade. Inga slutsatser om privatdatasetets fel dras från den öppna koden. + +H0 korrekthet: små unitära kretsar, efter full layoutkorrigering, ska vara ekvivalenta upp till global fas till tolerans 1e-9. För större/dynamiska kretsar ska resultatet vara UNKNOWN, aldrig obestyrkt EXACT. H1 system: körning med resume ska ge exakt samma uppgifts-ID:n och inte räkna cacheträffar som nya observationer. H2 läckage: alla härledda varianter av ett root_id ska få samma split. Dessa är programvarukrav, inte hypoteser om att WQT vinner. + +Närliggande primärkällor, kontrollerade 2026-10-27: MQT Bench dokumentation (kretsgeneratorer, stödda algoritmer); MQT Predictor dokumentation (device selection, device-specific compilation). https://mqt.readthedocs.io/projects/bench/en/latest/ och https://mqt.readthedocs.io/projects/predictor/en/latest/. Ingen nyhetsclaim om att ett gemensamt benchmark saknas görs utan bredare litteraturrevision. NeurIPS 2027-regler eller deadline är inte verifierade och styr inte denna implementation. + +## Lösning + +En separat modul `westquant.wqbench` i existerande umbrella-repo; gammal API behålls. Normaliserad SQLite-lagring med circuits, backends och runs; råa JSONL-exporter. TargetSpec bygger Qiskit Target med dokumenterade syntetiska gatefel/durationer. Denna första etapp omfattar optimization_level 0–3, INTE de tolv privata actions eller multistegspass. Inga QPU-jobb eller modellträningar. + +SHA256 av ett versionssatt, exakt instruktionsdokument ger content_id. Det är en representationshash, inte en semantisk kanonisering. Ursprungligt root_id krävs; det grupperar gauger, parametrar och permutationer via proveniens. En härledd template-hash räcker inte som bevis att orelaterade inputs är oberoende. Datasetexporten bevarar källkod och kompilerad krets, seed, config, miljö och gitrevision. Backendproveniens måste vara synthetic eller measured med källa; nu implementerade mål är synthetic. + +Råmått: djup, storlek, grindhistogram, tvåqubitsgrindar, gate_time_sum_seconds och independent_gate_success_proxy. Proxy är en oberoende produktmodell, inte uppmätt framgång eller kvanttillståndsfidelitet. Summan av gate-tider är inte schemalagd kritisk väglängd. Olika kommando-/konfigurationshashar ger separata run-id:n. Saknade värden är null, inte noll. + +## Prövning och begränsningar + +Första smoke-workload: 3 familjer (GHZ, QAOA, slump), 2 storlekar (3,4), 2 instansseeds =12 källkretsar inom 10 ursprungsgrupper (GHZ-vinkelvarianter delar root); 2 topologier (line,ring), 4 nivåer, 2 transpilerseeds =192 kompileringar. Därefter samma körning med resume: 0 nya kompileringar. Detta är en pipelinekontroll, inte ett oberoende modellbenchmark och ingen prestandastudie. Tak: 5 minuter lokal CPU för smoke, ingen molnkostnad. + +Meningsfulla tester: target routing, layoutkorrigerad operator-ekvivalens, skadad krets upptäcks, UNKNOWN vid för stor verifieringsuppgift, felposter utan fabricerade mått, olika inputs får olika ID:n, samma root håller ihop splits, targetändring invalidaterar cache, resume och nollnämnare i regret. + +Full matris S0–S5 och T1–T6 i användarens plan är roadmap. Statistik för modelljämförelser ska låsas senare med krets-/blockbootstrap och variansbaserad budget, inte behandla 192 rader som 192 problem. S2 kräver eget backendholdout-manifest, inte att blanda dess axel med S0:s root-split. En normalized-regret-denominator utan orakelvinst är odefinierad och redovisas separat. Beam search är en heuristisk referens, inget exakt flerstegsorakel. + +Semantiska gates: failed ekuivalens utesluter raden från legitima åtgärdslabels, UNKNOWN förblir UNKNOWN; fysisk koppling och gate-support valideras separat. GHZ kräver kompletterande basmätningar för koherensclaims. Ruby analog Pulser får ingen påhittad universell gateadapter. Befintliga gauge-hårdvarudata är ett separat experiment och ersätter inte benchmarkens frysta hårdvaruset. + +Beslut: implementera och verifiera denna grund nu. Nästa etapp är revision/import av existerande rådata, Parquet/schemaexport och större oberoende kretsmanifest; först därefter full datafabrik och modellbenchmark. Befintlig DATA_POLICY kräver uttryckligt val av vad som ska bli öppet: nuvarande privata träningsdata publiceras inte genom denna uppgift. + +Preciserat före slutlig smoke: GHZ:s två vinkelvarianter per storlek räknas som två källkretsar men EN ursprungsgrupp. Antalet kompileringar är fortfarande 192, men oberoende gruppnämnare är 10, inte 12. Detta är ett avsiktligt test av proveniensgrupperingen. diff --git a/docs/wqbench/README.md b/docs/wqbench/README.md new file mode 100644 index 0000000..58b19b8 --- /dev/null +++ b/docs/wqbench/README.md @@ -0,0 +1,55 @@ +# WQBench data engine foundation + +An additive, stricter path than the legacy `generate_training_data` API. Version 0.1 is a validated compilation/provenance foundation, not the complete six-task benchmark or an evaluation of WQT50M. + +## Run locally + +Python 3.12 and Qiskit 2.6.1 were used for the verified smoke campaign. From this repository: + +```bash +python -m venv .venv +. .venv/bin/activate +python -m pip install 'qiskit==2.6.1' 'pytest==9.1.1' +PYTHONPATH=src python -m westquant.wqbench --dry-run +PYTHONPATH=src python -m westquant.wqbench --db smoke.sqlite --export smoke_export +PYTHONPATH=src python -m westquant.wqbench --db smoke.sqlite --export smoke_export +PYTHONPATH=src python -m pytest tests -q +``` + +The 12 source circuits belong to 10 root groups: GHZ angle variants share lineage. The second unchanged campaign uses the 192 cached records. Existing failures are cached too; changing the environment/config creates different IDs. No retry overwrites earlier records. SQLite primary keys prevent duplicate rows; parallel writers/retry policy/timeout handling are not implemented in this first serial runner. + +Exports are normalized `circuits.jsonl`, `backends.jsonl`, `runs.jsonl`; requested formats are never silently replaced. QASM3 source and output are retained. Fixed root IDs control a deterministic 80/10/10 hash split (small datasets need not have those exact proportions). This is one IID split, not implementations of S1–S5. Caller-provided root IDs must reflect actual common lineage; the library cannot discover all semantic/permutation duplicates. Exact duplicate indexed representations registered with different roots raise an error. + +```python +from qiskit import QuantumCircuit +from westquant.wqbench import Engine, TargetSpec + +circuit = QuantumCircuit(4) +circuit.h(0) +circuit.cx(0, 3) +with Engine('experiment.sqlite') as engine: + record, cache_hit = engine.compile( + circuit, + root_id='my-independent-problem-001', + family='my_family', + target_spec=TargetSpec('synthetic-line-4', 4), + optimization_level=3, + seed=101, + ) + print(record['status'], record['verification'], cache_hit) + engine.export('experiment_export') +``` + +Synthetic profiles are configurable. Every change is hashed into the backend ID. Errors and durations are consumed by the explicit Target and metric calculation, not by an actual noise simulator. No real calibration or fidelity measurement is claimed. `gate_time_sum_seconds` is a sum, not scheduled makespan; `independent_gate_success_proxy` is a product proxy, not state fidelity, task success or readout mitigation. + +Verification is a full layout-corrected operator check for unitary circuits up to eight qubits with matching source/output widths. Extra target ancillas, larger circuits, measurements/reset and control flow remain UNKNOWN. The target gate set and directed coupling constraints are checked separately. `eligible_for_exact_training` is false for all non-success and non-EXACT records. Structural metrics can be retained for UNKNOWN cases, but should not be relabeled as certified transformations. + +Hashing is exact representation identity with fixed qubit indices and numeric parameters. It does not canonicalize arbitrary commuting instructions, qubit permutations, or semantically equivalent circuits. `root_id` is the separate provenance grouping key. Do not confuse content identity with equivalence or statistical independence. + +## Scope and next implementation + +Implemented: explicit synthetic targets, source/output IDs and QASM, raw metrics, structured failures, conservative equivalence, target validation, root split, deterministic tasks, SQLite resume, normalized JSONL, unit/regression tests. + +Next: audit/import the actual WQT50M raw data; manifest/schema validation and Parquet/Croissant; held-out family/backend/size/calibration manifests; true transition/state graphs and the actual twelve action definitions; worker isolation/timeouts/parallelism; scheduling and measured calibration adapters; objective views/Pareto analysis; large-circuit verification and nonunitary semantic checks; predeclared hardware evaluation. None of those is claimed completed here. + +Publication decisions and access to private data remain separate. No remote hardware jobs, model training, public release or model-card changes are made by this module. See PLAN_v0_1.md and AUDIT.md. diff --git a/src/westquant/wqbench.py b/src/westquant/wqbench.py new file mode 100644 index 0000000..7e809cf --- /dev/null +++ b/src/westquant/wqbench.py @@ -0,0 +1,222 @@ +"""WQBench v0.1: explicit targets, provenance, conservative verification, resumable runs. + +No hardware submission. No noise simulation. Synthetic error-product is a proxy. +""" +from __future__ import annotations +import argparse +from dataclasses import asdict, dataclass +import hashlib +import importlib.metadata +import json +import math +from pathlib import Path +import sqlite3 +import subprocess +import time + +SCHEMA='wqbench-0.1' + +def encoded(obj): + return json.dumps(obj,sort_keys=True,separators=(',',':'),allow_nan=False) + +def digest(obj): + return hashlib.sha256(encoded(obj).encode()).hexdigest() + +def circuit_document(qc): + """Exact indexed instruction identity, NOT semantic canonicalization.""" + if qc.parameters: raise ValueError('Bind all parameters before collecting labels') + return {'schema':SCHEMA,'qubits':qc.num_qubits,'clbits':qc.num_clbits, + 'global_phase':float(qc.global_phase).hex(), + 'instructions':[{'name':i.operation.name,'q':[qc.find_bit(q).index for q in i.qubits], + 'c':[qc.find_bit(c).index for c in i.clbits], + 'params':[float(p).hex() for p in i.operation.params]} for i in qc.data]} + +def split_for_root(root_id,seed=20261027): + if not isinstance(root_id,str) or not root_id.strip(): raise ValueError('Explicit nonempty root_id required') + value=int(digest(['root-split-v1',seed,root_id])[:16],16)/(1<<64) + return 'train' if value<.8 else ('validation' if value<.9 else 'test') + +@dataclass(frozen=True) +class TargetSpec: + name: str + num_qubits: int + topology: str='line' + one_qubit_error: float=.001 + two_qubit_error: float=.01 + readout_error: float=.02 + one_qubit_seconds: float=20e-9 + two_qubit_seconds: float=200e-9 + measurement_seconds: float=800e-9 + provenance: str='synthetic' + source: str='WQBench illustrative profile; not a device calibration' + + def build(self): + from qiskit.circuit import Parameter + from qiskit.circuit.library import RZGate,SXGate,XGate,CXGate + from qiskit.circuit import Measure + from qiskit.transpiler import Target,InstructionProperties + if self.provenance!='synthetic': raise ValueError('Measured profiles require a separate calibrated adapter') + if self.num_qubits<2 or self.num_qubits>30: raise ValueError('Target width must be 2–30') + if self.topology not in ('line','ring','all_to_all'):raise ValueError('Unsupported topology') + for x in (self.one_qubit_error,self.two_qubit_error,self.readout_error): + if not math.isfinite(x) or not 0<=x<=1:raise ValueError('Invalid error probability') + for x in (self.one_qubit_seconds,self.two_qubit_seconds,self.measurement_seconds): + if not math.isfinite(x) or x<0: raise ValueError('Invalid duration in seconds') + n=self.num_qubits + if self.topology=='all_to_all': edges={(i,j) for i in range(n) for j in range(n) if i!=j} + else: + pairs={(i,i+1) for i in range(n-1)} + if self.topology=='ring':pairs.add((0,n-1)) + edges={e for i,j in pairs for e in ((i,j),(j,i))} + t=Target(num_qubits=n) + for gate,duration,error in [(RZGate(Parameter('angle')),0,0),(SXGate(),self.one_qubit_seconds,self.one_qubit_error),(XGate(),self.one_qubit_seconds,self.one_qubit_error),(Measure(),self.measurement_seconds,self.readout_error)]: + t.add_instruction(gate,{(q,):InstructionProperties(duration=duration,error=error) for q in range(n)}) + t.add_instruction(CXGate(),{e:InstructionProperties(duration=self.two_qubit_seconds,error=self.two_qubit_error) for e in sorted(edges)}) + return t + + +def raw_metrics(qc,target=None): + counts=dict(qc.count_ops());duration=0.;proxy=1.;known=target is not None + for i in qc.data: + if i.operation.name=='barrier':continue + if target is not None: + qs=tuple(qc.find_bit(q).index for q in i.qubits) + try:p=target[i.operation.name][qs] + except KeyError:known=False;continue + if p is None or p.duration is None or p.error is None:known=False;continue + duration+=p.duration;proxy*=1-p.error + return {'depth':qc.depth(),'size':qc.size(),'two_qubit_gates':sum(len(i.qubits)==2 for i in qc.data if i.operation.name!='barrier'), + 'operation_counts':counts,'gate_time_sum_seconds':duration if known else None, + 'independent_gate_success_proxy':proxy if known else None, + 'metric_provenance':'structural; target-derived independent-product proxy, not physical fidelity'} + + +def target_valid(qc,target): + return all(i.operation.name=='barrier' or target.instruction_supported(operation_name=i.operation.name,qargs=tuple(qc.find_bit(q).index for q in i.qubits)) for i in qc.data) + + +def verify(source,compiled,max_qubits=8): + from qiskit.quantum_info import Operator + if max(source.num_qubits,compiled.num_qubits)>max_qubits: + return {'status':'UNKNOWN','reason':'exact_operator_width_limit','max_qubits':max_qubits} + if source.num_qubits!=compiled.num_qubits: + return {'status':'UNKNOWN','reason':'ancilla_isometry_not_implemented'} + if any(i.operation.name in ('measure','reset','if_else','while_loop','for_loop') for q in (source,compiled) for i in q.data): + return {'status':'UNKNOWN','reason':'nonunitary_or_measurement_mapping_not_verified'} + a=source;b=compiled + if any(i.operation.name in ('measure','reset','if_else','while_loop','for_loop') for q in (a,b) for i in q.data): + return {'status':'UNKNOWN','reason':'nonunitary_or_control_flow'} + try: + # Operator.from_circuit includes initial/final layout permutations. + left=Operator.from_circuit(a);right=Operator.from_circuit(b) + same=bool(left.equiv(right,atol=1e-9,rtol=1e-9)) + return {'status':'EXACT' if same else 'FAILED','method':'full_layout_corrected_operator_up_to_global_phase','atol':1e-9,'rtol':1e-9} + except Exception as e:return {'status':'UNKNOWN','reason':str(e)} + + +def normalized_regret(selected,baseline,oracle,tolerance=1e-12): + """Conditional improvement-normalized cost regret; no opportunity => undefined.""" + if not all(math.isfinite(x) for x in (selected,baseline,oracle)):raise ValueError('Finite costs required') + if selected