wai-quantum: the estimate job on the reaction-limited clock, one tick rule shared with quantum_arch, the step clock's answers and receipts unchanged #673

Closed
dcharlot wants to merge 9 commits from wai/quantum-estimate-reaction-clock into main
27 changed files with 1503 additions and 92 deletions

View file

@ -6,7 +6,8 @@ name: wai-quantum resource estimation
# each rotation's T count against its synthesised word, the interchange
# documents against the format's schema and a self-made fixture, the
# frontier, the seals, and the planar model and its receipts
# (quantum_resource, quantum_resource_receipt) beside them.
# (quantum_resource, quantum_resource_receipt) beside them, on the step clock
# and the reaction-limited clock.
#
# The published-architecture check (src/published_architectures.rs) feeds the
# stated assumptions of published resource estimates into the estimators
@ -91,7 +92,18 @@ jobs:
engine::estimate::interchange::tests::the_fixture_meets_the_schema_and_its_totals_are_its_children_s \
engine::estimate::tests::an_estimate_is_sealed_verifies_and_a_changed_one_does_not \
quantum_resource::tests::the_summary_rows_are_reproduced \
quantum_resource_receipt::tests::a_counted_program_binds_its_digest_and_its_epsilon; do
quantum_resource_receipt::tests::a_counted_program_binds_its_digest_and_its_epsilon \
quantum_resource::tests::the_step_clock_is_the_estimate_bit_for_bit_and_the_reaction_clock_keeps_pace \
quantum_resource_receipt::tests::on_the_step_clock_a_clocked_job_is_the_job_and_on_the_reaction_clock_another \
engine::estimate::tests::without_a_clock_the_answer_is_the_step_clock_s_and_names_none \
engine::estimate::tests::the_reaction_clock_runs_each_toffoli_in_a_tick_with_factories_to_keep_up \
engine::estimate::tests::a_reaction_clock_estimate_is_sealed_as_a_clocked_job \
quantum_resource::tests::a_reaction_time_is_never_shorter_than_the_qubit_s_measurement \
quantum_resource::tests::a_machine_past_sixty_four_bits_is_refused_and_not_wrapped \
quantum_resource_receipt::tests::a_reaction_clock_receipt_names_its_clock_and_a_step_clock_receipt_is_unchanged \
engine::estimate::tests::a_reaction_time_shorter_than_a_measurement_is_refused \
engine::estimate::tests::the_answer_echoes_the_reaction_time_as_written \
engine::estimate::tests::the_schemas_say_of_the_clock_what_the_engine_takes; do
grep -q "^test $t \.\.\. ok$" "$RUNNER_TEMP/estimate.log" || { echo "::error::$t did not run and pass"; exit 1; }
done
- name: Published architectures, re-estimated, and the page that prints them
@ -104,6 +116,8 @@ jobs:
for t in published_architectures::tests::the_page_is_what_the_estimators_give \
published_architectures::tests::every_row_holds_to_its_published_figure \
published_architectures::tests::the_ours_column_holds_only_ours_and_the_text_says_what_the_sources_say \
published_architectures::tests::section_2_11_is_the_parallel_basis_rounded_and_the_serial_figure_is_ours \
published_architectures::tests::the_reaction_rows_say_what_they_rest_on \
quantum_bicycle::tests::the_cost_tables_are_what_the_search_finds \
quantum_bicycle::tests::the_source_s_mean_costs_come_from_taking_z_by_the_inverse \
quantum_bicycle::tests::the_fits_give_the_printed_rates \

View file

@ -140,7 +140,7 @@ jobs:
rm -f "$report"
WAI_LANDSCAPE_REPORT="$report" nice -n 10 \
cargo test --locked --release --features "${{ matrix.features }}" --test landscape_devices -- --test-threads=1 2>&1 | tee "$RUNNER_TEMP/devices.log"
grep -qE '^test result: ok\. 7 passed; 0 failed' "$RUNNER_TEMP/devices.log" || { echo "::error::not every device test ran and passed (7 expected)"; exit 1; }
grep -qE '^test result: ok\. 9 passed; 0 failed' "$RUNNER_TEMP/devices.log" || { echo "::error::not every device test ran and passed (9 expected)"; exit 1; }
cat "$report"
IFS=',' read -ra want <<< "$WAI_LANDSCAPE_EXPECT"
for p in "${want[@]}"; do

View file

@ -0,0 +1,30 @@
### wai-quantum: the estimate job on the reaction-limited clock
- An estimate's request takes `"clock": {"reaction_us": t}` (`wai-quantum estimate
--reaction-us t`): each Toffoli has its Clifford fixups teleported away from the data and
takes one tick of the reaction time, its three logical steps gone, with enough factories
that a CCZ state comes every tick. The figures add `clock` (`reaction_us`, `tick_us`,
`toffolis`); `steps` are then the lattice-surgery steps alone. A reaction includes the
measurement it reacts to (arXiv:1905.09749 App. B.1), so a reaction time shorter than a
measurement on the qubit is refused (`QE08`); `--qubit all` shows such a qubit as refused
beside the others. A machine whose qubits pass 64 bits is refused where it would have
wrapped. Without a clock, or with `"steps"`, the answer and its receipts are what they
were, bit for bit.
- The tick is one rule, `quantum_resource::reaction_tick`, which `quantum_arch::Timing`
takes too (`quantum_arch` now enables `quantum_resource`). `estimate_isa_clocked`,
`estimate_best_isa_clocked`, `Clock` and `Clocked` price it; on the step clock they are
`estimate_isa`, bit for bit.
- A reaction-clock estimate is sealed as a `ClockedJob`, whose hash is a form of its own
(`wai:resource-job-clocked\x01` over the job's hash and the reaction time), so a receipt
sealed on one clock never verifies as sealed on the other.
`ResourceReceipt::seal_clocked` and `verify_computation_clocked` take it; on the step
clock they seal and check what `seal` and `verify_computation` do. Such a receipt names
its clock itself: `ResourceReceipt::tick_time_ns` and a `clock` member, read from the
figures it signs, give the runtime its steps do not account for, and `wai-quantum verify`
prints it. `wai-quantum estimate` names the clock in its report and its `--qubit all`
table.
- The published-architecture check runs arXiv:1905.09749's counts through the job at its
10 µs: 7.50 hours at d = 25, every Toffoli after the one before, against the step clock's
60.7 and the paper's 5.1. The paper's §2.11 "approximately 7 hours" is not on that basis:
it is the 5.1 hours' parallel basis, rounded. The paper prints no serial figure; its
equations, at 10 µs a Toffoli one after another, give 7.3 hours, which is our derivation.

View file

@ -76,7 +76,7 @@ export class TabEngine {
targets() { return this.call('targets'); }
target(spec) { return this.call('target', { spec }); }
targetPlace(qasm, target) { return this.call('target_place', { qasm, target }); }
// A resource estimate ({qasm, budget, qubit, calibration, max_slowdown}): every
// A resource estimate ({qasm, budget, qubit, calibration, max_slowdown, clock}): every
// estimate sealed with this browser's key; estimateVerify checks the seals and re-runs it.
estimate(request) { return this.call('estimate', { request }); }
estimateVerify(body) { return this.call('estimate_verify', { body }); }

View file

@ -172,7 +172,7 @@ Parameters:
### `estimate_resources`: Estimate a program's fault-tolerant resources
What a fault-tolerant quantum computer would need to run a program. First its logical counts: T gates, Toffolis, rotations (each costed at the T count of its own synthesised Clifford+T word, within its share of the budget), measurements, T depth and total depth, and logical qubits with the ancillas that gates under several controls need. Then, on a physical qubit, the planar model's code distance (the gate-based surface code's model fitted to this engine's own decoders), T factories, physical qubits and runtime, and the frontier of estimates trading qubits for time. Takes a program, or a logical-counts or routine document. Every estimate is sealed in the resource-estimate receipt family, binding the program's digest and the ε budget; the answer's notes state the model's assumptions. Quote the figures with the qubit and the budget, and call them an estimate. Sealed with this user's key; verify_quantum_result checks the seals and re-runs it.
What a fault-tolerant quantum computer would need to run a program. First its logical counts: T gates, Toffolis, rotations (each costed at the T count of its own synthesised Clifford+T word, within its share of the budget), measurements, T depth and total depth, and logical qubits with the ancillas that gates under several controls need. Then, on a physical qubit, the planar model's code distance (the gate-based surface code's model fitted to this engine's own decoders), T factories, physical qubits and runtime, and the frontier of estimates trading qubits for time. Takes a program, or a logical-counts or routine document. Every estimate is sealed in the resource-estimate receipt family, binding the program's digest and the ε budget; the answer's notes state the model's assumptions. Quote the figures with the qubit, the budget and the clock, and call them an estimate. Sealed with this user's key; verify_quantum_result checks the seals and re-runs it.
Parameters:
- `qasm` (string): The program, OpenQASM 3 or 2, or a generator (integer input, const, for loops) with its values in "inputs".
@ -183,6 +183,7 @@ Parameters:
- `qubit` (any): The physical qubit: a reference qubit (ns-e4, the default: 50 ns gates, 1e-4 error; us-* are 100 µs gates, meas-* measurement-based), or one described.
- `calibration` (string, one of exact_matching, union_find, assumed): The gate-based surface code's model: fitted to this engine's exact-matching decoder (default) or its union-find decoder, or the planar model's assumed one.
- `max_slowdown` (integer): The most the frontier slows the program by to need fewer factories (default 256).
- `clock` (any): How long a Toffoli takes: steps (the default: three logical steps of d cycles), or {"reaction_us": t}, the reaction-limited clock: its Clifford fixups teleported away, one tick of the control system's reaction time t µs (never shorter than a measurement on the qubit), with enough factories to keep up.
- `export` (string, one of counts, routine): Also give the program's counts as a logical-counts or a routine document, under export.
- `name` (string): The program's name in a routine document.
@ -763,7 +764,7 @@ Parameters:
### `estimate_resources`: Estimate a quantum program's fault-tolerant resources
What a fault-tolerant quantum computer would need to run a program. First its logical counts: T gates, Toffolis, rotations (each costed at the T count of its own synthesised Clifford+T word, within its share of the budget), measurements, T depth and total depth, and logical qubits with the ancillas that gates under several controls need. Then, on a physical qubit, the planar model's code distance (the gate-based surface code's model fitted to this engine's own decoders), T factories, physical qubits and runtime, and the frontier of estimates trading qubits for time. Takes a program, or a logical-counts or routine document. Every estimate is sealed in the resource-estimate receipt family, binding the program's digest and the ε budget; the answer's notes state the model's assumptions. Quote the figures with the qubit and the budget, and call them an estimate. Sealed with the service's key; verify_quantum_receipt checks the seals and re-runs it. A program with more than 256 distinct rotation angles, or a budget too fine for its rotations, is refused with the local command.
What a fault-tolerant quantum computer would need to run a program. First its logical counts: T gates, Toffolis, rotations (each costed at the T count of its own synthesised Clifford+T word, within its share of the budget), measurements, T depth and total depth, and logical qubits with the ancillas that gates under several controls need. Then, on a physical qubit, the planar model's code distance (the gate-based surface code's model fitted to this engine's own decoders), T factories, physical qubits and runtime, and the frontier of estimates trading qubits for time. Takes a program, or a logical-counts or routine document. Every estimate is sealed in the resource-estimate receipt family, binding the program's digest and the ε budget; the answer's notes state the model's assumptions. Quote the figures with the qubit, the budget and the clock, and call them an estimate. Sealed with the service's key; verify_quantum_receipt checks the seals and re-runs it. A program with more than 256 distinct rotation angles, or a budget too fine for its rotations, is refused with the local command.
Parameters:
- `counts` (object): A logical-counts document instead of a program: numQubits, tCount, rotationCount, rotationDepth, cczCount, ccixCount, measurementCount.
@ -772,6 +773,7 @@ Parameters:
- `qubit` (any): The physical qubit: a reference qubit (ns-e4, the default: 50 ns gates, 1e-4 error; us-* are 100 µs gates, meas-* measurement-based), or one described.
- `calibration` (string, one of exact_matching, union_find, assumed): The gate-based surface code's model: fitted to this engine's exact-matching decoder (default) or its union-find decoder, or the planar model's assumed one.
- `max_slowdown` (integer): The most the frontier slows the program by to need fewer factories (default 256).
- `clock` (any): How long a Toffoli takes: steps (the default: three logical steps of d cycles), or {"reaction_us": t}, the reaction-limited clock: its Clifford fixups teleported away, one tick of the control system's reaction time t µs (never shorter than a measurement on the qubit), with enough factories to keep up.
- `export` (string, one of counts, routine): Also give the program's counts as a logical-counts or a routine document, under export.
- `name` (string): The program's name in a routine document.
- `qasm` (string): The program, OpenQASM 3 or 2. Give this, counts or routine.
@ -2543,6 +2545,11 @@ Options:
[default: 256]
--reaction-us <MICROSECONDS>
Run each Toffoli on the reaction-limited clock: its Clifford fixups teleported away, one
tick of this reaction time (µs, no shorter than a measurement on the qubit), with enough
factories to keep up, instead of three logical steps
--export <FORM>
Print the program's counts as a document instead of the report: counts (the logical-counts
document) or routine (the routine document); the report goes to standard error

View file

@ -19366,7 +19366,7 @@
{
"name": "estimate_resources",
"title": "Estimate a program's fault-tolerant resources",
"description": "What a fault-tolerant quantum computer would need to run a program. First its logical counts: T gates, Toffolis, rotations (each costed at the T count of its own synthesised Clifford+T word, within its share of the budget), measurements, T depth and total depth, and logical qubits with the ancillas that gates under several controls need. Then, on a physical qubit, the planar model's code distance (the gate-based surface code's model fitted to this engine's own decoders), T factories, physical qubits and runtime, and the frontier of estimates trading qubits for time. Takes a program, or a logical-counts or routine document. Every estimate is sealed in the resource-estimate receipt family, binding the program's digest and the ε budget; the answer's notes state the model's assumptions. Quote the figures with the qubit and the budget, and call them an estimate. Sealed with this user's key; verify_quantum_result checks the seals and re-runs it.",
"description": "What a fault-tolerant quantum computer would need to run a program. First its logical counts: T gates, Toffolis, rotations (each costed at the T count of its own synthesised Clifford+T word, within its share of the budget), measurements, T depth and total depth, and logical qubits with the ancillas that gates under several controls need. Then, on a physical qubit, the planar model's code distance (the gate-based surface code's model fitted to this engine's own decoders), T factories, physical qubits and runtime, and the frontier of estimates trading qubits for time. Takes a program, or a logical-counts or routine document. Every estimate is sealed in the resource-estimate receipt family, binding the program's digest and the ε budget; the answer's notes state the model's assumptions. Quote the figures with the qubit, the budget and the clock, and call them an estimate. Sealed with this user's key; verify_quantum_result checks the seals and re-runs it.",
"inputSchema": {
"type": "object",
"properties": {
@ -19453,6 +19453,31 @@
"maximum": 4096,
"description": "The most the frontier slows the program by to need fewer factories (default 256)."
},
"clock": {
"anyOf": [
{
"type": "string",
"enum": [
"steps"
]
},
{
"type": "object",
"properties": {
"reaction_us": {
"type": "number",
"exclusiveMinimum": 0,
"exclusiveMaximum": 1000000000.0
}
},
"required": [
"reaction_us"
],
"additionalProperties": false
}
],
"description": "How long a Toffoli takes: steps (the default: three logical steps of d cycles), or {\"reaction_us\": t}, the reaction-limited clock: its Clifford fixups teleported away, one tick of the control system's reaction time t µs (never shorter than a measurement on the qubit), with enough factories to keep up."
},
"export": {
"type": "string",
"enum": [
@ -19515,6 +19540,28 @@
"max_slowdown": {
"type": "integer"
},
"clock": {
"description": "How long a Toffoli takes: steps (the default: three logical steps of d cycles), or the reaction-limited clock, a Toffoli one tick of the control system's reaction time in µs, its Clifford fixups teleported away, with enough factories to keep up.",
"anyOf": [
{
"const": "steps"
},
{
"type": "object",
"required": [
"reaction_us"
],
"properties": {
"reaction_us": {
"type": "number",
"exclusiveMinimum": 0,
"exclusiveMaximum": 1000000000
}
},
"additionalProperties": false
}
]
},
"export": {
"enum": [
"counts",
@ -19749,6 +19796,27 @@
"kind": {
"const": "quantum-resource"
},
"clock": {
"type": "object",
"description": "On the reaction-limited clock only: the runtime the estimate's steps do not account for, read from its signed figures. How long one tick was is the job's, whose hash binds the reaction time.",
"required": [
"kind",
"tick_time_ns_bits",
"tick_time_s"
],
"properties": {
"kind": {
"const": "reaction"
},
"tick_time_ns_bits": {
"type": "string"
},
"tick_time_s": {
"type": "number"
}
},
"additionalProperties": false
},
"estimate": {
"type": "object"
},
@ -19789,6 +19857,26 @@
"type": "object"
}
}
},
"clock": {
"type": "object",
"description": "On the reaction-limited clock only: its reaction time, one tick at this estimate's slowdown, and the Toffolis run on ticks. Then steps are the lattice-surgery steps alone, and runtime_s adds the ticks.",
"required": [
"reaction_us",
"tick_us",
"toffolis"
],
"properties": {
"reaction_us": {
"type": "number"
},
"tick_us": {
"type": "number"
},
"toffolis": {
"type": "integer"
}
}
}
}
},
@ -19885,6 +19973,27 @@
"kind": {
"const": "quantum-resource"
},
"clock": {
"type": "object",
"description": "On the reaction-limited clock only: the runtime the estimate's steps do not account for, read from its signed figures. How long one tick was is the job's, whose hash binds the reaction time.",
"required": [
"kind",
"tick_time_ns_bits",
"tick_time_s"
],
"properties": {
"kind": {
"const": "reaction"
},
"tick_time_ns_bits": {
"type": "string"
},
"tick_time_s": {
"type": "number"
}
},
"additionalProperties": false
},
"estimate": {
"type": "object"
},
@ -19925,6 +20034,26 @@
"type": "object"
}
}
},
"clock": {
"type": "object",
"description": "On the reaction-limited clock only: its reaction time, one tick at this estimate's slowdown, and the Toffolis run on ticks. Then steps are the lattice-surgery steps alone, and runtime_s adds the ticks.",
"required": [
"reaction_us",
"tick_us",
"toffolis"
],
"properties": {
"reaction_us": {
"type": "number"
},
"tick_us": {
"type": "number"
},
"toffolis": {
"type": "integer"
}
}
}
}
},

View file

@ -40920,7 +40920,7 @@
{
"name": "estimate_resources",
"title": "Estimate a quantum program's fault-tolerant resources",
"description": "What a fault-tolerant quantum computer would need to run a program. First its logical counts: T gates, Toffolis, rotations (each costed at the T count of its own synthesised Clifford+T word, within its share of the budget), measurements, T depth and total depth, and logical qubits with the ancillas that gates under several controls need. Then, on a physical qubit, the planar model's code distance (the gate-based surface code's model fitted to this engine's own decoders), T factories, physical qubits and runtime, and the frontier of estimates trading qubits for time. Takes a program, or a logical-counts or routine document. Every estimate is sealed in the resource-estimate receipt family, binding the program's digest and the ε budget; the answer's notes state the model's assumptions. Quote the figures with the qubit and the budget, and call them an estimate. Sealed with the service's key; verify_quantum_receipt checks the seals and re-runs it. A program with more than 256 distinct rotation angles, or a budget too fine for its rotations, is refused with the local command.",
"description": "What a fault-tolerant quantum computer would need to run a program. First its logical counts: T gates, Toffolis, rotations (each costed at the T count of its own synthesised Clifford+T word, within its share of the budget), measurements, T depth and total depth, and logical qubits with the ancillas that gates under several controls need. Then, on a physical qubit, the planar model's code distance (the gate-based surface code's model fitted to this engine's own decoders), T factories, physical qubits and runtime, and the frontier of estimates trading qubits for time. Takes a program, or a logical-counts or routine document. Every estimate is sealed in the resource-estimate receipt family, binding the program's digest and the ε budget; the answer's notes state the model's assumptions. Quote the figures with the qubit, the budget and the clock, and call them an estimate. Sealed with the service's key; verify_quantum_receipt checks the seals and re-runs it. A program with more than 256 distinct rotation angles, or a budget too fine for its rotations, is refused with the local command.",
"inputSchema": {
"type": "object",
"properties": {
@ -40996,6 +40996,31 @@
"maximum": 4096,
"description": "The most the frontier slows the program by to need fewer factories (default 256)."
},
"clock": {
"anyOf": [
{
"type": "string",
"enum": [
"steps"
]
},
{
"type": "object",
"properties": {
"reaction_us": {
"type": "number",
"exclusiveMinimum": 0,
"exclusiveMaximum": 1000000000.0
}
},
"required": [
"reaction_us"
],
"additionalProperties": false
}
],
"description": "How long a Toffoli takes: steps (the default: three logical steps of d cycles), or {\"reaction_us\": t}, the reaction-limited clock: its Clifford fixups teleported away, one tick of the control system's reaction time t µs (never shorter than a measurement on the qubit), with enough factories to keep up."
},
"export": {
"type": "string",
"enum": [
@ -41065,6 +41090,28 @@
"max_slowdown": {
"type": "integer"
},
"clock": {
"description": "How long a Toffoli takes: steps (the default: three logical steps of d cycles), or the reaction-limited clock, a Toffoli one tick of the control system's reaction time in µs, its Clifford fixups teleported away, with enough factories to keep up.",
"anyOf": [
{
"const": "steps"
},
{
"type": "object",
"required": [
"reaction_us"
],
"properties": {
"reaction_us": {
"type": "number",
"exclusiveMinimum": 0,
"exclusiveMaximum": 1000000000
}
},
"additionalProperties": false
}
]
},
"export": {
"enum": [
"counts",
@ -41299,6 +41346,27 @@
"kind": {
"const": "quantum-resource"
},
"clock": {
"type": "object",
"description": "On the reaction-limited clock only: the runtime the estimate's steps do not account for, read from its signed figures. How long one tick was is the job's, whose hash binds the reaction time.",
"required": [
"kind",
"tick_time_ns_bits",
"tick_time_s"
],
"properties": {
"kind": {
"const": "reaction"
},
"tick_time_ns_bits": {
"type": "string"
},
"tick_time_s": {
"type": "number"
}
},
"additionalProperties": false
},
"estimate": {
"type": "object"
},
@ -41339,6 +41407,26 @@
"type": "object"
}
}
},
"clock": {
"type": "object",
"description": "On the reaction-limited clock only: its reaction time, one tick at this estimate's slowdown, and the Toffolis run on ticks. Then steps are the lattice-surgery steps alone, and runtime_s adds the ticks.",
"required": [
"reaction_us",
"tick_us",
"toffolis"
],
"properties": {
"reaction_us": {
"type": "number"
},
"tick_us": {
"type": "number"
},
"toffolis": {
"type": "integer"
}
}
}
}
},
@ -41435,6 +41523,27 @@
"kind": {
"const": "quantum-resource"
},
"clock": {
"type": "object",
"description": "On the reaction-limited clock only: the runtime the estimate's steps do not account for, read from its signed figures. How long one tick was is the job's, whose hash binds the reaction time.",
"required": [
"kind",
"tick_time_ns_bits",
"tick_time_s"
],
"properties": {
"kind": {
"const": "reaction"
},
"tick_time_ns_bits": {
"type": "string"
},
"tick_time_s": {
"type": "number"
}
},
"additionalProperties": false
},
"estimate": {
"type": "object"
},
@ -41475,6 +41584,26 @@
"type": "object"
}
}
},
"clock": {
"type": "object",
"description": "On the reaction-limited clock only: its reaction time, one tick at this estimate's slowdown, and the Toffolis run on ticks. Then steps are the lattice-surgery steps alone, and runtime_s adds the ticks.",
"required": [
"reaction_us",
"tick_us",
"toffolis"
],
"properties": {
"reaction_us": {
"type": "number"
},
"tick_us": {
"type": "number"
},
"toffolis": {
"type": "integer"
}
}
}
}
},

View file

@ -45,6 +45,9 @@ pub struct Args {
/// The most the frontier slows the program by, to need fewer factories (it tries powers of two up to this)
#[arg(long, default_value_t = estimate::DEFAULT_MAX_SLOWDOWN)]
pub max_slowdown: u64,
/// Run each Toffoli on the reaction-limited clock: its Clifford fixups teleported away, one tick of this reaction time (µs, no shorter than a measurement on the qubit), with enough factories to keep up, instead of three logical steps
#[arg(long, value_name = "MICROSECONDS")]
pub reaction_us: Option<f64>,
/// Print the program's counts as a document instead of the report: counts (the logical-counts document) or routine (the routine document); the report goes to standard error
#[arg(long, value_name = "FORM")]
pub export: Option<String>,
@ -74,6 +77,9 @@ pub fn requests(a: &Args) -> Result<Vec<Value>, String> {
if let Some(e) = &a.export {
r["export"] = json!(e);
}
if let Some(t) = a.reaction_us {
r["clock"] = json!({ "reaction_us": t });
}
let named = a.file.as_deref().or(a.routine.as_deref()).or(a.counts.as_deref()).filter(|p| *p != Path::new("-"));
if let Some(stem) = named.and_then(Path::file_stem).and_then(|s| s.to_str()) {
r["name"] = json!(stem);
@ -115,6 +121,37 @@ fn headline(qubit: &str, e: &Value) -> String {
)
}
/// What an answer on the reaction-limited clock must say of it: the clock
/// moves the runtime and the factories more than any other option.
fn clock_line(body: &Value) -> Option<String> {
let t = body["result"]["options"]["clock"]["reaction_us"].as_f64()?;
Some(format!("reaction-limited: each Toffoli one tick of {t} µs, with enough factories to keep up, not three logical steps"))
}
/// The sealed answers, and each qubit that stood aside with its reason.
type Sealed = (Vec<Value>, Vec<(String, String)>);
/// The requests sealed. With several and a reaction time, a reference qubit
/// that time cannot run on stands aside with its reason, so `--qubit all`
/// still shows the others; with one, or if none can, the refusal stands.
fn sealed_each(reqs: &[Value], key: &wai_quantum::ed25519_dalek::SigningKey) -> Result<Sealed, String> {
let (mut bodies, mut refused) = (Vec::new(), Vec::new());
for q in reqs {
if reqs.len() > 1 {
let options = estimate::Options::read(q)?;
if let Some(why) = estimate::clock_refusal(estimate::clock_of(q)?, &options.qubit) {
refused.push((options.qubit_name, why));
continue;
}
}
bodies.push(estimate::sealed(q, key)?);
}
match refused.first() {
Some((_, why)) if bodies.is_empty() => Err(why.clone()),
_ => Ok((bodies, refused)),
}
}
/// The estimate as a reader takes it in.
pub fn report(body: &Value) -> String {
let r = &body["result"];
@ -157,6 +194,9 @@ pub fn report(body: &Value) -> String {
} else {
format!("calibration {}\n", s(&cal["decoder"]))
};
if let Some(line) = clock_line(body) {
out += &format!("clock {line}\n");
}
out += &format!("estimate {}\n", headline(&s(&r["options"]["qubit"]["name"]), &r["estimate"]));
let f = r["frontier"].as_array().cloned().unwrap_or_default();
if f.len() > 1 {
@ -173,7 +213,7 @@ pub fn main(a: Args) {
let r = (|| -> Result<(), String> {
let reqs = requests(&a)?;
let k = super::load_key(&a.key);
let bodies: Vec<Value> = reqs.iter().map(|q| estimate::sealed(q, &k.key)).collect::<Result<_, _>>()?;
let (bodies, refused) = sealed_each(&reqs, &k.key)?;
let write = |text: &str| -> Result<(), String> {
match &a.out {
Some(p) => std::fs::write(p, text).map_err(|e| format!("{}: {e}", p.display())),
@ -184,7 +224,10 @@ pub fn main(a: Args) {
}
};
if a.json {
let v = if bodies.len() == 1 { bodies[0].clone() } else { Value::Array(bodies) };
for (q, why) in &refused {
eprintln!("{q}: {why}");
}
let v = if reqs.len() == 1 { bodies[0].clone() } else { Value::Array(bodies) };
return write(&format!("{v}\n"));
}
if a.export.is_some() {
@ -192,12 +235,18 @@ pub fn main(a: Args) {
eprint!("{}", report(&bodies[0]));
return Ok(());
}
if bodies.len() > 1 {
if reqs.len() > 1 {
if let Some(line) = clock_line(&bodies[0]) {
println!("clock: {line}");
}
println!("{:<10} {:<36} {:>4} {:>9} {:>15} runtime", "qubit", "code", "d", "factories", "physical");
for b in &bodies {
let e = &b["result"]["estimate"];
println!("{:<10} {:<36} {:>4} {:>9} {:>15} {}", s(&b["result"]["options"]["qubit"]["name"]), s(&e["code"]), s(&e["distance"]), s(&e["factories"]), s(&e["physical_qubits"]), secs(&e["runtime_s"]));
}
for (q, why) in &refused {
println!("{q:<10} refused: {why}");
}
return Ok(());
}
print!("{}", report(&bodies[0]));
@ -244,4 +293,43 @@ mod tests {
assert!(super::super::Cli::try_parse_from(["wai-quantum", "estimate", "p.qasm", "--counts", "c.json"]).is_err());
assert!(super::super::Cli::try_parse_from(["wai-quantum", "estimate", "--routine", "r.json", "--export", "counts"]).is_ok());
}
/// `--reaction-us` reaches the request, the report names the clock, and
/// on every qubit the time cannot run on, the table says why.
#[test]
fn the_reaction_clock_reaches_the_request_and_the_report_names_it() {
let path = std::env::temp_dir().join(format!("wai-quantum-estimate-clock-{}.json", std::process::id()));
std::fs::write(&path, r#"{"numQubits": 100, "cczCount": 1000000}"#).unwrap();
let args = |rest: &[&str]| {
let mut v = vec!["wai-quantum", "estimate", "--counts", path.to_str().unwrap(), "--max-slowdown", "1"];
v.extend_from_slice(rest);
let c = super::super::Cli::try_parse_from(v).unwrap();
let super::super::Command::Estimate(a) = c.command else { panic!("not estimate") };
requests(&a).unwrap()
};
let key = wai_quantum::ed25519_dalek::SigningKey::from_bytes(&[4; 32]);
let fast = args(&["--qubit", "ns-e4", "--reaction-us", "10"]);
assert_eq!(fast[0]["clock"], json!({ "reaction_us": 10.0 }));
let body = estimate::sealed(&fast[0], &key).unwrap();
let text = report(&body);
assert!(text.contains("clock reaction-limited: each Toffoli one tick of 10 µs"), "{text}");
let plain = args(&["--qubit", "ns-e4"]);
assert!(plain[0].get("clock").is_none());
assert!(!report(&estimate::sealed(&plain[0], &key).unwrap()).contains("clock "));
// The receipt alone says so too, as `wai-quantum verify` prints it.
let r = wai_quantum::quantum_resource_receipt::ResourceReceipt::from_json(&body["result"]["estimate"]["receipt"].to_string()).unwrap();
assert!(super::super::lab::resource_clock_line(&r).unwrap().starts_with("reaction-limited"));
// The 100 µs qubits stand aside; the others are estimated.
let (bodies, refused) = sealed_each(&args(&["--qubit", "all", "--reaction-us", "10"]), &key).unwrap();
assert_eq!(refused.iter().map(|r| r.0.as_str()).collect::<Vec<_>>(), ["us-e3", "us-e4"]);
assert_eq!(bodies.len() + refused.len(), estimate::QUBITS.len());
assert!(refused[0].1.contains("a reaction includes the measurement"), "{}", refused[0].1);
// One qubit alone is refused outright, and so is a time none can run.
assert!(sealed_each(&args(&["--qubit", "us-e3", "--reaction-us", "10"]), &key).is_err());
assert!(sealed_each(&args(&["--qubit", "all", "--reaction-us", "0.001"]), &key).is_err());
std::fs::remove_file(&path).ok();
}
}

View file

@ -457,6 +457,13 @@ fn estimate_json(e: &qr::Estimate) -> String {
)
}
/// What a resource receipt's signed figures say of its clock: on the
/// reaction-limited clock the runtime is more than its steps.
pub(crate) fn resource_clock_line(r: &ResourceReceipt) -> Option<String> {
let ticks = r.tick_time_ns();
(ticks > 0.0).then(|| format!("reaction-limited: {} of the runtime is Toffoli ticks, not steps", human_time(ticks)))
}
/// Check a receipt of any kind this tool or the library signs: `src` is its
/// JSON. Prints what it binds, and exits 1 when it does not verify.
pub fn verify_receipt(src: &str) {
@ -473,6 +480,9 @@ pub fn verify_receipt(src: &str) {
println!("qubit {} ({}, d = {})", e.qubit, e.code, e.distance);
println!("physical {} qubits", e.physical_qubits);
println!("runtime {}", human_time(e.runtime_ns));
if let Some(clock) = resource_clock_line(r) {
println!("clock {clock}");
}
println!("factories {}", e.factories);
println!("joules {}", joules_line(r.joules_micro));
println!("energy class {}", energy_class_line(any.energy_class(), r.joules_micro));

View file

@ -338,6 +338,7 @@ def estimate(
export: Optional[str] = None,
name: Optional[str] = None,
key: KeyLike = None,
reaction_us: Optional[float] = None,
) -> Dict[str, Any]:
"""What a fault-tolerant machine would need to run ``program`` (or the
logical-counts document ``counts``, or the routine document
@ -351,7 +352,10 @@ def estimate(
``union_find`` or ``assumed``), the headline estimate on ``qubit`` (a
reference qubit's name, ``ns-e4`` by default, or a dict describing one)
and the frontier between physical qubits and runtime (slowed up to
``max_slowdown``). Every estimate is sealed with ``key`` in the
``max_slowdown``). With ``reaction_us`` each Toffoli runs on the
reaction-limited clock, one tick of that reaction time in µs (no shorter
than a measurement on the qubit), instead of three logical steps. Every
estimate is sealed with ``key`` in the
resource-estimate receipt family; ``verify_estimate`` checks the seals
and re-runs it. ``export`` (``counts`` or ``routine``) adds the
program's counts as a document, named ``name``. A request the engine
@ -369,6 +373,8 @@ def estimate(
for k, v in (("budget", budget), ("qubit", qubit), ("calibration", calibration), ("max_slowdown", max_slowdown), ("export", export), ("name", name)):
if v is not None:
request[k] = dict(v) if isinstance(v, Mapping) else v
if reaction_us is not None:
request["clock"] = {"reaction_us": reaction_us}
return _engine.estimate(request, key_of(key).seed)

View file

@ -390,6 +390,20 @@ def test_an_estimate_counts_the_program_seals_every_figure_and_re_runs():
assert wq.estimate(LADDER, qubit="us-e3", budget=1e-4, max_slowdown=64, key=DEMO) == e
def test_an_estimate_on_the_reaction_clock_runs_each_toffoli_in_a_tick():
steps = wq.estimate(LADDER, qubit="us-e3", budget=1e-4, max_slowdown=1, key=DEMO)["result"]["estimate"]
e = wq.estimate(LADDER, qubit="us-e3", budget=1e-4, max_slowdown=1, key=DEMO, reaction_us=100)
r = e["result"]
assert r["options"]["clock"] == {"reaction_us": 100.0}
assert r["estimate"]["clock"] == {"reaction_us": 100.0, "tick_us": 100.0, "toffolis": 3}
assert "clock" not in steps and r["estimate"]["runtime_s"] < steps["runtime_s"]
assert r["estimate"]["receipt"]["clock"]["kind"] == "reaction"
assert wq.verify_estimate(e)["valid"]
# A reaction includes a measurement, and this qubit's takes 100 µs.
with pytest.raises(wq.EngineError):
wq.estimate(LADDER, qubit="us-e3", budget=1e-4, max_slowdown=1, key=DEMO, reaction_us=10)
def test_an_estimate_reads_and_writes_the_interchange_documents():
doc = wq.estimate(LADDER, export="counts", key=DEMO)["result"]["export"]
assert doc == {"numQubits": 5, "tCount": 1, "rotationCount": 1, "rotationDepth": 1, "cczCount": 3, "ccixCount": 0, "measurementCount": 4}

View file

@ -153,7 +153,7 @@ Counts are always seeded samples.
| `POST /v1/shortcuts` | `{"qasm"}`: each exact classical route to simulating the program, priced on it, and the cheapest for each question |
| `POST /v1/compile` | `{"qasm", "epsilon", "level", "basis"}`: the program compiled through checked passes (to Clifford+T within `epsilon`, by default `1e-6`), with a certificate of every pass and the distance, sealed with the service's key in a job's form so `POST /v1/verify` checks it ([the compiler page](../wai-quantum/docs/compiler.md)) |
| `GET /v1/targets` | the built-in device targets (synthetic), each with its qubits, pairs, gates, errors and hash |
| `POST /v1/estimate` | `{"qasm"}` (or `{"counts"}`, `{"routine"}`), with `budget`, `qubit`, `calibration`, `max_slowdown` and `export`, each optional: what a fault-tolerant machine would need to run the program, its logical counts, the estimate and the frontier, each estimate sealed with the service's key in the resource-estimate receipt family and the answer in a job's form; `POST /v1/verify` checks the seals and re-runs it ([the estimate page](../wai-quantum/docs/estimate.md)) |
| `POST /v1/estimate` | `{"qasm"}` (or `{"counts"}`, `{"routine"}`), with `budget`, `qubit`, `calibration`, `max_slowdown`, `clock` and `export`, each optional: what a fault-tolerant machine would need to run the program, its logical counts, the estimate and the frontier, each estimate sealed with the service's key in the resource-estimate receipt family and the answer in a job's form; `POST /v1/verify` checks the seals and re-runs it ([the estimate page](../wai-quantum/docs/estimate.md)) |
| `POST /v1/repair` | `{"qasm", "proposed"?, "max_iterations"?, "dyadic_bits"?, "sizes"?}`: the program repaired on the lines its diagnostics flag alone, a proposed repair taken only if it changes no other line, and what needs a person |
| `GET /.well-known/wai-quantum-receipt-keys` | every key receipts are signed with, each with its validity window, a retired one's transition to its successor signed by both |
| `POST /v1/qec/code` | `{"code", "certify"?, "bound"?, "certificate"?}`: an error-correcting code (`surface:D`, `toric:L`, `steane`, `color:D`, `bb:72` to `bb:288`, `gross`), its checks, `k`, layout and decoders, and its distance certificate, or a given certificate checked against it |

View file

@ -132,7 +132,7 @@ quantum_gbs = []
quantum_sv = []
# Architecture-level estimation: yoked storage, magic-state cultivation, CCZ
# factories and reaction-limited time, assembled into whole-machine estimates.
quantum_arch = []
quantum_arch = ["quantum_resource"]
# qLDPC memory at circuit level: bivariate-bicycle codes, their depth-8 syndrome
# cycle, and BP-OSD decoding of the circuit's error model.
quantum_ldpc = ["quantum_frame"]

View file

@ -185,6 +185,7 @@ def main() -> int:
("qasm_diagnostics", {"code": "Q103"}),
("qasm_diagnostics", {"code": "J021"}),
("estimate_resources", {"qasm": rot, "qubit": "us-e4", "budget": 1e-3, "export": "counts"}),
("estimate_resources", {"qasm": rot, "qubit": "ns-e4", "budget": 1e-3, "clock": {"reaction_us": 10}}),
("debug_branches", {"qasm": tele}),
("debug_step", {"qasm": tele, "line": 14, "outcomes": [1, 1], "probe": {"qubits": [2]}}),
("list_targets", {}),

View file

@ -74,6 +74,7 @@ receipts 5 estimates sealed by did:key:z6Mk…; they verify and re-run here
| `qubit` | `ns-e4` | a reference qubit (below), or `{"name", "instruction_set": "gate"\|"measurement", "gate_ns", "meas_ns", "clifford_error", "t_error"}` |
| `calibration` | `exact_matching` | the gate-based surface code's model: `exact_matching`, `union_find` (fitted to the decoders) or `assumed` (the planar model's own) |
| `max_slowdown` | `256` | the frontier slows the program by 1, 2, 4, … up to this (at most 4096) |
| `clock` | `"steps"` | how long a Toffoli takes: `"steps"`, three logical steps; or `{"reaction_us": t}`, the reaction-limited clock (below), `t` above 0, below 10⁹ and no shorter than a measurement on the qubit |
| `export` | none | also give the counts as a `counts` or `routine` document |
| `name` | `program` | the program's name in a routine document |
@ -197,12 +198,38 @@ On a physical qubit and a planar code:
- enough factory copies run that the program's T states arrive within its
runtime.
**The clock.** By default a Toffoli takes three logical steps, as the
planar model has it. On the reaction-limited clock, `"clock":
{"reaction_us": t}` (`wai-quantum estimate --reaction-us t`), a Toffoli has
its Clifford fixups teleported away from the data and takes one tick: the
slower of the reaction time `t` and the CCZ supply, the rule
`quantum_arch`'s clock takes too (arXiv:1905.09749 §2.11). Then:
- the Toffolis' `3·X` steps leave the steps, and `X` ticks join the
runtime: runtime = steps × `τ(d)` + `X` × tick, every Toffoli after the
one before, as the layout runs its steps;
- the distance holds the tiles' error over the whole runtime;
- the factories are as many as the T states need over the runtime, and at
least as many as deliver a CCZ state (four T states) every tick, so the
supply keeps up and a tick is the reaction time;
- a slowed program stretches its ticks as its steps: `s` times the reaction
time;
- a reaction includes the logical measurement it reacts to (arXiv:1905.09749
App. B.1), so a reaction time shorter than a measurement on the qubit is
refused (`QE08`): the 100 µs qubits have no 10 µs tick. `wai-quantum estimate
--qubit all --reaction-us t` shows such a qubit as refused, with the reason,
beside the others.
Without a clock, or with `"steps"`, the answer and its receipts are what
they were before the clock was named, bit for bit.
| Field | Meaning |
|---|---|
| `qubit`, `code`, `distance`, `slowdown` | the qubit, the code (with its calibration), `d`, and how many times slower than the fewest steps the program runs |
| `physical_qubits` = `algorithm_qubits` + `factory_qubits` | tiles × qubits per tile, and the factories' |
| `factories`, `factory_fraction` | factory copies, and their share of the physical qubits |
| `runtime_s`, `steps`, `step_us` | wall-clock time = steps × the step time `τ(d)` |
| `runtime_s`, `steps`, `step_us` | wall-clock time = steps × the step time `τ(d)`; on the reaction-limited clock, `steps` are the lattice-surgery steps alone and the runtime adds the ticks |
| `clock` | on the reaction-limited clock only: `reaction_us`, `tick_us` (at this slowdown) and the `toffolis` run on ticks |
| `tiles`, `tile_qubits` | `Q` and the physical qubits of one tile at `d` |
| `logical_error` | `P(d)`, per tile per step |
| `t_states`, `t_target` | the T states and the error each must meet |
@ -299,8 +326,14 @@ origin, its digest, every count, the ancillas, the rotations' `T`, the
depths, the budget, the synthesis share, each rotation's share, the bound
and the layout), the qubit, the code with its calibrated constants as exact
IEEE-754 bits, and the slowdown; its result hash binds every figure of the
estimate down to each factory round. The frontier's receipts name the
headline's as their parent. The whole result is then sealed in a job's
estimate down to each factory round. On the reaction-limited clock the job
is a clocked job (`quantum_resource_receipt::ClockedJob`): its hash is a
form of its own, `wai:resource-job-clocked\x01` over the job's hash and the
reaction time, so a receipt sealed on one clock never verifies as sealed on
the other. Such a receipt also says so itself: its `clock` member gives the
runtime its steps do not account for (`tick_time_ns_bits`, `tick_time_s`), read
from the figures it signs, and a step-clock receipt has none. How long one tick
was is the job's. The frontier's receipts name the headline's as their parent. The whole result is then sealed in a job's
form, so any surface's `verify` checks it as it checks a job.
`engine::estimate::verify` (`wai-quantum verify`, `POST /v1/verify`,
@ -321,7 +354,7 @@ estimates bit for bit, and the same figures, counts and notes.
| `QE05` | nothing to estimate: no qubits, or no measurement, `T` gate, rotation or Toffoli (no logical step) |
| `QE06` | the physical model refuses: the qubit's error is at or above the code's threshold, no distance up to 51 meets the logical share, or no factory meets the T target |
| `QE07` | a document that does not read: a key or a value that is not one, a symbolic value, a repetition, a schema violation |
| `QE08` | a request that does not read: the program, the budget, the qubit, the calibration, the slowdown or the export |
| `QE08` | a request that does not read: the program, the budget, the qubit, the calibration, the slowdown, the clock or the export; or a reaction time shorter than a measurement on the qubit |
## What it assumes
@ -333,7 +366,9 @@ estimates bit for bit, and the same figures, counts and notes.
uncomputation in a ladder is another Toffoli (measurement-based
uncomputation, which needs none, is not assumed).
- Classical control (the latency of a branch on a measured bit) is not
modelled.
modelled, except as the reaction-limited clock's tick when that clock is
asked for. On either clock every Toffoli follows the one before: a
program's Toffoli depth is not read.
- A rotation under two or more controls (`ctrl(2) @ rz(θ)`, and `rx`, `ry`,
`u` the same) is counted as the reader writes it, through its phase
polynomial, not through ancillas.

View file

@ -105,14 +105,29 @@ Toffoli count. Its title's 8 hours is the re-run's 7.31 hours expected across
retries, taken up to one digit. Its hand-worked §2.14 (23 million qubits)
rounds as it goes and says so. Its §2.12 reads a 6.4% distillation error from
an earlier paper's spreadsheet where its own script's formula gives 0.6%. Our
estimate job, given the same counts, finds the same code distance but takes 12
times as long: it prices the planar architecture of arXiv:2211.07629, three
logical steps of `d` cycles a Toffoli, and has no reaction-limited clock. That
clock is in `quantum_arch` (**our limit** in the job).
estimate job, given the same counts, finds the same code distance on its
default step clock, three logical steps of `d` cycles a Toffoli as the planar
model of arXiv:2211.07629 has it, and takes 12 times as long. On its
reaction-limited clock at the paper's 10 µs, the rule `quantum_arch` takes, it
takes 7.50 hours at d = 25: every Toffoli after the one before. The paper does
not run them so, and counts alone do not carry how it does (**our limit**): its
5.1 hours and its §2.11 "approximately 7 hours" are one parallel basis, 37 ms a
lookup addition, with its two runway pieces' additions side by side and its
lookups at half a beat an entry. §2.11 reaches 7 by rounding its lookup additions
up to 0.1·n_e·n and then 3.7·n_e·n ms up to 4·n_e·n; 37 ms on the unrounded count
is 5.18 hours. The paper prints no serial figure. Its equations, at 10 µs a
Toffoli one after another, give 7.3 hours, a figure of ours, not the paper's. The
job's 20.05 million qubits are near the paper's 20 million by chance: 129 T
factories at d = 25, not its CCZ factories at d = 27.
**FeMoCo.** Its floorplan, factory, Toffoli rate and 3 days come back on the
same clock; its §IV.C.5 says 3.5 days where §IV.C.4 says 3. The estimate job
again runs at the beat.
runs at the beat on its step clock; on its reaction-limited clock it keeps
enough T factories to tick at 10 µs, 0.78 days on 10.8 million qubits, where the
paper's four CCZ factories set its pace at 38.75 µs. Most of that factory count
is the kind of factory, not the tick: at the paper's 38.75 µs the job keeps 41
of its 15-to-1 units, each about ten times slower to a CCZ state than the
paper's.
**RSA-2048 (2025).** As `quantum_arch` documents: its stated durations give
10.6 hours a shot where it prints 12.07, its hot-storage counts disagree with
@ -122,10 +137,9 @@ its own formula, and so on. Its Toffoli count comes back.
## What this does not do
- **The estimate job has no reaction-limited clock.** It prices the planar
architecture, and a laid-out machine that teleports its fixups takes about
a tenth of the time the job gives. `quantum_arch` has that clock; the job
does not offer it.
- **The estimate job runs every Toffoli after the one before.** On either
clock it reads no Toffoli depth, so a program whose Toffolis run side by side
takes less time than it gives.
- **The bicycle machine is not scheduled.** A rotation's time is the mean of
its parts, a few percent under the paper's compiled critical path, and
repeated in-module measurements are not removed.
@ -309,8 +323,11 @@ its own formula, and so on. Its Toffoli count comes back.
| physical qubits, job, assumed code model | arXiv:1905.09749 Table 3; its counts in the estimate job | 20 (megaqubits) | 18,906,836 | -5.47% | our limit | The job lays out `2N + √(8N) + 1` tiles of `2d²` qubits and T factories from 15-to-1; the source a board of `2(d+1)²`-qubit patches and CCZ factories, placed by hand. |
| physical qubits, job, fitted code model | arXiv:1905.09749 Table 3; its counts in the estimate job | 20 (megaqubits) | 18,906,836 | -5.47% | our limit | The job lays out `2N + √(8N) + 1` tiles of `2d²` qubits and T factories from 15-to-1; the source a board of `2(d+1)²`-qubit patches and CCZ factories, placed by hand. |
| physical qubits, the planar model on the source's patch and error rule | arXiv:1905.09749 Table 3; its counts in the estimate job | 20 (megaqubits) | 27,571,456 | +37.86% | our limit | The job lays out `2N + √(8N) + 1` tiles of `2d²` qubits and T factories from 15-to-1; the source a board of `2(d+1)²`-qubit patches and CCZ factories, placed by hand. |
| hours per run, job, assumed code model | arXiv:1905.09749 Table 3; its counts in the estimate job | 5.1 | 60.71 | ×11.904 | our limit | The estimate job prices the planar architecture of arXiv:2211.07629: three logical steps of `d` cycles for each Toffoli. The source teleports a Toffoli's fixups away and runs an addition at one 10 µs tick a Toffoli. The job has no such clock; quantum_arch's (the re-run rows) does. |
| hours per run, the planar model on the source's patch and error rule | arXiv:1905.09749 Table 3; its counts in the estimate job | 5.1 | 69.71 | ×13.668 | our limit | The estimate job prices the planar architecture of arXiv:2211.07629: three logical steps of `d` cycles for each Toffoli. The source teleports a Toffoli's fixups away and runs an addition at one 10 µs tick a Toffoli. The job has no such clock; quantum_arch's (the re-run rows) does. |
| hours per run, job, assumed code model | arXiv:1905.09749 Table 3; its counts in the estimate job | 5.1 | 60.71 | ×11.904 | assumption | On its default step clock the estimate job prices the planar architecture of arXiv:2211.07629: three logical steps of `d` cycles for each Toffoli. The source teleports a Toffoli's fixups away and runs it in one tick; the job does so on its reaction-limited clock (the rows below). |
| hours per run, the planar model on the source's patch and error rule | arXiv:1905.09749 Table 3; its counts in the estimate job | 5.1 | 69.71 | ×13.668 | assumption | On its default step clock the estimate job prices the planar architecture of arXiv:2211.07629: three logical steps of `d` cycles for each Toffoli. The source teleports a Toffoli's fixups away and runs it in one tick; the job does so on its reaction-limited clock (the rows below). |
| code distance, job, reaction-limited clock at 10 µs | arXiv:1905.09749 §2.13 (d = 27); its counts in the estimate job | 27 | 25 | -7.41% | our limit | The tiles wait out 7.50 hours here and 60.71 on the step clock, so a smaller distance meets the same budget. |
| hours per run, job, reaction-limited clock at 10 µs | arXiv:1905.09749 Table 3; its counts in the estimate job | 5.1 | 7.50 | +46.97% | our limit | The job runs each Toffoli in one 10 µs tick, all of them one after another, as its layout runs every step; the source does not, and counts alone do not carry how it does. Its 5.1 hours and its §2.11 "approximately 7 hours" are one parallel basis, 37 ms a lookup addition, with the two runway pieces' additions side by side and the lookups at half a beat an entry. §2.11 reaches 7 by rounding: eq. (1)'s 503,808 lookup additions as 0.1·n_e·n, then 3.7·n_e·n ms as 4·n_e·n, which is 6.99 hours; 37 ms on the unrounded count is 5.18. The source prints no serial figure. 5,207 Toffolis a lookup addition at 10 µs each would be 7.3 hours: that is our derivation from its equations, not its figure. |
| physical qubits, job, reaction-limited clock at 10 µs | arXiv:1905.09749 Table 3; its counts in the estimate job | 20 (megaqubits) | 20,050,780 | +0.25% | our limit | The job lays out `2N + √(8N) + 1` tiles of `2d²` qubits and T factories from 15-to-1; the source a board of `2(d+1)²`-qubit patches and CCZ factories, placed by hand. On this clock the job keeps enough factories (129) that a CCZ state comes every tick, at d = 25: near the source's count by chance, a different machine from its d = 27 and CCZ factories. |
| factories, job | arXiv:1905.09749 §2.10; its counts in the estimate job | 14 CCZ per piece, 28 in all | 16 T factories | — | our limit | A CCZ state costs four T states in the planar model. |
| 1024 bits: megaqubits, re-run | arXiv:1905.09749 Table 3 | 9.7 | 9.62 | -0.78% | agrees | Its script takes each figure up to two digits. |
| 1024 bits: hours per run, re-run | arXiv:1905.09749 Table 3 | 1.3 | 1.26 | -2.79% | agrees | Its script takes each figure up to two digits. |
@ -342,10 +359,12 @@ its own formula, and so on. Its Toffoli count comes back.
| code distance, its layout's counts | arXiv:2011.03494 §IV.C.4; its counts in the estimate job | 31 | 29 | -6.45% | assumption | Its rule is `0.1·(100p)^((d+1)/2)` a cycle (§IV.C.5: "Λ of 10"); the planar model's `0.03·(p/0.01)^((d+1)/2)` a step of `d` cycles, lower by `d/0.3`, one step of distance at this size. |
| physical qubits, job, Table III's counts | arXiv:2011.03494 §IV.C.4; its counts in the estimate job | ≈ 4·10⁶ | 8,419,152 | ×2.105 | our limit | The job lays out `2N + √(8N) + 1` tiles of `2d²` qubits and T factories from 15-to-1; the source a board of `2(d+1)²`-qubit patches and CCZ factories, placed by hand. |
| physical qubits, job, its layout's counts | arXiv:2011.03494 §IV.C.4; its counts in the estimate job | ≈ 4·10⁶ | 3,529,152 | -11.77% | our limit | The job lays out `2N + √(8N) + 1` tiles of `2d²` qubits and T factories from 15-to-1; the source a board of `2(d+1)²`-qubit patches and CCZ factories, placed by hand. |
| days, job, Table III's counts | arXiv:2011.03494 §IV.C.4; its counts in the estimate job | 3 | 5.34 | +77.89% | our limit | The estimate job prices the planar architecture of arXiv:2211.07629: three logical steps of `d` cycles for each Toffoli. The source teleports a Toffoli's fixups away and runs an addition at one 10 µs tick a Toffoli. The job has no such clock; quantum_arch's (the re-run rows) does. |
| days, job, its layout's counts | arXiv:2011.03494 §IV.C.4; its counts in the estimate job | 3 | 6.75 | ×2.250 | our limit | The estimate job prices the planar architecture of arXiv:2211.07629: three logical steps of `d` cycles for each Toffoli. The source teleports a Toffoli's fixups away and runs an addition at one 10 µs tick a Toffoli. The job has no such clock; quantum_arch's (the re-run rows) does. |
| days, job, Table III's counts | arXiv:2011.03494 §IV.C.4; its counts in the estimate job | 3 | 5.34 | +77.89% | assumption | On its default step clock the estimate job prices the planar architecture of arXiv:2211.07629: three logical steps of `d` cycles for each Toffoli. The source teleports a Toffoli's fixups away and runs it in one tick; the job does so on its reaction-limited clock (the rows below). |
| days, job, its layout's counts | arXiv:2011.03494 §IV.C.4; its counts in the estimate job | 3 | 6.75 | ×2.250 | assumption | On its default step clock the estimate job prices the planar architecture of arXiv:2211.07629: three logical steps of `d` cycles for each Toffoli. The source teleports a Toffoli's fixups away and runs it in one tick; the job does so on its reaction-limited clock (the rows below). |
| physical qubits, job, p = 10⁻⁴ | arXiv:2011.03494 §IV.C.5; its counts in the estimate job | "one million" | 968,200 | -3.18% | our limit | The job lays out `2N + √(8N) + 1` tiles of `2d²` qubits and T factories from 15-to-1; the source a board of `2(d+1)²`-qubit patches and CCZ factories, placed by hand. |
| days, job, p = 10⁻⁴ | arXiv:2011.03494 §IV.C.5; its counts in the estimate job | 1.5 | 3.49 | ×2.328 | our limit | The estimate job prices the planar architecture of arXiv:2211.07629: three logical steps of `d` cycles for each Toffoli. The source teleports a Toffoli's fixups away and runs an addition at one 10 µs tick a Toffoli. The job has no such clock; quantum_arch's (the re-run rows) does. |
| days, job, p = 10⁻⁴ | arXiv:2011.03494 §IV.C.5; its counts in the estimate job | 1.5 | 3.49 | ×2.328 | assumption | On its default step clock the estimate job prices the planar architecture of arXiv:2211.07629: three logical steps of `d` cycles for each Toffoli. The source teleports a Toffoli's fixups away and runs it in one tick; the job does so on its reaction-limited clock (the rows below). |
| days, job, its layout's counts, reaction-limited clock at 10 µs | arXiv:2011.03494 §IV.C.4; its counts in the estimate job | 3 | 0.78 | ×0.259 | assumption | The job ticks at 10 µs where the source's four CCZ factories set a pace of 38.75 µs (25 kHz), and that ratio is this delta. It keeps 157 T factories to do so, and the two counts are different machines: each of the job's is a 15-to-1 unit taking 1,568 µs a CCZ state, each of the source's 155. At the source's own 38.75 µs the job keeps 41, so of the ×39 against four the tick is ×3.8 and the kind of factory ×10. The job's frontier slows the ticks to need fewer. |
| physical qubits, job, its layout's counts, reaction-limited clock at 10 µs | arXiv:2011.03494 §IV.C.4; its counts in the estimate job | ≈ 4·10⁶ | 10,799,568 | ×2.700 | assumption | The job lays out `2N + √(8N) + 1` tiles of `2d²` qubits and T factories from 15-to-1; the source a board of `2(d+1)²`-qubit patches and CCZ factories, placed by hand. As above: most of this is the T factories that keep up with the tick; at the source's own 38.75 µs the job needs 4,410,288 qubits. |
### RSA-2048 on yoked surface codes (arXiv:2505.15917, 2025)

View file

@ -43,6 +43,28 @@
"max_slowdown": {
"type": "integer"
},
"clock": {
"description": "How long a Toffoli takes: steps (the default: three logical steps of d cycles), or the reaction-limited clock, a Toffoli one tick of the control system's reaction time in µs, its Clifford fixups teleported away, with enough factories to keep up.",
"anyOf": [
{
"const": "steps"
},
{
"type": "object",
"required": [
"reaction_us"
],
"properties": {
"reaction_us": {
"type": "number",
"exclusiveMinimum": 0,
"exclusiveMaximum": 1000000000
}
},
"additionalProperties": false
}
]
},
"export": {
"enum": [
"counts",
@ -304,6 +326,26 @@
},
"receipt": {
"$ref": "#/$defs/resource_receipt"
},
"clock": {
"type": "object",
"description": "On the reaction-limited clock only: its reaction time, one tick at this estimate's slowdown, and the Toffolis run on ticks. Then steps are the lattice-surgery steps alone, and runtime_s adds the ticks.",
"required": [
"reaction_us",
"tick_us",
"toffolis"
],
"properties": {
"reaction_us": {
"type": "number"
},
"tick_us": {
"type": "number"
},
"toffolis": {
"type": "integer"
}
}
}
}
},
@ -320,6 +362,27 @@
"kind": {
"const": "quantum-resource"
},
"clock": {
"type": "object",
"description": "On the reaction-limited clock only: the runtime the estimate's steps do not account for, read from its signed figures. How long one tick was is the job's, whose hash binds the reaction time.",
"required": [
"kind",
"tick_time_ns_bits",
"tick_time_s"
],
"properties": {
"kind": {
"const": "reaction"
},
"tick_time_ns_bits": {
"type": "string"
},
"tick_time_s": {
"type": "number"
}
},
"additionalProperties": false
},
"estimate": {
"type": "object"
},

View file

@ -28,9 +28,19 @@
//! re-runs the request and holds every receipt and figure to it.
//!
//! The request (`{"qasm"}`, `{"counts"}` or `{"routine"}`, with `budget`,
//! `qubit`, `calibration`, `max_slowdown` and `export`, each optional) is
//! [`Options::read`]'s; `docs/estimate.md` explains every number of the
//! answer and what it assumes.
//! `qubit`, `calibration`, `max_slowdown`, `clock` and `export`, each
//! optional) is [`Options::read`]'s and [`clock_of`]'s; `docs/estimate.md`
//! explains every number of the answer and what it assumes.
//!
//! **The clock.** A Toffoli takes three logical steps of `d` cycles unless the
//! request names the reaction-limited clock, `"clock": {"reaction_us": t}`:
//! then its Clifford fixups are teleported away from the data and it takes one
//! tick of `t` µs, the factories keeping up (`quantum_resource::Clock`, the
//! rule `quantum_arch` takes too), `t` no shorter than a measurement on the
//! qubit ([`clock_refusal`]). A request without a clock is answered and
//! sealed as before, bit for bit; one with the reaction clock is sealed as a
//! clocked job (`quantum_resource_receipt::ClockedJob`), whose hash is a form
//! of its own.
pub mod calibration;
pub mod count;
@ -39,9 +49,9 @@ pub mod interchange;
use self::calibration::Calibration;
use self::interchange::{Imported, RoutineCounts};
use super::synth::rz;
use crate::quantum_resource::{Estimate, EstimateConfig, EstimateError, InstructionSet, IsaCounts, LogicalCounts, PhysicalQubit, QecModel, SearchLimits, UnitKind, layout};
use crate::quantum_resource::{Clock, Estimate, EstimateConfig, EstimateError, InstructionSet, IsaCounts, LogicalCounts, PhysicalQubit, QecModel, SearchLimits, UnitKind, layout};
#[cfg(feature = "quantum_receipt")]
use crate::quantum_resource_receipt::{Counted, Origin, Program, ResourceJob, ResourceReceipt};
use crate::quantum_resource_receipt::{ClockedJob, Counted, Origin, Program, ResourceJob, ResourceReceipt};
use serde_json::{Value, json};
/// The answer's format.
@ -75,6 +85,42 @@ pub fn preset(name: &str) -> Option<PhysicalQubit> {
})
}
/// The clock a request asks for: `"clock": "steps"` (the default, three
/// logical steps a Toffoli) or `{"reaction_us": t}` (a Toffoli a tick of the
/// control system's reaction time, `t` µs, its fixups teleported away).
pub fn clock_of(request: &Value) -> Result<Clock, String> {
let refused = || "\"clock\" is \"steps\" (the default: three logical steps a Toffoli) or {\"reaction_us\": t}: a Toffoli a tick of the reaction time, t µs, above 0 and below 1e9 [QE08]".to_string();
match request.get("clock") {
None | Some(Value::Null) => Ok(Clock::Steps),
Some(Value::String(s)) if s == "steps" => Ok(Clock::Steps),
Some(Value::Object(o)) if o.len() == 1 => {
let t = o.get("reaction_us").and_then(Value::as_f64).filter(|t| *t > 0.0 && *t < 1e9).ok_or_else(refused)?;
Ok(Clock::Reaction { reaction_ns: t * 1e3 })
}
_ => Err(refused()),
}
}
/// Why `clock` cannot run on `qubit`, if it cannot. A reaction includes the
/// logical measurement it reacts to (arXiv:1905.09749 App. B.1), so a reaction
/// time shorter than one measurement on the qubit describes no machine.
pub fn clock_refusal(clock: Clock, qubit: &PhysicalQubit) -> Option<String> {
match clock {
Clock::Reaction { reaction_ns } if reaction_ns < qubit.meas_ns => Some(format!(
"a reaction time of {} µs is shorter than a measurement on this qubit, {} µs, and a reaction includes the measurement it reacts to [QE08]",
reaction_ns / 1e3,
qubit.meas_ns / 1e3
)),
_ => None,
}
}
/// The reaction time as the request wrote it, in µs: what the answer echoes.
/// (`t·10³/10³` is not always `t`.)
fn reaction_us_of(request: &Value) -> Option<f64> {
request.get("clock")?.get("reaction_us")?.as_f64()
}
/// The angles a document's rotations are costed at, which it does not give:
/// `(2k + 1)·π/17` for `k = 0..8`, none a multiple of `π/4`. A rotation then
/// costs the most `T` gates any of them needs.
@ -141,7 +187,7 @@ impl Options {
pub fn read(v: &Value) -> Result<Options, String> {
let mut o = Options::default();
if !v.is_object() {
return Err("an estimate's request is an object: {\"qasm\" | \"counts\" | \"routine\", \"budget\", \"qubit\", \"calibration\", \"max_slowdown\", \"export\"} [QE08]".into());
return Err("an estimate's request is an object: {\"qasm\" | \"counts\" | \"routine\", \"budget\", \"qubit\", \"calibration\", \"max_slowdown\", \"clock\", \"export\"} [QE08]".into());
}
if let Some(b) = v.get("budget").filter(|x| !x.is_null()) {
o.budget = b.as_f64().filter(|x| *x > 0.0 && *x < 1.0).ok_or("\"budget\" is the probability the run may fail, between 0 and 1 (exclusive) [QE08]")?;
@ -463,7 +509,7 @@ fn model_refusal(e: &EstimateError) -> String {
EstimateError::AboveThreshold => "the qubit's error rate is at or above the code's threshold, so no distance helps",
EstimateError::DistanceExceeded => "no code distance up to 51 meets the logical share of the budget",
EstimateError::NoFactory => "no T factory of up to three rounds and distance 51 meets the per-state target",
EstimateError::Invalid => "the counts or the budget are not ones the model takes",
EstimateError::Invalid => "the counts, the budget or the clock are not ones the model takes, or the machine's qubits do not fit in 64 bits",
EstimateError::Mismatch => "the code and the qubit use different instruction sets",
EstimateError::NotRun => "the estimate was not run",
};
@ -508,6 +554,18 @@ pub fn figures(e: &Estimate, slowdown: u64) -> Value {
})
}
/// [`figures`], with the reaction time `reaction_us` the request wrote if it
/// names the reaction-limited clock: they then add the clock's,
/// `{"reaction_us", "tick_us", "toffolis"}`, and `steps` are the
/// lattice-surgery steps alone.
pub fn figures_clocked(e: &Estimate, slowdown: u64, reaction_us: Option<f64>, toffolis: u64) -> Value {
let mut v = figures(e, slowdown);
if let Some(t) = reaction_us {
v["clock"] = json!({ "reaction_us": t, "tick_us": t * slowdown as f64, "toffolis": toffolis });
}
v
}
/// One estimate of the frontier: its code, slowdown and figures.
#[derive(Clone, Debug, PartialEq)]
pub struct Point {
@ -521,14 +579,19 @@ pub struct Point {
/// `max_slowdown` on each code that no other beats on both physical qubits
/// and runtime, fastest first. The headline is one of them.
pub fn frontier(isa: &IsaCounts, qubit: &PhysicalQubit, codes: &[QecModel], budget: f64, max_slowdown: u64) -> Result<(Vec<Point>, usize), String> {
frontier_clocked(isa, 0, qubit, codes, budget, max_slowdown, Clock::Steps)
}
/// [`frontier`] on `clock`, the program's `toffolis` among its counts.
pub fn frontier_clocked(isa: &IsaCounts, toffolis: u64, qubit: &PhysicalQubit, codes: &[QecModel], budget: f64, max_slowdown: u64, clock: Clock) -> Result<(Vec<Point>, usize), String> {
let cfg = |s: u64| EstimateConfig { error_budget: budget, slowdown: s, limits: SearchLimits::default() };
let mut all: Vec<Point> = Vec::new();
let mut last = None;
for code in codes {
let mut s = 1;
while s <= max_slowdown {
match crate::quantum_resource::estimate_isa(isa, qubit, code, &cfg(s)) {
Ok(e) => all.push(Point { code: code.clone(), slowdown: s, estimate: e }),
match crate::quantum_resource::estimate_isa_clocked(isa, toffolis, qubit, code, &cfg(s), clock) {
Ok(c) => all.push(Point { code: code.clone(), slowdown: s, estimate: c.estimate }),
Err(e) => last = Some(e),
}
s *= 2;
@ -578,12 +641,24 @@ pub struct Evaluation {
/// answer they make (not yet sealed); or why it is refused, with a code.
pub fn evaluate(request: &Value) -> Result<Evaluation, String> {
let options = Options::read(request)?;
let clock = clock_of(request)?;
if let Some(refusal) = clock_refusal(clock, &options.qubit) {
return Err(refusal);
}
let reaction_us = reaction_us_of(request);
let derived = derive(request, options.budget)?;
let codes = options.calibration.codes(&options.qubit);
let (points, headline) = frontier(&derived.isa, &options.qubit, &codes, options.budget, options.max_slowdown)?;
let toffolis = derived.logical.ccz;
let (points, headline) = frontier_clocked(&derived.isa, toffolis, &options.qubit, &codes, options.budget, options.max_slowdown, clock)?;
let mut notes = derived.notes.clone();
notes.push("Each rotation is a Clifford+T word found by exact search (about 3·log₂(1/ε) T gates); synthesis that measures and falls back reaches fewer T gates and is not assumed.".into());
notes.push("The physical model is the planar one of quantum_resource (parallel-synthesis sequential Pauli computation, 15-to-1 factories found by exhaustive search), with its assumptions: uniform, independent circuit noise; free transport of T states; smooth T consumption.".into());
if let Some(t) = reaction_us {
notes.push(format!(
"On the reaction-limited clock: each of the {toffolis} Toffoli(s) has its Clifford fixups teleported away from the data and takes one tick of the reaction time, {} µs (stretched as the steps are, slowed), one after another, with enough factories that a CCZ state comes every tick; the other operations take their logical steps. The steps counted are the lattice-surgery steps alone.",
t
));
}
let cal = options.calibration.describe(&options.qubit);
if cal["applies"] == true && cal["extrapolated"] == true {
notes.push(format!(
@ -602,10 +677,13 @@ pub fn evaluate(request: &Value) -> Result<Evaluation, String> {
"epsilon": derived.epsilon,
"calibration": cal,
"options": options.to_json(),
"estimate": figures(&h.estimate, h.slowdown),
"frontier": points.iter().map(|p| figures(&p.estimate, p.slowdown)).collect::<Vec<_>>(),
"estimate": figures_clocked(&h.estimate, h.slowdown, reaction_us, toffolis),
"frontier": points.iter().map(|p| figures_clocked(&p.estimate, p.slowdown, reaction_us, toffolis)).collect::<Vec<_>>(),
"notes": notes,
});
if let Some(t) = reaction_us {
result["options"]["clock"] = json!({ "reaction_us": t });
}
if derived.rotations.is_array() {
result["rotations"] = derived.rotations.clone();
}
@ -646,6 +724,11 @@ impl Evaluation {
config: EstimateConfig { error_budget: self.options.budget, slowdown: self.points[i].slowdown, limits: SearchLimits::default() },
}
}
/// [`Evaluation::job`] on `clock`: on the step clock, the job itself.
pub fn clocked_job(&self, i: usize, clock: Clock) -> ClockedJob {
ClockedJob { job: self.job(i), clock }
}
}
/// `request` estimated and sealed with `key`: `{"request", "result"}`, each
@ -656,13 +739,14 @@ impl Evaluation {
pub fn sealed(request: &Value, key: &ed25519_dalek::SigningKey) -> Result<Value, String> {
super::job::exact_request(request)?;
let ev = evaluate(request)?;
let clock = clock_of(request)?;
let did = crate::quantum_receipt::did_key(&key.verifying_key());
let unmetered = |run: &mut dyn FnMut()| {
run();
0
};
let receipt_of = |r: &ResourceReceipt| serde_json::from_str::<Value>(&r.to_json()).expect("a receipt's JSON reads");
let head = ResourceReceipt::seal(key, did.clone(), &ev.job(ev.headline), None, unmetered).map_err(|e| model_refusal(&e))?;
let head = ResourceReceipt::seal_clocked(key, did.clone(), &ev.clocked_job(ev.headline, clock), None, unmetered).map_err(|e| model_refusal(&e))?;
let mut result = ev.result.clone();
result["estimate"]["receipt"] = receipt_of(&head);
for i in 0..ev.points.len() {
@ -670,7 +754,7 @@ pub fn sealed(request: &Value, key: &ed25519_dalek::SigningKey) -> Result<Value,
result["frontier"][i]["headline"] = json!(true);
continue;
}
let r = ResourceReceipt::seal(key, did.clone(), &ev.job(i), Some(head.receipt_hash()), unmetered).map_err(|e| model_refusal(&e))?;
let r = ResourceReceipt::seal_clocked(key, did.clone(), &ev.clocked_job(i, clock), Some(head.receipt_hash()), unmetered).map_err(|e| model_refusal(&e))?;
result["frontier"][i]["receipt"] = receipt_of(&r);
}
// The engine that made it, and the schema it follows, inside the seal.
@ -692,6 +776,7 @@ pub fn verify(body: &Value) -> Result<Value, String> {
return Err(format!("not an estimate: its result's format is not {FORMAT}"));
}
let ev = evaluate(request)?;
let clock = clock_of(request)?;
let parse = |v: &Value| ResourceReceipt::from_json(&v.to_string());
let key_bytes = key.as_str().and_then(|k| (0..32).map(|i| u8::from_str_radix(k.get(2 * i..2 * i + 2)?, 16).ok()).collect::<Option<Vec<u8>>>());
let mine = |r: &ResourceReceipt| key_bytes.as_deref() == Some(r.signer_pubkey.as_slice()) && r.verify();
@ -699,7 +784,7 @@ pub fn verify(body: &Value) -> Result<Value, String> {
let mut receipts = frontier.len() == ev.points.len();
let head = parse(&result["estimate"]["receipt"]);
match &head {
Some(h) => receipts &= mine(h) && h.parent_receipt_hash.is_none() && h.computation_matches(&ev.job(ev.headline)),
Some(h) => receipts &= mine(h) && h.parent_receipt_hash.is_none() && h.computation_matches_clocked(&ev.clocked_job(ev.headline, clock)),
None => receipts = false,
}
for (i, f) in frontier.iter().enumerate().take(ev.points.len()) {
@ -708,7 +793,7 @@ pub fn verify(body: &Value) -> Result<Value, String> {
continue;
}
receipts &= match (parse(&f["receipt"]), &head) {
(Some(r), Some(h)) => mine(&r) && r.parent_receipt_hash == Some(h.receipt_hash()) && r.computation_matches(&ev.job(i)),
(Some(r), Some(h)) => mine(&r) && r.parent_receipt_hash == Some(h.receipt_hash()) && r.computation_matches_clocked(&ev.clocked_job(i, clock)),
_ => false,
};
}
@ -756,13 +841,17 @@ pub fn properties() -> Value {
], "description": "The physical qubit: a reference qubit (ns-e4, the default: 50 ns gates, 1e-4 error; us-* are 100 µs gates, meas-* measurement-based), or one described." },
"calibration": { "type": "string", "enum": ["exact_matching", "union_find", "assumed"], "description": "The gate-based surface code's model: fitted to this engine's exact-matching decoder (default) or its union-find decoder, or the planar model's assumed one." },
"max_slowdown": { "type": "integer", "minimum": 1, "maximum": MAX_SLOWDOWN, "description": "The most the frontier slows the program by to need fewer factories (default 256)." },
"clock": { "anyOf": [
{ "type": "string", "enum": ["steps"] },
{ "type": "object", "properties": { "reaction_us": { "type": "number", "exclusiveMinimum": 0, "exclusiveMaximum": 1e9 } }, "required": ["reaction_us"], "additionalProperties": false }
], "description": "How long a Toffoli takes: steps (the default: three logical steps of d cycles), or {\"reaction_us\": t}, the reaction-limited clock: its Clifford fixups teleported away, one tick of the control system's reaction time t µs (never shorter than a measurement on the qubit), with enough factories to keep up." },
"export": { "type": "string", "enum": ["counts", "routine"], "description": "Also give the program's counts as a logical-counts or a routine document, under export." },
"name": { "type": "string", "description": "The program's name in a routine document." }
})
}
/// What an agent is told of the `estimate_resources` tool, on both MCP servers.
pub const DESCRIPTION: &str = "What a fault-tolerant quantum computer would need to run a program. First its logical counts: T gates, Toffolis, rotations (each costed at the T count of its own synthesised Clifford+T word, within its share of the budget), measurements, T depth and total depth, and logical qubits with the ancillas that gates under several controls need. Then, on a physical qubit, the planar model's code distance (the gate-based surface code's model fitted to this engine's own decoders), T factories, physical qubits and runtime, and the frontier of estimates trading qubits for time. Takes a program, or a logical-counts or routine document. Every estimate is sealed in the resource-estimate receipt family, binding the program's digest and the ε budget; the answer's notes state the model's assumptions. Quote the figures with the qubit and the budget, and call them an estimate.";
pub const DESCRIPTION: &str = "What a fault-tolerant quantum computer would need to run a program. First its logical counts: T gates, Toffolis, rotations (each costed at the T count of its own synthesised Clifford+T word, within its share of the budget), measurements, T depth and total depth, and logical qubits with the ancillas that gates under several controls need. Then, on a physical qubit, the planar model's code distance (the gate-based surface code's model fitted to this engine's own decoders), T factories, physical qubits and runtime, and the frontier of estimates trading qubits for time. Takes a program, or a logical-counts or routine document. Every estimate is sealed in the resource-estimate receipt family, binding the program's digest and the ε budget; the answer's notes state the model's assumptions. Quote the figures with the qubit, the budget and the clock, and call them an estimate.";
/// The service's form, unsealed: the answer, or why it is refused. A host
/// with a key seals with [`sealed`] instead.
@ -820,6 +909,131 @@ mod tests {
assert_eq!(u.points[u.headline].code.threshold, Calibration::UnionFind.fit().unwrap().threshold);
}
#[test]
fn without_a_clock_the_answer_is_the_step_clock_s_and_names_none() {
let req = json!({ "counts": { "numQubits": 100, "cczCount": 1_000_000 }, "qubit": "us-e4" });
let a = call(&req).unwrap();
let mut steps = req.clone();
steps["clock"] = json!("steps");
assert_eq!(call(&steps).unwrap(), a);
assert!(a["estimate"].get("clock").is_none() && a["options"].get("clock").is_none());
for bad in [json!("fast"), json!({ "reaction_us": 0 }), json!({ "reaction_us": 1e9 }), json!({ "reaction_us": 10, "x": 1 }), json!({ "reaction": 10 }), json!(10)] {
let mut r = req.clone();
r["clock"] = bad.clone();
assert!(call(&r).unwrap_err().contains("[QE08]"), "{bad}");
}
}
/// A reaction includes a measurement: the 100 µs qubit takes no 10 µs
/// reaction time, and is refused for that reason, not sealed with a tick
/// shorter than anything it can do.
#[test]
fn a_reaction_time_shorter_than_a_measurement_is_refused() {
let mut req = json!({ "counts": { "numQubits": 100, "cczCount": 1_000_000 }, "qubit": "us-e3", "clock": { "reaction_us": 10 } });
let refusal = call(&req).unwrap_err();
assert!(refusal.contains("[QE08]") && refusal.contains("a reaction includes the measurement"), "{refusal}");
assert!(sealed(&req, &key()).is_err(), "nothing is sealed");
req["clock"] = json!({ "reaction_us": 100 });
assert_eq!(call(&req).unwrap()["options"]["clock"], json!({ "reaction_us": 100.0 }));
// Past the floor, on a qubit described to allow it, a machine too big
// for a count is refused where it once wrapped into a signed receipt.
let mut far = rsa_2021(Some(json!({ "reaction_us": 1e-15 })));
far["qubit"]["meas_ns"] = json!(1e-13);
far["max_slowdown"] = json!(1);
let refusal = sealed(&far, &key()).unwrap_err();
assert!(refusal.contains("do not fit in 64 bits"), "{refusal}");
}
/// The answer says the reaction time the request wrote: `t·10³/10³` is not
/// always `t`.
#[test]
fn the_answer_echoes_the_reaction_time_as_written() {
let t = 42.6591;
assert_ne!(t * 1e3 / 1e3, t, "a time that does not survive the conversion");
let a = call(&rsa_2021(Some(json!({ "reaction_us": t })))).unwrap();
let e = &a["estimate"];
assert_eq!(a["options"]["clock"]["reaction_us"].as_f64(), Some(t));
assert_eq!(e["clock"]["reaction_us"].as_f64(), Some(t));
assert_eq!(e["clock"]["tick_us"].as_f64(), Some(t * e["slowdown"].as_f64().unwrap()));
assert!(a["notes"].as_array().unwrap().iter().any(|n| n.as_str().unwrap().contains("42.6591 µs")), "{}", a["notes"]);
}
/// The tool's schema, the answer's and `clock_of` say one thing.
#[test]
fn the_schemas_say_of_the_clock_what_the_engine_takes() {
let committed: Value = serde_json::from_str(include_str!("../../../schemas/estimate.v1.json")).unwrap();
let tool = properties();
for object in [&tool["clock"]["anyOf"][1], &committed["properties"]["request"]["properties"]["clock"]["anyOf"][1]] {
assert_eq!(object["additionalProperties"], false, "{object}");
let t = &object["properties"]["reaction_us"];
assert_eq!((t["exclusiveMinimum"].as_f64(), t["exclusiveMaximum"].as_f64()), (Some(0.0), Some(1e9)), "{object}");
}
for (t, taken) in [(0.0, false), (1e-3, true), (999_999_999.0, true), (1e9, false)] {
assert_eq!(clock_of(&json!({ "clock": { "reaction_us": t } })).is_ok(), taken, "{t}");
}
assert!(clock_of(&json!({ "clock": { "reaction_us": 1e9 } })).unwrap_err().contains("below 1e9"));
}
/// arXiv:1905.09749's counts (its re-run's 2.698·10⁹ Toffolis on 6,189
/// logical qubits) on its 1 µs, 10⁻³ qubit: the step clock gives about 61
/// hours, the reaction clock the Toffolis one after another at 10 µs.
fn rsa_2021(clock: Option<Value>) -> Value {
let mut r = json!({
"counts": { "numQubits": 6189, "cczCount": 2_698_000_000u64 },
"qubit": { "name": "1 µs cycle", "instruction_set": "gate", "gate_ns": 1000.0 / 6.0, "meas_ns": 1000.0 / 6.0, "clifford_error": 1e-3, "t_error": 1e-3 },
"budget": 0.31,
"calibration": "assumed",
"max_slowdown": 4,
});
if let Some(c) = clock {
r["clock"] = c;
}
r
}
#[test]
fn the_reaction_clock_runs_each_toffoli_in_a_tick_with_factories_to_keep_up() {
let (a, b) = (call(&rsa_2021(None)).unwrap(), call(&rsa_2021(Some(json!({ "reaction_us": 10 })))).unwrap());
let hours = |v: &Value| v["estimate"]["runtime_s"].as_f64().unwrap() / 3600.0;
assert!(hours(&a) > 50.0, "{}", hours(&a));
// 2.698·10⁹ ticks of 10 µs: 7.49 hours, and no other step.
assert_eq!(b["estimate"]["steps"], 0);
assert!((hours(&b) - 2.698e9 * 10e-6 / 3600.0).abs() < 1e-9, "{}", hours(&b));
assert_eq!(b["estimate"]["clock"], json!({ "reaction_us": 10.0, "tick_us": 10.0, "toffolis": 2_698_000_000u64 }));
assert_eq!(b["options"]["clock"], json!({ "reaction_us": 10.0 }));
// More factories than the step clock's, to keep up with the ticks
// (quantum_resource's test holds the supply to the tick).
assert!(b["estimate"]["factories"].as_u64().unwrap() > a["estimate"]["factories"].as_u64().unwrap());
// Slowed, the ticks stretch with the steps.
for p in b["frontier"].as_array().unwrap() {
let s = p["slowdown"].as_f64().unwrap();
assert_eq!(p["clock"]["tick_us"].as_f64().unwrap(), 10.0 * s);
}
assert!(b["notes"].as_array().unwrap().iter().any(|n| n.as_str().unwrap().starts_with("On the reaction-limited clock")));
}
#[cfg(feature = "quantum_receipt")]
#[test]
fn a_reaction_clock_estimate_is_sealed_as_a_clocked_job() {
let req = rsa_2021(Some(json!({ "reaction_us": 10 })));
let body = sealed(&req, &key()).unwrap();
assert_eq!(body["result"]["estimate"]["receipt"]["clock"]["kind"], "reaction", "the receipt says so itself");
let v = verify(&body).unwrap();
assert_eq!((v["valid"].clone(), v["receipts"].clone(), v["reproduced"].clone()), (json!(true), json!(true), json!(true)));
let ev = evaluate(&req).unwrap();
let r = ResourceReceipt::from_json(&body["result"]["estimate"]["receipt"].to_string()).unwrap();
assert!(r.computation_matches_clocked(&ev.clocked_job(ev.headline, clock_of(&req).unwrap())));
assert!(!r.computation_matches(&ev.job(ev.headline)), "not the step clock's job");
// The step clock's request seals what it sealed before clocks were named.
let plain = rsa_2021(None);
let ev = evaluate(&plain).unwrap();
let body = sealed(&plain, &key()).unwrap();
assert!(body["result"]["estimate"]["receipt"].get("clock").is_none());
let r = ResourceReceipt::from_json(&body["result"]["estimate"]["receipt"].to_string()).unwrap();
assert!(r.computation_matches(&ev.job(ev.headline)));
assert_eq!(r.job_hash, ev.job(ev.headline).job_hash());
}
#[test]
fn the_frontier_is_pareto_and_holds_the_headline() {
for q in QUBITS {

View file

@ -559,16 +559,26 @@ struct Planar {
physical: f64,
factories: u64,
runtime_h: f64,
/// One factory's time for a CCZ state, four T states, µs.
ccz_us: f64,
}
fn job(counts: Value, budget: f64, p: f64, calibration: &str) -> Result<Planar, String> {
let request = json!({
job_on(counts, budget, p, calibration, None)
}
/// [`job`] on the reaction-limited clock, `reaction_us` a tick, when given.
fn job_on(counts: Value, budget: f64, p: f64, calibration: &str, reaction_us: Option<f64>) -> Result<Planar, String> {
let mut request = json!({
"counts": counts,
"budget": budget,
"qubit": { "name": "1 µs cycle", "instruction_set": "gate", "gate_ns": 1000.0 / 6.0, "meas_ns": 1000.0 / 6.0, "clifford_error": p, "t_error": p },
"calibration": calibration,
"max_slowdown": 1,
});
if let Some(t) = reaction_us {
request["clock"] = json!({ "reaction_us": t });
}
let answer = estimate::call(&request)?;
let e = &answer["estimate"];
Ok(Planar {
@ -576,9 +586,44 @@ fn job(counts: Value, budget: f64, p: f64, calibration: &str) -> Result<Planar,
physical: e["physical_qubits"].as_f64().unwrap_or(0.0),
factories: e["factories"].as_u64().unwrap_or(0),
runtime_h: e["runtime_s"].as_f64().unwrap_or(0.0) / 3600.0,
ccz_us: 4.0 * e["factory"]["duration_us"].as_f64().unwrap_or(0.0),
})
}
/// arXiv:1905.09749 §2.11's arithmetic at `n = 2048`, `n_e = 1.5·n`, on its
/// §2.8's settings. Its 37 ms a lookup addition is a lookup at half a beat an
/// entry and one runway piece's addition, the two pieces side by side: the
/// parallel basis of its 5.1 hours. Its "approximately 7 hours" is that basis
/// rounded. It prints no serial figure; `serial_h` is ours.
struct Section211 {
/// Eq. (1) without its `≈ 0.1·n_e·n`.
lookup_additions: f64,
/// 37 ms on that count.
unrounded_h: f64,
/// Eq. (5)'s `4·n_e·n` ms, as §2.11 takes it.
rounded_h: f64,
/// Toffolis a lookup addition, from §2.8. It prints the lookup's term as
/// `2^(c_exp + c_pad)`; the lookup is `2^(c_exp + c_mul)` Toffolis (§2.5),
/// which its depth, eq. (3), has too.
toffolis_each: f64,
/// Our derivation: those Toffolis one after another, 10 µs each.
serial_h: f64,
}
fn section_2_11() -> Section211 {
let (n, n_e, c_exp, c_mul, c_sep) = (2048.0_f64, 3072.0_f64, 5.0_f64, 5.0_f64, 1024.0_f64);
let c_pad = 2.0 * n.log2() + n_e.log2() + 10.0;
let lookup_additions = 2.0 * n * n_e / (c_exp * c_mul) * (c_sep + 1.0) / c_sep;
let toffolis_each = 2.0 * n + n * c_pad / c_sep + 2.0_f64.powf(c_exp + c_mul);
Section211 {
lookup_additions,
unrounded_h: lookup_additions * 37.0 / 3.6e6,
rounded_h: 4.0 * n_e * n / 3.6e6,
toffolis_each,
serial_h: lookup_additions * toffolis_each * 10.0 / 3.6e9,
}
}
/// The planar model on the source's own assumptions: `2(d+1)²` qubits a patch,
/// and `0.1·(100p)^((d+1)/2)` per cycle, at the distance the source uses.
fn their_noise(at_distance: f64) -> QecModel {
@ -589,8 +634,8 @@ fn one_us(p: f64) -> PhysicalQubit {
PhysicalQubit { name: "1 µs cycle".into(), instruction_set: InstructionSet::GateBased, gate_ns: 1000.0 / 6.0, meas_ns: 1000.0 / 6.0, clifford_error: p, t_error: p }
}
/// What the estimate job leaves out against a laid-out surface-code machine.
const JOB_TIME: &str = "The estimate job prices the planar architecture of arXiv:2211.07629: three logical steps of `d` cycles for each Toffoli. The source teleports a Toffoli's fixups away and runs an addition at one 10 µs tick a Toffoli. The job has no such clock; quantum_arch's (the re-run rows) does.";
/// The estimate job on its default clock, against a laid-out machine.
const JOB_TIME: &str = "On its default step clock the estimate job prices the planar architecture of arXiv:2211.07629: three logical steps of `d` cycles for each Toffoli. The source teleports a Toffoli's fixups away and runs it in one tick; the job does so on its reaction-limited clock (the rows below).";
const JOB_SPACE: &str = "The job lays out `2N + √(8N) + 1` tiles of `2d²` qubits and T factories from 15-to-1; the source a board of `2(d+1)²`-qubit patches and CCZ factories, placed by hand.";
fn rsa_2021() -> Result<Section, String> {
@ -628,8 +673,41 @@ fn rsa_2021() -> Result<Section, String> {
r.num("r21.job.qubits.assumed", "physical qubits, job, assumed code model", assumed.physical, F::I, Class::OurLimit, JOB_SPACE);
r.num("r21.job.qubits.fitted", "physical qubits, job, fitted code model", fitted.physical, F::I, Class::OurLimit, JOB_SPACE);
r.num("r21.job.qubits.source_rule", "physical qubits, the planar model on the source's patch and error rule", source_noise.physical_qubits as f64, F::I, Class::OurLimit, JOB_SPACE);
r.num("r21.job.hours.assumed", "hours per run, job, assumed code model", assumed.runtime_h, F::D(2), Class::OurLimit, JOB_TIME);
r.num("r21.job.hours.source_rule", "hours per run, the planar model on the source's patch and error rule", source_noise.runtime_ns / 3.6e12, F::D(2), Class::OurLimit, JOB_TIME);
r.num("r21.job.hours.assumed", "hours per run, job, assumed code model", assumed.runtime_h, F::D(2), Class::Assumption, JOB_TIME);
r.num("r21.job.hours.source_rule", "hours per run, the planar model on the source's patch and error rule", source_noise.runtime_ns / 3.6e12, F::D(2), Class::Assumption, JOB_TIME);
let fast = job_on(json!({ "numQubits": 6189, "cczCount": toffolis }), 0.31, 1e-3, "assumed", Some(10.0))?;
let s = section_2_11();
r.num(
"r21.job.distance.reaction",
"code distance, job, reaction-limited clock at 10 µs",
fast.distance as f64,
F::I,
Class::OurLimit,
format!("The tiles wait out {} hours here and {} on the step clock, so a smaller distance meets the same budget.", print(fast.runtime_h, F::D(2)), print(assumed.runtime_h, F::D(2))),
);
r.num(
"r21.job.hours.reaction",
"hours per run, job, reaction-limited clock at 10 µs",
fast.runtime_h,
F::D(2),
Class::OurLimit,
format!(
"The job runs each Toffoli in one 10 µs tick, all of them one after another, as its layout runs every step; the source does not, and counts alone do not carry how it does. Its 5.1 hours and its §2.11 \"approximately 7 hours\" are one parallel basis, 37 ms a lookup addition, with the two runway pieces' additions side by side and the lookups at half a beat an entry. §2.11 reaches 7 by rounding: eq. (1)'s {} lookup additions as 0.1·n_e·n, then 3.7·n_e·n ms as 4·n_e·n, which is {} hours; 37 ms on the unrounded count is {}. The source prints no serial figure. {} Toffolis a lookup addition at 10 µs each would be {} hours: that is our derivation from its equations, not its figure.",
print(s.lookup_additions, F::I),
print(s.rounded_h, F::D(2)),
print(s.unrounded_h, F::D(2)),
print(s.toffolis_each, F::I),
print(s.serial_h, F::D(1))
),
);
r.num(
"r21.job.qubits.reaction",
"physical qubits, job, reaction-limited clock at 10 µs",
fast.physical,
F::I,
Class::OurLimit,
format!("{JOB_SPACE} On this clock the job keeps enough factories ({}) that a CCZ state comes every tick, at d = {}: near the source's count by chance, a different machine from its d = 27 and CCZ factories.", fast.factories, fast.distance),
);
r.text("r21.job.factories", "factories, job", format!("{} T factories", assumed.factories), Class::OurLimit, "A CCZ state costs four T states in the planar model.");
for n in [1024, 3072, 4096, 8192, 12288, 16384] {
@ -667,10 +745,36 @@ fn femoco() -> Result<Section, String> {
r.num("femoco.job.distance", "code distance, its layout's counts", b.distance as f64, F::I, Class::Assumption, "Its rule is `0.1·(100p)^((d+1)/2)` a cycle (§IV.C.5: \"Λ of 10\"); the planar model's `0.03·(p/0.01)^((d+1)/2)` a step of `d` cycles, lower by `d/0.3`, one step of distance at this size.");
r.num("femoco.job.qubits.table", "physical qubits, job, Table III's counts", a.physical, F::I, Class::OurLimit, JOB_SPACE);
r.num("femoco.job.qubits.layout", "physical qubits, job, its layout's counts", b.physical, F::I, Class::OurLimit, JOB_SPACE);
r.num("femoco.job.days.table", "days, job, Table III's counts", a.runtime_h / 24.0, F::D(2), Class::OurLimit, JOB_TIME);
r.num("femoco.job.days.layout", "days, job, its layout's counts", b.runtime_h / 24.0, F::D(2), Class::OurLimit, JOB_TIME);
r.num("femoco.job.days.table", "days, job, Table III's counts", a.runtime_h / 24.0, F::D(2), Class::Assumption, JOB_TIME);
r.num("femoco.job.days.layout", "days, job, its layout's counts", b.runtime_h / 24.0, F::D(2), Class::Assumption, JOB_TIME);
r.num("femoco.job.qubits.e4", "physical qubits, job, p = 10⁻⁴", low.physical, F::I, Class::OurLimit, JOB_SPACE);
r.num("femoco.job.days.e4", "days, job, p = 10⁻⁴", low.runtime_h / 24.0, F::D(2), Class::OurLimit, JOB_TIME);
r.num("femoco.job.days.e4", "days, job, p = 10⁻⁴", low.runtime_h / 24.0, F::D(2), Class::Assumption, JOB_TIME);
let on_clock = |t: f64| job_on(json!({ "numQubits": 700, "cczCount": toffolis as u64 }), 0.01, 1e-3, "assumed", Some(t));
let (fast, paced) = (on_clock(10.0)?, on_clock(38.75)?);
r.num(
"femoco.job.days.reaction",
"days, job, its layout's counts, reaction-limited clock at 10 µs",
fast.runtime_h / 24.0,
F::D(2),
Class::Assumption,
format!(
"The job ticks at 10 µs where the source's four CCZ factories set a pace of 38.75 µs (25 kHz), and that ratio is this delta. It keeps {} T factories to do so, and the two counts are different machines: each of the job's is a 15-to-1 unit taking {} µs a CCZ state, each of the source's 155. At the source's own 38.75 µs the job keeps {}, so of the ×{} against four the tick is ×{} and the kind of factory ×{}. The job's frontier slows the ticks to need fewer.",
fast.factories,
print(fast.ccz_us, F::I),
paced.factories,
print(fast.factories as f64 / 4.0, F::D(0)),
print(fast.factories as f64 / paced.factories as f64, F::D(1)),
print(paced.factories as f64 / 4.0, F::D(0))
),
);
r.num(
"femoco.job.qubits.reaction",
"physical qubits, job, its layout's counts, reaction-limited clock at 10 µs",
fast.physical,
F::I,
Class::Assumption,
format!("{JOB_SPACE} As above: most of this is the T factories that keep up with the tick; at the source's own 38.75 µs the job needs {} qubits.", print(paced.physical, F::I)),
);
Ok(section("FeMoCo on the surface code (arXiv:2011.03494, PRX Quantum 2, 030305, 2021)", r))
}
@ -839,6 +943,46 @@ mod tests {
assert!(page[start..stop] == want, "the page differs from the estimators: run the example and read the difference");
}
/// §2.11's 37 ms is the 5.1 hours' own parallel basis, and it reaches its
/// "approximately 7 hours" by rounding. The source prints no serial figure,
/// so the one here is ours, and the text must say so.
#[test]
fn section_2_11_is_the_parallel_basis_rounded_and_the_serial_figure_is_ours() {
let s = section_2_11();
assert_eq!(print(s.lookup_additions, F::I), "503,808");
assert_eq!(print(s.toffolis_each, F::I), "5,207");
assert_eq!(print(s.unrounded_h, F::D(2)), "5.18", "the same basis, unrounded: its 5.1 hours");
assert_eq!(print(s.rounded_h, F::D(2)), "6.99", "eq. (5): its 7 hours");
assert_eq!(print(s.serial_h, F::D(1)), "7.3");
// The page's prose, which the row test does not reach.
let page: String = include_str!("../docs/published-architectures.md").split_whitespace().collect::<Vec<_>>().join(" ");
assert!(!page.contains("is the basis of the paper's own"));
assert!(page.contains("are one parallel basis"));
assert!(page.contains("7.3 hours, a figure of ours, not the paper's"));
}
/// What the reaction-clock rows lean on is on the page: which basis the
/// source's figures are on, the job's distance, and how much of the
/// FeMoCo factory count is the tick.
#[test]
#[cfg_attr(debug_assertions, ignore = "the search over 4¹¹ Paulis takes minutes in a debug build")]
fn the_reaction_rows_say_what_they_rest_on() {
let sections = report().unwrap();
let rows: BTreeMap<&str, &Row> = sections.iter().flat_map(|s| &s.rows).map(|r| (r.id.as_str(), r)).collect();
let hours = &rows["r21.job.hours.reaction"].why;
assert!(!hours.contains("on that basis"), "{hours}");
assert!(hours.contains("one parallel basis") && hours.contains("7.3 hours: that is our derivation"), "{hours}");
// On the reaction clock the job's machine is not the source's.
assert_eq!(rows["r21.job.distance.assumed"].values.unwrap().1, 27.0);
assert_eq!(rows["r21.job.distance.reaction"].values.unwrap().1, 25.0);
assert!(rows["r21.job.qubits.reaction"].why.contains("at d = 25: near the source's count by chance"));
// The tick is the smaller part of the factory gap.
let days = &rows["femoco.job.days.reaction"].why;
assert!(days.contains("It keeps 157 T factories") && days.contains("taking 1,568 µs a CCZ state") && days.contains("the job keeps 41"), "{days}");
assert!(days.contains("the tick is ×3.8 and the kind of factory ×10"), "{days}");
assert!(rows["femoco.job.qubits.reaction"].why.contains("T factories from 15-to-1"));
}
#[test]
#[cfg_attr(debug_assertions, ignore = "the search over 4¹¹ Paulis takes minutes in a debug build")]
fn every_row_holds_to_its_published_figure() {

View file

@ -795,9 +795,11 @@ impl Timing {
}
/// One tick: the slower of the CCZ supply and the reaction time. A Toffoli
/// whose Clifford fixups are teleported away from the data takes one tick
/// and no lattice-surgery beat, unlike a [`Timing::step_us`].
/// and no lattice-surgery beat, unlike a [`Timing::step_us`]. The rule is
/// [`crate::quantum_resource::reaction_tick`], which the estimate job's
/// reaction-limited clock takes too.
pub fn tick_us(&self) -> f64 {
self.ccz_period_us().max(self.reaction_us)
crate::quantum_resource::reaction_tick(self.reaction_us, self.ccz_period_us())
}
/// A ripple-carry addition over `n` qubits, its Toffolis at one tick each:
/// `2n − 1` ticks, a sweep and its return.

View file

@ -67,7 +67,7 @@ __device__ __forceinline__ u64 splitmix(u64 s, u64 j) {
return x ^ (x >> 31);
}
extern "C" __global__ void chains_in_order(const unsigned* rows, const unsigned* cols, const int* vals, const i64* fields, i64 cost, unsigned n, unsigned chains, const i64* offsets, unsigned k0, unsigned k1, const u64* seeds, const unsigned* sweeps, const u64* table, int* cells) {
extern "C" __global__ void chains_in_order(const unsigned* rows, const unsigned* cols, const int* vals, const i64* fields, i64 cost, unsigned n, unsigned chains, const i64* offsets, unsigned k0, unsigned k1, const u64* seeds, const unsigned* sweeps, const u64* table, u64 entries, int* cells, unsigned* fault) {
unsigned c = blockIdx.x * blockDim.x + threadIdx.x;
if (c >= chains) return;
int* s = cells + (u64)c * n;
@ -83,6 +83,13 @@ extern "C" __global__ void chains_in_order(const unsigned* rows, const unsigned*
i64 h = field_of(rows, cols, vals, fields, s, i);
u64 u = splitmix(seed, (u64)k * n + i + 1) >> 11;
u64 e = (u64)(kind - 1u) + (u64)h + (u64)offsets[i];
// The host sizes the table to every field a cell can reach, so these kernels never
// pass it; a port that gets a field wrong is stopped here, before it reads past the
// table and the device's context is lost to every job after it.
if (e >= entries) {
atomicOr(fault, 1u);
return;
}
s[i] = u < table[2 * e] ? -1 : (u < table[2 * e + 1] ? 0 : 1);
}
}
@ -235,21 +242,27 @@ impl CudaLandscape {
let offsets = s.clone_htod(&reach.offsets).map_err(driver)?;
let (cost, n32, count32) = (w.cost(), n as u32, count as u32);
let window = device::window(w, &reach, sampling);
let mut fault = s.alloc_zeros::<u32>(1).map_err(driver)?;
let mut k0 = 0usize;
while k0 < temperatures.len() {
let k1 = (k0 + window).min(temperatures.len());
let (sweeps, table) = device::window_sweeps(w, &reach, &temperatures[k0..k1]);
let entries = (table.len() / 2) as u64;
let (sweeps, table) = (s.clone_htod(&sweeps).map_err(driver)?, s.clone_htod(&nonempty(&table)).map_err(driver)?);
let (a0, a1) = (k0 as u32, k1 as u32);
let mut a = s.launch_builder(&self.kernels.chains);
a.arg(&rows).arg(&cols).arg(&vals).arg(&fields).arg(&cost).arg(&n32).arg(&count32).arg(&offsets).arg(&a0).arg(&a1).arg(&seeds).arg(&sweeps).arg(&table).arg(&mut cells);
a.arg(&rows).arg(&cols).arg(&vals).arg(&fields).arg(&cost).arg(&n32).arg(&count32).arg(&offsets).arg(&a0).arg(&a1).arg(&seeds).arg(&sweeps).arg(&table).arg(&entries).arg(&mut cells).arg(&mut fault);
// SAFETY: 2·count·n cells, an offset a cell, a seed a chain, a word a sweep of the
// window, and the window's table of (a, b) pairs for every field a cell can reach.
// window, the window's table of `entries` (a, b) pairs, which the kernel reads only
// below `entries`, and one fault word.
unsafe { a.launch(launch(count as u64)) }.map_err(driver)?;
// One window at a time: only its table is held, and a failure leaves no more queued.
s.synchronize().map_err(driver)?;
k0 = k1;
}
if s.clone_dtoh(&fault).map_err(driver)?[0] != 0 {
return Err("the sampling kernel met a field its table does not reach, and stopped rather than read past it".into());
}
let got = s.clone_dtoh(&cells).map_err(driver)?;
Ok(device::outcomes(w, &got, count, temperatures))
}

View file

@ -132,14 +132,26 @@ pub fn discover_with(source: &str) -> Vec<Result<WebGpuLandscape, String>> {
if wgpu::Instance::enabled_backend_features().is_empty() {
return Vec::new();
}
let mut d = wgpu::InstanceDescriptor::new_without_display_handle();
d.backends = wgpu::Backends::PRIMARY;
d.flags = wgpu::InstanceFlags::empty();
let instance = wgpu::Instance::new(d);
let Ok(adapters) = resolve(instance.enumerate_adapters(wgpu::Backends::PRIMARY)) else { return Vec::new() };
let Ok(adapters) = resolve(instance().enumerate_adapters(wgpu::Backends::PRIMARY)) else { return Vec::new() };
adapters.into_iter().map(|a| open(a, source)).collect()
}
/// The process's one instance of the platform's GPU interfaces, which every
/// discovery enumerates. A discovery that made its own lost a GPU after ten:
/// an NVIDIA Vulkan driver (595.99, an RTX 4050) lists its card to a process's
/// first ten instances and not after, even with every one dropped, so a host
/// that discovers its members again (each test, each faulted kernel) found
/// that card gone.
fn instance() -> &'static wgpu::Instance {
static INSTANCE: std::sync::OnceLock<wgpu::Instance> = std::sync::OnceLock::new();
INSTANCE.get_or_init(|| {
let mut d = wgpu::InstanceDescriptor::new_without_display_handle();
d.backends = wgpu::Backends::PRIMARY;
d.flags = wgpu::InstanceFlags::empty();
wgpu::Instance::new(d)
})
}
fn open(adapter: wgpu::Adapter, source: &str) -> Result<WebGpuLandscape, String> {
let info = adapter.get_info();
let backend = backend_label(info.backend).ok_or_else(|| format!("{}: backend {:?} is not one this member runs on", info.name, info.backend))?;

View file

@ -94,11 +94,34 @@
//! uniform, independent circuit noise; free transport of T states; and
//! smooth T consumption.
//! - **Planar codes only.** Surface and Floquet-honeycomb patches with lattice
//! surgery. Block (qLDPC) codes, magic-state cultivation and reaction-time-
//! limited scheduling need models of their own.
//! surgery. Block (qLDPC) codes and magic-state cultivation need models of
//! their own.
//! - **Rounding.** The source prints its times truncated (`163.8 ms` as
//! `163 ms`). This module reports exact values; the tests compare at the
//! printed precision.
//!
//! # Clocks
//!
//! A Toffoli takes three logical steps of `d` cycles in the source's model:
//! the step clock, [`Clock::Steps`], every estimate's unless another is asked
//! for. On the reaction-limited clock, [`Clock::Reaction`] (arXiv:1905.09749
//! §2.11), a Toffoli whose Clifford fixups are teleported away from the data
//! takes one tick, the slower of the control system's reaction time and the
//! CCZ supply ([`reaction_tick`], the rule `quantum_arch::Timing` takes too),
//! and no lattice-surgery step. [`estimate_isa_clocked`] prices both:
//!
//! - the Toffolis' `3·R_CCZ` steps leave the step count, and `R_CCZ` ticks
//! join the runtime: `runtime = steps·τ(d) + R_CCZ·tick`, all of them one
//! after another, as the model lays its steps out;
//! - the tiles' logical error runs over the whole runtime, in steps of `τ(d)`;
//! - the factories are as many as the T states need over the runtime, and at
//! least as many as deliver a CCZ state (four T states) every tick, so the
//! supply keeps up with the reaction time and the tick is the reaction time;
//! - a slowdown `s` stretches both: `s·C` steps, ticks of `s` reaction times;
//! - a reaction includes the logical measurement it reacts to (its App. B.1),
//! so a reaction time shorter than the qubit's measurement is refused.
//!
//! On the step clock it is [`estimate_isa`], bit for bit.
use crate::repro::ln;
@ -344,7 +367,7 @@ pub fn layout(c: &LogicalCounts, total_budget: f64) -> IsaCounts {
fn ceil_pos(x: f64) -> u64 {
let t = x as u64;
if (t as f64) < x { t + 1 } else { t }
if (t as f64) < x { t.saturating_add(1) } else { t }
}
/// The two 15-to-1 distillation units.
@ -586,6 +609,41 @@ fn search(
}
}
/// How long a Toffoli takes.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Clock {
/// Three logical steps of `d` cycles: the planar model's own, and every
/// estimate's unless another clock is asked for.
Steps,
/// One tick a Toffoli, its Clifford fixups teleported away from the data:
/// the slower of the reaction time (ns) and the CCZ supply. The reaction
/// time includes a logical measurement, so it is never shorter than the
/// qubit's: [`estimate_isa_clocked`] refuses one that is.
Reaction { reaction_ns: f64 },
}
/// One tick of the reaction-limited clock: the slower of the control system's
/// reaction time and the CCZ supply's period, in one unit. A Toffoli whose
/// Clifford fixups are teleported away from the data takes one tick
/// (arXiv:1905.09749 §2.11). `quantum_arch::Timing::tick_us` and
/// [`Clock::Reaction`] both take it from here.
pub fn reaction_tick(reaction: f64, supply_period: f64) -> f64 {
reaction.max(supply_period)
}
/// An estimate on a clock. Under [`Clock::Reaction`] the estimate's `steps`
/// are the lattice-surgery steps alone, and its `runtime_ns` adds the
/// Toffolis' ticks.
#[derive(Clone, Debug, PartialEq)]
pub struct Clocked {
pub estimate: Estimate,
pub clock: Clock,
/// Toffolis run on ticks: none on the step clock.
pub ticks: u64,
/// One tick, ns: `0` on the step clock.
pub tick_ns: f64,
}
/// How fast to run: at the minimum step count, or slowed by a factor to
/// trade factories for time.
#[derive(Clone, Copy, Debug, PartialEq)]
@ -613,7 +671,9 @@ pub struct Estimate {
pub distance: u32,
pub tile_qubits: u64,
pub step_ns: f64,
/// Logical steps actually run.
/// Logical steps actually run: on [`Clock::Reaction`] the lattice-surgery
/// steps alone, the Toffolis being ticks, so the runtime is more than
/// `steps · step_ns`.
pub steps: u64,
pub runtime_ns: f64,
/// Per-tile, per-step logical error at the chosen distance.
@ -647,7 +707,9 @@ pub enum EstimateError {
DistanceExceeded,
/// No factory within the search limits meets the T-state target.
NoFactory,
/// An empty program, a zero budget or a zero slowdown.
/// An empty program, a zero budget, a zero slowdown, a reaction time
/// shorter than the qubit's measurement, or a machine whose qubits do not
/// fit in 64 bits.
Invalid,
/// The code and the qubit use different instruction sets.
Mismatch,
@ -665,9 +727,26 @@ pub fn estimate(c: &LogicalCounts, qubit: &PhysicalQubit, qec: &QecModel, cfg: &
/// Estimate from ISA-level counts directly.
pub fn estimate_isa(isa: &IsaCounts, qubit: &PhysicalQubit, qec: &QecModel, cfg: &EstimateConfig) -> Result<Estimate, EstimateError> {
estimate_isa_clocked(isa, 0, qubit, qec, cfg, Clock::Steps).map(|c| c.estimate)
}
/// [`estimate_isa`] on `clock`, the program's `toffolis` among its counts
/// (the layout gives each three of `isa.min_steps` and four of its T states).
/// On [`Clock::Steps`] `toffolis` is not read and the estimate is
/// [`estimate_isa`]'s, bit for bit.
pub fn estimate_isa_clocked(isa: &IsaCounts, toffolis: u64, qubit: &PhysicalQubit, qec: &QecModel, cfg: &EstimateConfig, clock: Clock) -> Result<Clocked, EstimateError> {
if isa.tiles == 0 || isa.min_steps == 0 || cfg.slowdown == 0 || !(cfg.error_budget > 0.0 && cfg.error_budget < 1.0) {
return Err(EstimateError::Invalid);
}
let (ticks, reaction_ns) = match clock {
Clock::Steps => (0, 0.0),
Clock::Reaction { reaction_ns } => {
if !(reaction_ns > 0.0 && reaction_ns.is_finite()) || reaction_ns < qubit.meas_ns || toffolis.checked_mul(3).is_none_or(|t| t > isa.min_steps) {
return Err(EstimateError::Invalid);
}
(toffolis, reaction_ns)
}
};
if qec.instruction_set != qubit.instruction_set {
return Err(EstimateError::Mismatch);
}
@ -684,31 +763,47 @@ pub fn estimate_isa(isa: &IsaCounts, qubit: &PhysicalQubit, qec: &QecModel, cfg:
} else {
Some(find_factory(qubit, qec, t_target, cfg.limits).ok_or(EstimateError::NoFactory)?)
};
let mut steps = isa.min_steps.saturating_mul(cfg.slowdown);
// A tick of a slowed program: the reaction time, stretched as its steps are.
let stretched = reaction_ns * cfg.slowdown as f64;
let mut steps = (isa.min_steps - 3 * ticks).saturating_mul(cfg.slowdown);
loop {
let distance = distance_for(qec, qubit, isa.tiles, steps, budget.logical, cfg.limits.max_distance)?;
let distance = if ticks == 0 {
distance_for(qec, qubit, isa.tiles, steps, budget.logical, cfg.limits.max_distance)?
} else {
distance_for_ticks(qec, qubit, isa.tiles, steps, ticks as f64 * stretched, budget.logical, cfg.limits.max_distance)?
};
let step_ns = qec.step_ns(qubit, distance);
let runtime_ns = step_ns * steps as f64;
let runtime_ns = if ticks == 0 { step_ns * steps as f64 } else { step_ns * steps as f64 + stretched * ticks as f64 };
// The program must outlast one factory run.
if let Some(f) = &factory
&& runtime_ns < f.duration_ns
{
let need = ceil_pos(f.duration_ns / step_ns);
let need = if ticks == 0 { ceil_pos(f.duration_ns / step_ns) } else { steps + ceil_pos((f.duration_ns - runtime_ns) / step_ns) };
if need > steps {
steps = need;
continue;
}
}
let (factories, factory_qubits) = match &factory {
let (factories, factory_qubits, tick_ns) = match &factory {
Some(f) => {
let n = ceil_pos(isa.t_states as f64 * f.duration_ns / (f.outputs as f64 * runtime_ns));
(n, n * f.qubits)
let mut n = ceil_pos(isa.t_states as f64 * f.duration_ns / (f.outputs as f64 * runtime_ns));
let mut tick_ns = 0.0;
if ticks > 0 {
// A CCZ state is four T states: enough factories that one
// comes every tick, so the supply keeps up with the
// reaction time.
let per_factory = 4.0 * f.duration_ns / f.outputs as f64;
n = n.max(ceil_pos(per_factory / stretched));
tick_ns = reaction_tick(stretched, per_factory / n as f64);
}
(n, n.checked_mul(f.qubits).ok_or(EstimateError::Invalid)?, tick_ns)
}
None => (0, 0),
None => (0, 0, if ticks > 0 { stretched } else { 0.0 }),
};
let tile_qubits = qec.tile_qubits(distance);
let algorithm_qubits = isa.tiles * tile_qubits;
return Ok(Estimate {
let algorithm_qubits = isa.tiles.checked_mul(tile_qubits).ok_or(EstimateError::Invalid)?;
let physical_qubits = algorithm_qubits.checked_add(factory_qubits).ok_or(EstimateError::Invalid)?;
let estimate = Estimate {
qubit: qubit.name.clone(),
code: qec.name.clone(),
isa: *isa,
@ -724,11 +819,26 @@ pub fn estimate_isa(isa: &IsaCounts, qubit: &PhysicalQubit, qec: &QecModel, cfg:
factories,
algorithm_qubits,
factory_qubits,
physical_qubits: algorithm_qubits + factory_qubits,
});
physical_qubits,
};
return Ok(Clocked { estimate, clock, ticks, tick_ns });
}
}
/// Smallest odd distance with `tiles · (steps + tick_time/τ(d)) · P(d) ≤
/// budget`: the tiles wait out the ticks too, a step's error every `τ(d)`.
fn distance_for_ticks(qec: &QecModel, qubit: &PhysicalQubit, tiles: u64, steps: u64, tick_time_ns: f64, budget: f64, max: u32) -> Result<u32, EstimateError> {
let mut d = 1;
while d <= max {
let volume = tiles as f64 * (steps as f64 + tick_time_ns / qec.step_ns(qubit, d));
if volume * qec.logical_error(qubit.clifford_error, d) <= budget {
return Ok(d);
}
d += 2;
}
Err(EstimateError::DistanceExceeded)
}
/// Smallest odd distance with `tiles · steps · P(d) ≤ budget`.
fn distance_for(qec: &QecModel, qubit: &PhysicalQubit, tiles: u64, steps: u64, budget: f64, max: u32) -> Result<u32, EstimateError> {
let volume = tiles as f64 * steps as f64;
@ -749,6 +859,24 @@ pub fn estimate_best(c: &LogicalCounts, qubit: &PhysicalQubit, cfg: &EstimateCon
estimate_best_isa(&isa, qubit, cfg)
}
/// [`estimate_best_isa`] on `clock`: the cheapest by space-time volume over
/// every code the qubit supports.
pub fn estimate_best_isa_clocked(isa: &IsaCounts, toffolis: u64, qubit: &PhysicalQubit, cfg: &EstimateConfig, clock: Clock) -> Result<Clocked, EstimateError> {
let mut best: Option<Clocked> = None;
let mut last_err = EstimateError::Mismatch;
for qec in QecModel::for_qubit(qubit) {
match estimate_isa_clocked(isa, toffolis, qubit, &qec, cfg, clock) {
Ok(e) => {
if best.as_ref().is_none_or(|b| e.estimate.volume() < b.estimate.volume()) {
best = Some(e);
}
}
Err(e) => last_err = e,
}
}
best.ok_or(last_err)
}
/// [`estimate_best`] from ISA-level counts.
pub fn estimate_best_isa(isa: &IsaCounts, qubit: &PhysicalQubit, cfg: &EstimateConfig) -> Result<Estimate, EstimateError> {
let mut best: Option<Estimate> = None;
@ -920,6 +1048,84 @@ mod tests {
}
}
/// On the step clock the clocked estimate is the plain one; on the
/// reaction clock the Toffolis run on ticks and the supply keeps up.
#[test]
fn the_step_clock_is_the_estimate_bit_for_bit_and_the_reaction_clock_keeps_pace() {
let c = LogicalCounts { qubits: 12_581, measurements: 1_080_000_000, rotations: 12, rotation_depth: 12, t_gates: 12, ccz: 3_730_000_000 };
let isa = layout(&c, 1.0 / 3.0);
let mut reacted = 0;
for q in [PhysicalQubit::ns_e4(), PhysicalQubit::ns_e3(), PhysicalQubit::us_e3()] {
for s in [1, 4] {
let cfg = EstimateConfig { slowdown: s, ..EstimateConfig::new(1.0 / 3.0) };
for qec in QecModel::for_qubit(&q) {
let plain = estimate_isa(&isa, &q, &qec, &cfg);
let clocked = estimate_isa_clocked(&isa, c.ccz, &q, &qec, &cfg, Clock::Steps);
assert_eq!(plain, clocked.clone().map(|x| x.estimate));
if let Ok(x) = &clocked {
assert_eq!((x.ticks, x.tick_ns), (0, 0.0));
}
let Ok(r) = estimate_isa_clocked(&isa, c.ccz, &q, &qec, &cfg, Clock::Reaction { reaction_ns: 10_000.0 }) else { continue };
let e = &r.estimate;
assert_eq!((r.ticks, r.tick_ns), (c.ccz, 10_000.0 * s as f64));
assert_eq!(e.runtime_ns, e.step_ns * e.steps as f64 + r.tick_ns * r.ticks as f64);
assert_eq!(e.steps, (isa.min_steps - 3 * c.ccz) * s);
let f = e.factory.as_ref().unwrap();
assert!(4.0 * f.duration_ns / (f.outputs as f64 * e.factories as f64) <= r.tick_ns, "the supply keeps up with the reaction time");
assert!(e.runtime_ns < plain.as_ref().unwrap().runtime_ns, "a tick is shorter than three steps on these qubits");
reacted += 1;
}
}
}
// The 100 µs qubit's measurement is longer than a 10 µs reaction, so
// it has no such tick (the test below): the two 50 ns qubits, once
// each slowdown.
assert_eq!(reacted, 4);
// Too many Toffolis for the counts, or no reaction time: refused.
let cfg = EstimateConfig::new(1.0 / 3.0);
let q = PhysicalQubit::ns_e4();
let qec = QecModel::surface_gate();
assert_eq!(estimate_isa_clocked(&isa, isa.min_steps, &q, &qec, &cfg, Clock::Reaction { reaction_ns: 1.0 }), Err(EstimateError::Invalid));
assert_eq!(estimate_isa_clocked(&isa, c.ccz, &q, &qec, &cfg, Clock::Reaction { reaction_ns: 0.0 }), Err(EstimateError::Invalid));
// The rule quantum_arch's clock takes: the slower of the two.
assert_eq!(reaction_tick(10.0, 33.75), 33.75);
assert_eq!(reaction_tick(10.0, 9.6), 10.0);
}
/// A reaction is the time to trigger a logical measurement, collect and
/// correct its result, and choose the next basis (arXiv:1905.09749 App.
/// B.1), so one measurement on the qubit bounds it from below: the 100 µs
/// qubit has no 10 µs tick.
#[test]
fn a_reaction_time_is_never_shorter_than_the_qubit_s_measurement() {
let c = LogicalCounts { qubits: 100, ccz: 1_000_000, ..Default::default() };
let (isa, cfg, qec) = (layout(&c, 1e-3), EstimateConfig::new(1e-3), QecModel::surface_gate());
let q = PhysicalQubit::us_e3();
assert_eq!(q.meas_ns, 100_000.0);
let run = |reaction_ns| estimate_isa_clocked(&isa, c.ccz, &q, &qec, &cfg, Clock::Reaction { reaction_ns });
assert_eq!(run(10_000.0).map(|x| x.tick_ns), Err(EstimateError::Invalid));
assert_eq!(run(q.meas_ns).map(|x| x.tick_ns), Ok(q.meas_ns));
}
/// The factories keep up however short the tick, so a short enough one
/// asks for more qubits than a count holds: refused, never wrapped.
#[test]
fn a_machine_past_sixty_four_bits_is_refused_and_not_wrapped() {
assert_eq!((ceil_pos(2.5), ceil_pos(3.0), ceil_pos(1e300)), (3, 3, u64::MAX));
let c = LogicalCounts { qubits: 6189, ccz: 2_698_000_000, ..Default::default() };
let (isa, cfg, qec) = (layout(&c, 0.31), EstimateConfig::new(0.31), QecModel::surface_gate());
// A qubit whose measurement allows any of these reaction times.
let q = PhysicalQubit { meas_ns: 1e-300, ..PhysicalQubit::ns_e4() };
let run = |reaction_ns| estimate_isa_clocked(&isa, c.ccz, &q, &qec, &cfg, Clock::Reaction { reaction_ns });
for reaction_ns in [1e-12, 1e-300] {
assert_eq!(run(reaction_ns).map(|x| x.estimate.physical_qubits), Err(EstimateError::Invalid), "{reaction_ns}");
}
let held = run(1.0).unwrap();
let e = &held.estimate;
assert_eq!(e.physical_qubits, e.algorithm_qubits + e.factories * e.factory.as_ref().unwrap().qubits);
assert_eq!(held.tick_ns, 1.0, "the supply keeps up and the tick is the reaction time");
}
/// The summary rows for factoring and chemistry: distance and factory
/// count exactly; physical qubits and runtime to the printed figures.
#[test]

View file

@ -22,11 +22,20 @@
//! The receipt is a JWP profile like the crate's others (Merkle root over leaf
//! hashes, Ed25519). It can carry an energy-class label
//! (`quantum_energy::Labelled`).
//!
//! **Clocks.** A job runs on the step clock unless a [`ClockedJob`] names
//! another. On the step clock a clocked job is the job itself: the same hash,
//! the same estimate, the same receipt as before clocks were named. On the
//! reaction-limited clock the estimate's `steps` are the lattice-surgery steps
//! alone and its runtime adds the Toffolis' ticks, so the job takes a hash of
//! its own form, `wai:resource-job-clocked\x01` over the job's hash and the
//! clock, and a receipt sealed on one clock never verifies as sealed on the
//! other.
use crate::merkle::merkle_root;
use crate::quantum_resource::{
estimate_best, estimate_best_isa, estimate, estimate_isa, Budget, Estimate, EstimateConfig, EstimateError,
Factory, InstructionSet, IsaCounts, LogicalCounts, PhysicalQubit, QecModel, Round, SearchLimits, UnitKind,
estimate_best, estimate_best_isa, estimate_best_isa_clocked, estimate, estimate_isa, estimate_isa_clocked, layout, Budget, Clock, Clocked, Estimate,
EstimateConfig, EstimateError, Factory, InstructionSet, IsaCounts, LogicalCounts, PhysicalQubit, QecModel, Round, SearchLimits, UnitKind,
};
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use serde_json::{json, Value};
@ -35,6 +44,7 @@ const DOMAIN_RECEIPT: &[u8] = b"wai:resource-receipt\x01";
const DOMAIN_RECEIPT_ID: &[u8] = b"wai:resource-receipt-id\x01";
const DOMAIN_JOB: &[u8] = b"wai:resource-job\x01";
const DOMAIN_RESULT: &[u8] = b"wai:resource-estimate\x01";
const DOMAIN_JOB_CLOCKED: &[u8] = b"wai:resource-job-clocked\x01";
/// The program being estimated, at the level it is known.
#[derive(Clone, Debug, PartialEq)]
@ -183,6 +193,63 @@ impl ResourceJob {
}
}
/// A job on a clock: [`ResourceJob`] on [`Clock::Steps`] is the job itself.
#[derive(Clone, Debug, PartialEq)]
pub struct ClockedJob {
pub job: ResourceJob,
pub clock: Clock,
}
impl ClockedJob {
/// The program's Toffolis, where its counts give them.
pub fn toffolis(&self) -> Option<u64> {
match &self.job.program {
Program::Logical(c) => Some(c.ccz),
Program::Counted(c) => Some(c.logical.ccz),
Program::Isa(_) => None,
}
}
/// The estimate on the clock. Layout-level counts name no Toffolis, so
/// only the step clock takes them.
pub fn run(&self) -> Result<Clocked, EstimateError> {
let j = &self.job;
if self.clock == Clock::Steps {
return j.run().map(|estimate| Clocked { estimate, clock: Clock::Steps, ticks: 0, tick_ns: 0.0 });
}
let toffolis = self.toffolis().ok_or(EstimateError::Invalid)?;
let isa = match &j.program {
Program::Logical(c) => {
if c.qubits == 0 || j.config.error_budget.is_nan() || j.config.error_budget <= 0.0 {
return Err(EstimateError::Invalid);
}
layout(c, j.config.error_budget)
}
Program::Isa(i) => *i,
Program::Counted(c) => c.isa,
};
match &j.code {
Some(q) => estimate_isa_clocked(&isa, toffolis, &j.qubit, q, &j.config, self.clock),
None => estimate_best_isa_clocked(&isa, toffolis, &j.qubit, &j.config, self.clock),
}
}
/// The job's hash on the step clock; on another, BLAKE3 over
/// `wai:resource-job-clocked\x01`, the job's hash and the clock.
pub fn job_hash(&self) -> [u8; 32] {
match self.clock {
Clock::Steps => self.job.job_hash(),
Clock::Reaction { reaction_ns } => {
let mut o = DOMAIN_JOB_CLOCKED.to_vec();
o.extend_from_slice(&self.job.job_hash());
o.push(2);
f(reaction_ns, &mut o);
*blake3::hash(&o).as_bytes()
}
}
}
}
fn u(x: u64, o: &mut Vec<u8>) {
o.extend_from_slice(&x.to_be_bytes());
}
@ -371,10 +438,66 @@ impl ResourceReceipt {
Ok(r)
}
/// [`ResourceReceipt::seal`] for a job on a clock: on the step clock the
/// same receipt, bit for bit.
pub fn seal_clocked(
signer: &SigningKey,
signer_id: impl Into<String>,
job: &ClockedJob,
parent_receipt_hash: Option<[u8; 32]>,
meter: impl FnOnce(&mut dyn FnMut()) -> u64,
) -> Result<ResourceReceipt, EstimateError> {
let mut out = None;
let joules_micro = meter(&mut || out = Some(job.run()));
let estimate = out.ok_or(EstimateError::NotRun)??.estimate;
let mut r = ResourceReceipt {
job_hash: job.job_hash(),
result_hash: result_hash(&estimate),
estimate,
merkle_root: [0; 32],
joules_micro,
parent_receipt_hash,
signer_pubkey: signer.verifying_key().to_bytes(),
signer_id: signer_id.into(),
sig: [0; 64],
};
r.merkle_root = merkle_root(&r.leaves());
r.sig = signer.sign(&r.payload_with(None)).to_bytes();
Ok(r)
}
/// The re-run half of [`Self::verify_computation_clocked`].
pub(crate) fn computation_matches_clocked(&self, job: &ClockedJob) -> bool {
job.job_hash() == self.job_hash && job.run().is_ok_and(|c| result_hash(&c.estimate) == self.result_hash)
}
/// [`Self::verify_computation`] for a job on a clock.
pub fn verify_computation_clocked(&self, job: &ClockedJob) -> bool {
self.computation_matches_clocked(job) && self.verify()
}
fn body_verifies(&self) -> bool {
result_hash(&self.estimate) == self.result_hash && merkle_root(&self.leaves()) == self.merkle_root
}
/// The runtime its steps do not account for: on the reaction-limited clock
/// the Toffolis' ticks, on the step clock exactly none (the runtime is
/// `step_ns · steps`, in that arithmetic). It comes from figures the
/// receipt signs, so it needs no field of its own, and it is how a receipt
/// says which clock it is on. How long one tick was is the job's: its hash
/// binds the reaction time.
pub fn tick_time_ns(&self) -> f64 {
let e = &self.estimate;
e.runtime_ns - e.step_ns * e.steps as f64
}
/// What [`Self::to_json`] says of the clock: nothing on the step clock, so
/// that receipt is what it always was.
fn clock_json(&self) -> Option<Value> {
let ticks = self.tick_time_ns();
(ticks > 0.0).then(|| json!({ "kind": "reaction", "tick_time_ns_bits": bits(ticks), "tick_time_s": ticks / 1e9 }))
}
/// The receipt is internally consistent and its signature is valid. Does
/// not re-run the job: see [`Self::verify_computation`].
pub fn verify(&self) -> bool {
@ -410,7 +533,8 @@ impl ResourceReceipt {
/// Canonical JSON, `kind` first and then keys sorted. Every floating-point
/// figure appears as its exact bits (`*_bits`), which are what a verifier
/// reads back; the headline runtime also appears as a readable number.
/// reads back; the headline runtime also appears as a readable number. On
/// the reaction-limited clock `clock` names it ([`Self::tick_time_ns`]).
pub fn to_json(&self) -> String {
let e = &self.estimate;
let factory = e.factory.as_ref().map(|fa| {
@ -469,7 +593,19 @@ impl ResourceReceipt {
});
// The crate's serde_json keeps insertion order, and keys are written in
// sorted order after `kind`, so this is canonical.
v.to_string()
match (self.clock_json(), v) {
(Some(clock), Value::Object(fields)) => {
let mut o = serde_json::Map::new();
for (k, x) in fields {
if k == "estimate" {
o.insert("clock".into(), clock.clone());
}
o.insert(k, x);
}
Value::Object(o).to_string()
}
(_, v) => v.to_string(),
}
}
pub fn from_json(s: &str) -> Option<ResourceReceipt> {
@ -543,7 +679,7 @@ impl ResourceReceipt {
Some(Value::String(h)) => Some(unhex::<32>(h)?),
_ => None,
};
Some(ResourceReceipt {
let receipt = ResourceReceipt {
job_hash: unhex(s("job_hash")?)?,
estimate,
result_hash: unhex(s("result_hash")?)?,
@ -553,7 +689,22 @@ impl ResourceReceipt {
signer_pubkey: unhex(s("signer_pubkey")?)?,
signer_id: s("signer_id")?.to_string(),
sig: unhex(s("sig")?)?,
})
};
// `clock` is read from the signed figures, so one that disagrees with
// them, or is missing where they name one, is not this receipt. Its
// bits are compared; `tick_time_s` is the readable form, as
// `runtime_s` is, and a parser need not round it back exactly.
let agrees = match (v.get("clock"), receipt.clock_json()) {
(None, None) => true,
(Some(have), Some(want)) => {
have.get("kind") == want.get("kind")
&& have.get("tick_time_ns_bits") == want.get("tick_time_ns_bits")
&& have.get("tick_time_s").is_some_and(Value::is_number)
&& have.as_object().is_some_and(|o| o.len() == 3)
}
_ => false,
};
agrees.then_some(receipt)
}
}
@ -606,6 +757,75 @@ mod tests {
assert_eq!(r.joules_micro, 1_700);
}
#[test]
fn on_the_step_clock_a_clocked_job_is_the_job_and_on_the_reaction_clock_another() {
let j = ResourceJob::default();
let steps = ClockedJob { job: j.clone(), clock: Clock::Steps };
assert_eq!(steps.job_hash(), j.job_hash());
let a = ResourceReceipt::seal(&key(4), "sink", &j, None, unmetered).unwrap();
let b = ResourceReceipt::seal_clocked(&key(4), "sink", &steps, None, unmetered).unwrap();
assert_eq!(a, b, "the step clock seals the same receipt, bit for bit");
let fast = ClockedJob { job: j.clone(), clock: Clock::Reaction { reaction_ns: 10_000.0 } };
assert_ne!(fast.job_hash(), j.job_hash());
let r = ResourceReceipt::seal_clocked(&key(4), "sink", &fast, None, unmetered).unwrap();
assert!(r.verify() && r.verify_computation_clocked(&fast));
assert!(!r.verify_computation(&j), "a reaction-clock receipt is not the step clock's");
assert!(!r.verify_computation_clocked(&steps));
assert!(!a.verify_computation_clocked(&fast));
let slower = ClockedJob { clock: Clock::Reaction { reaction_ns: 20_000.0 }, ..fast.clone() };
assert!(!r.verify_computation_clocked(&slower), "the reaction time is bound");
// The Toffolis leave the steps and run on ticks of the reaction time.
let c = fast.run().unwrap();
let e = &c.estimate;
assert_eq!((c.ticks, c.tick_ns), (3_730_000_000, 10_000.0));
assert_eq!(e.runtime_ns, e.step_ns * e.steps as f64 + c.tick_ns * c.ticks as f64);
assert_eq!(e.steps, 1_080_000_000 + 12 + 12 + e.isa.t_per_rotation * 12);
// Layout-level counts name no Toffolis: only the step clock takes them.
let isa = ResourceJob { program: Program::Isa(e.isa), ..j.clone() };
assert_eq!(ClockedJob { job: isa.clone(), clock: fast.clock }.run(), Err(EstimateError::Invalid));
assert!(ClockedJob { job: isa, clock: Clock::Steps }.run().is_ok());
}
/// A reader with the receipt alone can tell the clocks apart, from figures
/// the signature covers; a step-clock receipt reads as it always did.
#[test]
fn a_reaction_clock_receipt_names_its_clock_and_a_step_clock_receipt_is_unchanged() {
let j = ResourceJob::default();
let step = ResourceReceipt::seal(&key(4), "sink", &j, None, unmetered).unwrap();
assert_eq!(step.tick_time_ns(), 0.0);
let plain: Value = serde_json::from_str(&step.to_json()).unwrap();
assert!(plain.get("clock").is_none());
let fast = ClockedJob { job: j, clock: Clock::Reaction { reaction_ns: 10_000.0 } };
let r = ResourceReceipt::seal_clocked(&key(4), "sink", &fast, None, unmetered).unwrap();
let c = fast.run().unwrap();
assert_eq!(r.tick_time_ns(), c.estimate.runtime_ns - c.estimate.step_ns * c.estimate.steps as f64);
assert!(r.tick_time_ns() > 0.0);
let text = r.to_json();
let v: Value = serde_json::from_str(&text).unwrap();
assert_eq!(v["clock"]["kind"], "reaction");
let keys: Vec<&String> = v.as_object().unwrap().keys().collect();
assert_eq!((keys[0].as_str(), keys[1].as_str(), keys[2].as_str()), ("kind", "clock", "estimate"), "canonical: kind, then sorted");
let back = ResourceReceipt::from_json(&text).unwrap();
assert_eq!(back, r);
assert_eq!(back.to_json(), text);
assert!(back.verify());
// The member repeats signed figures: changed or removed, it is refused.
let mut wrong = v.clone();
wrong["clock"]["tick_time_ns_bits"] = json!(bits(1.0));
assert!(ResourceReceipt::from_json(&wrong.to_string()).is_none());
let mut named = v.clone();
named["clock"]["kind"] = json!("steps");
assert!(ResourceReceipt::from_json(&named.to_string()).is_none());
let mut on_a_step_receipt = plain;
on_a_step_receipt["clock"] = v["clock"].clone();
assert!(ResourceReceipt::from_json(&on_a_step_receipt.to_string()).is_none());
let mut gone = v;
gone.as_object_mut().unwrap().remove("clock");
assert!(ResourceReceipt::from_json(&gone.to_string()).is_none());
}
#[test]
fn the_json_round_trips_to_the_same_bytes() {
// With a factory, and a Clifford-only program without one.

View file

@ -142,6 +142,9 @@ r21.job.qubits.fitted arXiv:1905.09749 Table 3; its counts in the estimate job 2
r21.job.qubits.source_rule arXiv:1905.09749 Table 3; its counts in the estimate job 20 (megaqubits) 20000000.0 guard 2.757e7
r21.job.hours.assumed arXiv:1905.09749 Table 3; its counts in the estimate job 5.1 5.1 guard 6.071e1
r21.job.hours.source_rule arXiv:1905.09749 Table 3; its counts in the estimate job 5.1 5.1 guard 6.971e1
r21.job.distance.reaction arXiv:1905.09749 §2.13 (d = 27); its counts in the estimate job 27 27 guard 2.500e1
r21.job.hours.reaction arXiv:1905.09749 Table 3; its counts in the estimate job 5.1 5.1 guard 7.495e0
r21.job.qubits.reaction arXiv:1905.09749 Table 3; its counts in the estimate job 20 (megaqubits) 20000000.0 guard 2.005e7
r21.job.factories arXiv:1905.09749 §2.10; its counts in the estimate job 14 CCZ per piece, 28 in all text
r21.table3.1024.megaqubits arXiv:1905.09749 Table 3 9.7 9.7 up 0.1
r21.table3.1024.hours arXiv:1905.09749 Table 3 1.3 1.3 up 0.1
@ -172,6 +175,8 @@ femoco.job.days.table arXiv:2011.03494 §IV.C.4; its counts in the estimate job
femoco.job.days.layout arXiv:2011.03494 §IV.C.4; its counts in the estimate job 3 3.0 guard 6.751e0
femoco.job.qubits.e4 arXiv:2011.03494 §IV.C.5; its counts in the estimate job "one million" 1000000.0 guard 9.682e5
femoco.job.days.e4 arXiv:2011.03494 §IV.C.5; its counts in the estimate job 1.5 1.5 guard 3.492e0
femoco.job.days.reaction arXiv:2011.03494 §IV.C.4; its counts in the estimate job 3 3.0 guard 7.759e-1
femoco.job.qubits.reaction arXiv:2011.03494 §IV.C.4; its counts in the estimate job ≈ 4·10⁶ 4000000.0 guard 1.080e7
r25.qubits arXiv:2505.15917 §3.2 897,864 897864 guard 9.585e5
r25.hours arXiv:2505.15917 §3.2 12.07 12.07 guard 1.058e1
r25.days arXiv:2505.15917 §3.2 4.96 4.96 guard 4.305e0

Can't render this file because it contains an unexpected character in line 163 and column 61.

View file

@ -330,6 +330,56 @@ fn every_fault_in_a_device_s_kernels_loses_its_grade() {
}
}
/// A host finds the same members however often it looks. Each discovery once
/// made its own instance of the GPU interfaces, and an NVIDIA Vulkan driver
/// lists its card to a process's first ten instances and not after: by a test
/// late in this file, that card was gone. Twelve discoveries, one process.
#[test]
fn a_member_is_found_however_often_the_host_looks() {
let ids = |found: Found| -> Vec<String> { found.into_iter().filter_map(Result::ok).map(|m| m.id()).collect() };
let first: Vec<Vec<String>> = backends(&[]).into_iter().map(|(_, f)| ids(f)).collect();
for look in 2..=12 {
let again: Vec<Vec<String>> = backends(&[]).into_iter().map(|(_, f)| ids(f)).collect();
assert_eq!(again, first, "discovery {look} found other members than the first");
}
}
/// A port whose sampling kernel looks a field up past its table (by 2⁴⁰
/// entries on CUDA, so the address is never mapped) loses its grade and not
/// the device: the clean kernels then open on as many devices as before and
/// are certified bit-exact on each. A CUDA kernel that read there would lose
/// its context to every job after it in the process; WebGPU's kernels are
/// bounds-checked.
#[test]
fn a_kernel_that_looks_past_its_table_loses_its_grade_and_not_the_device() {
let present: Vec<String> = members().iter().map(|m| m.id()).collect();
let have = built();
let past: Vec<(&str, &str, &str)> = [
("cuda", "u64 e = (u64)(kind - 1u) + (u64)h + (u64)offsets[i];", "u64 e = (u64)(kind - 1u) + (u64)h + (u64)offsets[i] + (1ull << 40);"),
("webgpu", "return kind - 1u + bitcast<u32>(h) + graph[shape.offsets + i];", "return kind - 1u + bitcast<u32>(h) + graph[shape.offsets + i] + 0x1000000u;"),
]
.into_iter()
.filter(|(b, _, _)| have.contains(b))
.collect();
for (backend, found) in backends(&past) {
if !past.iter().any(|(b, _, _)| *b == backend) {
continue;
}
for m in found.into_iter().filter_map(Result::ok) {
let c = member::certify(m.as_ref()).unwrap();
report(&format!("fault a lookup past the table in {}: settle {}, sample {}", m.id(), c.parity(KERNEL_SETTLE).label(), c.parity(KERNEL_SAMPLE).label()));
assert_ne!(c.parity(KERNEL_SAMPLE), Parity::BitExact, "{}: a lookup past the table kept the sampler bit-exact", m.id());
}
let again: Vec<Box<dyn Member>> = backends(&[]).into_iter().filter(|(b, _)| *b == backend).flat_map(|(_, f)| f).filter_map(Result::ok).collect();
let before = present.iter().filter(|id| backend_of(id) == backend).count();
assert_eq!(again.len(), before, "{backend}: after the fault the clean kernels opened on {} devices, before it on {before}", again.len());
for m in again {
let c = member::certify(m.as_ref()).unwrap();
assert_eq!((c.parity(KERNEL_SETTLE), c.parity(KERNEL_SAMPLE)), (Parity::BitExact, Parity::BitExact), "{}: the device lost its grade with the faulted kernels", m.id());
}
}
}
/// A WebGPU member waits once a window, so a run longer than one wait but made
/// of windows shorter than it finishes; and a job a member cannot finish in its
/// wait goes to the next member, the answer naming the one that failed.

View file

@ -92,7 +92,7 @@ impl QuantumEngine {
/// What a fault-tolerant machine would need to run a program
/// (`{"qasm" | "counts" | "routine", "budget", "qubit", "calibration",
/// "max_slowdown", "export"}`): its logical counts, each estimate on the
/// "max_slowdown", "clock", "export"}`): its logical counts, each estimate on the
/// frontier sealed with the page's key in the resource-estimate receipt
/// family and the whole answer in a job's form, so `engine_verify`
/// checks its seal and `engine_estimate_verify` re-runs it; or