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
27 changed files with 1503 additions and 92 deletions
18
.github/workflows/wai-quantum-estimate.yml
vendored
18
.github/workflows/wai-quantum-estimate.yml
vendored
|
|
@ -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 \
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
30
wai/changes/2026-11-06-estimate-reaction-clock.md
Normal file
30
wai/changes/2026-11-06-estimate-reaction-clock.md
Normal 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.
|
||||
|
|
@ -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 }); }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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", {}),
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))?;
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue