WAI Extension: Quantum Answer Views
Mirrored from the canonical text at commit 260cbd09 ().
Status: Draft. Defines
wai.quantum.answer/1, the answer view layer: a few view statements written beside an OpenQASM 3 program, each view’s numbers bound to an engine call written with@, so that an answer a model gives about a quantum program shows numbers the engine computed and never numbers the model wrote. The text is read line by line as it streams, every problem has a stable code with a defined outcome, and the calls the answer makes can be sealed and checked again later by running the answer. §9 records what the layer was measured to cost a model in tokens against plain OpenQASM and against a job’s JSON (29 to 32 percent more than the JSON for the same request), and how reliably a model wrote it (far less than a program alone). The syntax is not frozen. Reference implementation:wai_quantum::engine::answer; corpus:quantum-conformance/answers/. Keywords MUST, MUST NOT, SHOULD, MAY are RFC 2119/8174.
1. Scope and model
An answer is the text a model writes when it is asked to do something
with a quantum program: the program, and what to show of it. In this layer
the program is OpenQASM 3, unchanged, in a fenced block; what to show is a
handful of view statements (Histogram, Verdict, Receipt, …), and
every number a view shows comes from an engine call (@Sample,
@Check, @Seal, …) that the host carries out on the engine:
root = Answer([bell, counts, check, seal])
```qasm bell
OPENQASM 3.0;
qubit[2] q;
bit[2] c;
h q[0];
cx q[0], q[1];
c = measure q;
```
counts = Histogram(@Sample(bell, 1000))
check = Verdict(@Check(bell, {"00": 0.5, "11": 0.5}))
seal = Receipt(@Seal([bell, counts, check]))
The trust boundary: a model writes only this declarative text. It writes no
markup and no script, and the numbers it could get wrong (counts,
probabilities, expectation values, a verdict) are not in its text at all.
The host draws each view from the engine’s answer to its call; a view
whose argument is a number the model wrote is refused (§6, J104). A
renderer MUST NOT execute anything in an answer other than the engine calls
of §5, and MUST NOT draw a number in a view that the engine did not give.
In scope: the text (§2), statements (§3), the views (§4) and engine calls (§5), the diagnostics (§6), reading as the text streams (§7), carrying the calls out and sealing them (§8), the token comparison (§9) and conformance (§10). Out of scope: how a host lays a view out, and the program language, which is OpenQASM 3 as the engine’s reader reads it.
2. The text
An answer is UTF-8 text, read a line at a time. A line ends at LF; a CR before the LF is not part of the line. The last line need not end with LF. Each line is one of:
- blank (only white space), or a comment (its first non-space
characters
//): nothing; - a statement,
name = Expression(§3), on one line; - a program: a line
```qasm NAME(or```openqasm NAME) opens it, and the next line that is exactly```(white space aside) closes it. The lines between are the program’s text, OpenQASM 3, read by the engine’s reader when the block closes. ItsNAMEis a statement’s name; - a marker: a line that begins
]]>wai:.]]>wai:receipt {…}carries the receipt of a stored answer (§8.3). After an answer’s first marker only markers follow.
Any other line is not read (J100). A name is a letter or _, then
letters, digits or _.
The answer’s source is its text before its first marker, each line ending in LF (a CR before it dropped, and an LF added to a last line that has none). Its digest is the BLAKE3 of the source, as lowercase hex.
3. Statements
statement = name ws "=" ws expr [ws "//" comment]
expr = view | call | name | literal | list | object
view = Name "(" [expr *("," expr)] ")"
call = "@" Name "(" [expr *("," expr)] ")"
list = "[" [expr *("," expr)] "]"
object = "{" [key ":" expr *("," key ":" expr)] "}"
key = json-string | name
literal = json-number | json-string | "true" | "false" | "null"
A statement’s value is a view, an engine call, or a literal (a value:
a number, a string, a list or an object, which an engine call’s parameter
may name). Arguments are positional, in the order §4 and §5 give; null
leaves out an optional one. There are no named arguments: seed=7 in a
call is a line that does not parse (J100), and a reader SHOULD say that
arguments go by position. A value that several calls take, such as the
noise a sample and a verdict both run under, is declared once and passed by
its name. A name used before the statement that declares
it is a forward reference, and is allowed. A name declared again
replaces the earlier declaration (J110): this is how an answer is edited,
by writing the changed statement again.
The statement named root is the answer’s layout, root = Answer([...]),
and SHOULD come first, so that a renderer can lay the answer out before its
parts arrive. With no root, the views are drawn in the order they are
written (J107).
4. Views
A view’s first argument is what it shows: an engine call of the kind the
table names, written in place (Histogram(@Sample(bell, 1000))) or named
(s = @Sample(bell, 1000) then Histogram(s)).
| View | Arguments | Shows |
|---|---|---|
Answer | children: a list of views (a program in the list is drawn as its circuit) | the root: the views, in order |
Text | text: a string | a line of prose; a number in it is the model’s, and is warned of (J111) |
Circuit | program | the program’s circuit, from the engine’s layout, with the circuit in words |
Histogram | @Sample | the counts of a sample |
Expectation | @Expect | Pauli expectation values, each with its accuracy |
Amplitudes | @Amplitudes | amplitudes of basis states, with each one’s probability and phase |
Bloch | @State | one qubit’s Bloch vector and purity |
Verdict | @Check | how far the engine checked the program against its intent |
Equivalence | @Equivalent | whether two programs are the same circuit, and how the engine knows |
Critique | @Critique | the program’s measures and smells |
Estimate | @Estimate | a fault-tolerant resource estimate and its frontier |
Compile | @Compile | the program compiled through checked passes, and the bound on its distance |
Plan | @Plan | the method a run would take, and why |
Receipt | @Seal | a seal over the statements it names, checked where it is shown |
5. Engine calls
Each call is the engine’s own operation on the program it names. A parameter (shots, a seed, a qubit, a tolerance, ε, a budget) MAY be a literal or the name of a value statement; a call’s program is always a program’s name.
| Call | Arguments | The engine’s operation |
|---|---|---|
@Sample | program, shots, seed? (1), noise? | a job of that many shots at that seed (under that noise, an object of channel probabilities as a job’s noise), planned and run: its method, why, the request and the result |
@Expect | program, observables (a list of Pauli strings, as "Z0 Z1") | a job asking those expectation values |
@Amplitudes | program, states (bit strings, qubit n−1 first) | a job asking those amplitudes |
@State | program, qubit | the debugger stepped through every gate, before the final measurements: the qubit’s Bloch vector, its length and its purity. A program that measures in the middle is refused |
@Check | program, intent, tolerance?, noise? | the verdict a job with that intent (and that noise) gets (wai_quantum::engine::verdict): a distribution of its measured bits as an object ({"00": 0.5, "11": 0.5}), a program it must equal (its name, or {"equivalent": name}), or one whose state it must prepare ({"prepares": name}) |
@Equivalent | program, reference | the equivalence ladder: the same circuit up to a global phase, and how that was decided |
@Critique | program | the critique: measures and smells |
@Estimate | program, qubit? (a physical qubit’s name), budget? | the resource estimate |
@Compile | program, epsilon? | the compile, with its certificate |
@Plan | program | the plan of a run of 1024 shots |
@Seal | statements: a list of programs, calls, or views of a call | a receipt over what they give (§8.2) |
A call the engine refuses is a refusal with its codes (the job layer’s
J…, a program’s Q…), for that view alone; the rest of the answer is drawn.
6. Diagnostics
Every problem has a code that never changes meaning once released. A
program’s problems are the reader’s own codes (Q…), at the answer’s line.
A statement’s are the job layer’s, J100 to J112, each with what becomes
of the statement:
| Code | Outcome | Meaning |
|---|---|---|
J100 | dropped | A line that is not a statement, a program, a comment or a marker. |
J101 | dropped | A view the layer does not have. |
J102 | dropped | An @ call the engine does not have. |
J103 | dropped | A required argument missing. |
J104 | dropped | An argument of the wrong kind: a number where a view takes the engine’s call (a number in a view comes from the engine), a list entry the list does not take (left out of the list), a root that is not Answer. |
J105 | warned | More arguments than the view or call takes: the extra ones are left out. |
J106 | dropped | A name no statement declares, or one whose statement was left out (a program the reader refused, say): the statement that names it is left out, and a list that names it is drawn without it. |
J107 | defaulted | No root: the views in the order they are written. |
J108 | dropped | The text ends inside a program or a statement. |
J109 | dropped | Statements that name each other in a cycle. |
J110 | warned | A name declared again: the later declaration stands. |
J111 | warned | A number in a view’s text, which no engine call gave (one with a decimal point, a per cent, or two digits or more). |
J112 | warned | A statement the root does not reach: it is not drawn. |
wai_quantum::engine::refusal holds this table with the job layer’s others
(wai/wai-quantum/docs/job-refusals.md). An error is a diagnostic whose
outcome is dropped or refused; the rest are notes. A host that sends an
answer’s problems back to the model so that it can repair them SHOULD send
the errors alone, each as line N: message [CODE], and never the notes.
7. Reading as the text streams
A reader takes the text in pieces as they arrive, in any split, and reads each line as it completes. After each piece it can give a snapshot: each statement read so far, ready (read, checked, and everything it names is ready), pending (it names a statement that has not arrived) or dropped (with the diagnostic that says why). The root is ready as soon as its line arrives, and the snapshot’s order holds every view it lists, those that have not arrived among them, so a renderer lays the answer out first. A view that is ready can be drawn, and its call carried out, at once.
When the text has ended, the reader gives the answer: a last line
without its LF is read; a program or a statement the text ends inside is
dropped (J108); a name still undeclared is J106; the root’s reach is
checked (J112). A reader MUST give, for the same text, the same answer
however it was split: the same statements, statuses, order, diagnostics and
digest as reading the whole text at once.
8. Carrying the calls out, and sealing them
8.1 Values
A view’s value is its call’s; a Circuit’s is the engine’s layout of the
program and its description in words; a program’s is {"qasm", "digest"},
its text and the BLAKE3 of it. A value MUST hold nothing the engine did not
compute: a run’s time is the host’s, and a value MUST NOT carry it, so that
running the answer again gives the same bytes.
8.2 The seal
@Seal([a, b, …]) signs, with the host’s key, as the job layer’s receipt
does (Ed25519 over the BLAKE3 of the RFC 8785 canonical text), the request
{ "format": "wai.quantum.answer/1", "answer": "<the answer's digest>", "sealed": ["a", "b"] }
and the result {"statements": {"a": <value>, "b": <value>}}, the names
those of the statements listed that are ready. Its value is in a job’s form,
{"request", "result"} with the receipt in the result, so any surface that
checks a job’s receipt checks it. A host with no key refuses a seal.
8.3 A stored answer
A stored answer is its source followed by a marker line carrying its seal’s receipt:
]]>wai:receipt {"statement":"seal","receipt":{"alg":"ed25519","canon":"rfc8785","key":"…","digest":"…","signature":"…"}}
To check it, read the answer, carry the sealed statements out again, build the request and the result of §8.2, and check the receipt over them. The engine is deterministic, so the same answer gives the same bytes on every build; a changed statement changes the digest or the values, and the receipt does not verify. A receipt shows that the key sealed these values for this answer; it does not show who holds the key.
9. What the layer costs in tokens
The corpus is quantum-conformance/eval/answers.json: the fourteen tasks of
the model harness’s task set (eval/tasks.json), each answer written four
ways by wai-quantum eval corpus, and held by a test to the task set, every
answer in the layer reading with no diagnostic and running to its intents:
| Form | What it carries |
|---|---|
plain | the program alone, in a fenced block |
views | the layer whole: the program, its circuit, a sample when it measures, a verdict for each intent, a seal |
views_request | the layer with the sample and the verdicts alone |
job | the request an agent sends a run tool, as JSON with no white space |
views_request and job carry the same program and the same request, so
they are the comparison of the layer against JSON; plain is what a model
writes when it is asked for the program alone. Noise that a sample and a
verdict both run under is declared once in the layer and passed by name
(§3), as the JSON states it once. eval/token_cost.py counted every answer
with the vocabularies of three open-weight models, read from their GGUF
files by a tokenizer tool (eval/token-cost.json). Totals over the
fourteen answers:
| Vocabulary | plain | views | views_request | job | views_request / job |
|---|---|---|---|---|---|
| A, 49 159 tokens | 841 | 2 365 | 1 967 | 1 492 | 1.318 |
| B, 248 320 tokens | 834 | 2 312 | 1 928 | 1 491 | 1.293 |
| C, 262 144 tokens | 835 | 2 282 | 1 940 | 1 483 | 1.308 |
The means per answer are 60 tokens for the program alone, 163 to 169 for the layer whole, 138 to 141 for the layer with the request alone and 106 for the JSON. The three vocabularies, one a fifth the size of the other two, agree to within 3 percent on every ratio.
What the comparison shows:
- The layer costs more than JSON for the same request: 29 to 32 percent more in every vocabulary, 31 to 34 tokens an answer on the mean. The cost is per statement: the root’s line, the fence’s name, and each view’s name and call, 24 to 28 tokens for one sample and one verdict. Those tokens carry what the JSON does not: which views to draw, and a name for each.
- A second verdict is a second statement: 54 to 57 tokens more than the JSON on the task with two intents, one of them a reference program in its own fence. The noisy task, its noise named once, costs 36 to 41 more; written into each call, it cost 67 to 72 more.
- The layer whole, with the circuit and the seal, costs 2.7 to 2.8 times the program alone and 1.54 to 1.59 times the JSON; the circuit and the seal add 24 to 28 tokens to an answer with one verdict, 30 to 34 to one with two.
The layer is not cheaper than JSON in tokens, and this draft does not claim it is. What it buys for the tokens is §1’s: the numbers an answer shows are the engine’s, never the model’s, and the answer is drawn as it streams.
9.1 How well a model writes it
wai-quantum eval run asked one open-weight model each of the fourteen
tasks at temperature 0: a mixture of experts of 35 billion parameters with
3 billion active, quantised to 5 bits and served on the build host with its
reasoning turned off. It was asked once for the program alone and once for
an answer in the layer, each with no repair round and with one, which sends
the engine’s errors back for a corrected answer. Valid is an answer the
engine reads with no error (in the layer, no error in the layer either);
solved is one whose program holds the task’s intent by proof or
evidence. Joules are the model server’s CPU energy as the kernel counts it
for its process; the GPU the model runs on is not counted, so they compare
these runs with each other and do not measure a task’s whole cost.
| Asked for | Repair rounds | Valid | Solved | Joules per solved task |
|---|---|---|---|---|
| the program | 0 | 12 | 11 | 4.4 |
| the program | 1 | 13 | 12 | 4.0 |
| the layer | 0 | 4 | 3 | 17.6 |
| the layer | 1 | 7 | 6 | 10.6 |
This model writes the layer far less reliably than it writes a program. Its
errors are its own, not the reader’s: a program in a fence with no name, a
view given an engine call where it takes a program (Circuit(@Compile(p))),
a name the root or a seal lists that no statement declares, a text or a
circuit listed in a seal, arguments written as seed=7, and the prompt’s
example copied with its program’s name. An answer that is not valid still
draws, since a list leaves out what falls (§7) and only the statements in
error are missing, unless its program is lost, as in the four first answers
whose fence had no name or that ended inside the program. One repair round, with the codes and the lines, corrects
three of the ten. The prompt was revised after the first run on these tasks
to give each argument’s kind and to state the rules those errors broke; the
first run scored the same, 4 valid and 3 solved with no repair and 7 and 6
with one, and the errors moved rather than went. The prompt and the reader
are answer::prompt() and §6; whether a larger model or a model tuned on
the layer does better is not measured here, and a report ranks nothing.
10. Conformance
quantum-conformance/answers/ holds answers, each with the codes of its
errors and warnings in line order, its root, its order, what its views show
when run, and, for stored answers, whether the receipt verifies when the
answer is run again. Every code of §6 has a case. A conforming reader MUST
give each case’s codes, root and order, and MUST give the same answer
however the text is split (§7). A conforming host MUST give each view what
its manifest says when run, and MUST verify the stored cases as they say.
The reference implementation passes the set natively, under wasm32-wasip2
and in the browser (wai-web’s engine_answer_parse and
engine_answer_verify); its stored case was written natively and verifies
in each.
11. Implementation
wai_quantum::engine::answer: the tables of §4 and §5 (VIEWS,CALLS), the reader (parse, andStreamwithpush,snapshotandfinish), andeval(each call carried out, the seal, the marker, and a stored answer’s check).answer::prompt()describes the layer to a model from the same tables, with its rules and a whole answer (answer::EXAMPLE) that reads with no diagnostic.- wai-web (feature
quantum_engine):AnswerStream,engine_answer_parse,engine_answer_prompt,engine_answer_verify,engine_answer_marker, and on the page’s engineanswer_valueandanswer_evaluate. - Quantum Studio draws each statement as it arrives, its calls carried out by the engine in the page; the desktop app answers the same calls natively.
wai-quantum eval run --format viewsasks a model to answer in the layer and scores what it writes (§9.1).