diff --git a/.github/workflows/wai-quantum-cli.yml b/.github/workflows/wai-quantum-cli.yml index f059754cd..ba7c0d229 100644 --- a/.github/workflows/wai-quantum-cli.yml +++ b/.github/workflows/wai-quantum-cli.yml @@ -4,7 +4,11 @@ name: wai-quantum command line # warnings as errors, with and without `submit`; its smoke checks (smoke.sh: lint, # plan, run, verify and a forged result refused, key, draw, MCP over stdio, the # research tools); and the WASI component, whose output under wasmtime equals the -# native build's byte for byte. On macOS, also with the GPU state vector. +# native build's byte for byte. On macOS, also with the GPU state vector. Its tests +# include the model harness (src/eval.rs): every task's reference solved and an +# empty program not, a scripted run, a model server on this machine asked and one +# elsewhere refused, and the corpus (wai/quantum-conformance/eval/) held to the +# task set. concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -17,6 +21,7 @@ on: - 'wai/wai-quantum/**' - 'wai/wai-quantum-cli/**' - 'wai/wai-quantum-qpu/**' + - 'wai/quantum-conformance/eval/**' - 'rust-toolchain.toml' - '.github/workflows/wai-quantum-cli.yml' pull_request: @@ -24,6 +29,7 @@ on: - 'wai/wai-quantum/**' - 'wai/wai-quantum-cli/**' - 'wai/wai-quantum-qpu/**' + - 'wai/quantum-conformance/eval/**' - 'rust-toolchain.toml' - '.github/workflows/wai-quantum-cli.yml' @@ -80,6 +86,17 @@ jobs: an_agent_lists_estimates_is_refused_and_runs_only_with_confirm_cost; do grep -q "^test $t \.\.\. ok$" "$RUNNER_TEMP/test.log" || { echo "::error::$t did not run and pass"; exit 1; } done + # The model harness and the views in an agent's host, each test by name. + for t in eval::tests::every_task_s_reference_is_solved_and_an_empty_program_is_not \ + eval::tests::a_scripted_run_reports_what_the_engine_found \ + eval::tests::a_model_on_this_machine_is_asked_and_one_elsewhere_is_refused \ + eval::tests::the_corpus_is_the_task_set_written_three_ways \ + eval::tests::an_answer_is_read_for_its_program_and_scored \ + mcp::tests::a_host_that_draws_views_gets_them_and_one_that_does_not_gets_words; do + grep -q "^test $t \.\.\. ok$" "$RUNNER_TEMP/test.log" || { echo "::error::$t did not run and pass"; exit 1; } + done + # The token counter needs a model's vocabulary to run; here it is only read. + python3 -I -c 'import ast, sys; ast.parse(open(sys.argv[1]).read())' ../quantum-conformance/eval/token_cost.py cargo clippy --locked --release --all-targets -- -D warnings cargo clippy --locked --release --all-targets --no-default-features -- -D warnings - name: The smoke checks diff --git a/.github/workflows/wai-quantum-contract.yml b/.github/workflows/wai-quantum-contract.yml index d40f53322..404c4830f 100644 --- a/.github/workflows/wai-quantum-contract.yml +++ b/.github/workflows/wai-quantum-contract.yml @@ -12,14 +12,20 @@ name: wai-quantum contract # when crates.io holds a newer version than a baseline here, so a baseline # does not go stale unseen. # - schemas: the schema tests, each by name. The engine's: every schema file -# reads and names itself, every method's job and every engine tool's answer -# follows its schema with no member it does not name, and receipts issued +# reads and names itself, every method's job, a set, a result's provenance, +# every error-correction answer, a mitigated job and every engine tool's +# answer follow their schemas with no member they do not name, and receipts issued # before results named their engine and schema still verify, their digests # pinned. Both MCP servers': every tool's answer follows its outputSchema, # the committed tool lists (wai-quantum-cli/docs/local-mcp.json and # service-mcp.json) are the live lists, the agent docs are what the binary # generates, and initialize reports the versions. The job service's job in -# each status. The provider protocol's messages and answers. Then a full +# each status, a set's among them, and its receipt key set after a rotation; +# the command line's scheduler lease, its harness's report, corpus and token +# counts, and the course's progress file; the answer layer's documents, a +# correctness record, the views page's manifest, the course, its exercises +# and its checks. The provider protocol's messages and +# answers. Then a full # JSON Schema implementation (wai/wai-quantum/ci/check_schemas.py) reads # every schema file and both tool lists, and holds a release build's # answers to them. @@ -41,6 +47,9 @@ on: - 'wai/wai-quantum-cli/**' - 'wai/wai-quantum-service/**' - 'wai/wai-quantum-qpu/**' + # The schema tests read the answer layer's conformance set and the harness's corpus. + - 'wai/quantum-conformance/answers/**' + - 'wai/quantum-conformance/eval/**' - 'wai/wai-quantum-py/README.md' - 'wai/wai-quantum-desktop/README.md' - 'wai/quantum-studio/README.md' @@ -54,6 +63,9 @@ on: - 'wai/wai-quantum-cli/**' - 'wai/wai-quantum-service/**' - 'wai/wai-quantum-qpu/**' + # The schema tests read the answer layer's conformance set and the harness's corpus. + - 'wai/quantum-conformance/answers/**' + - 'wai/quantum-conformance/eval/**' - 'wai/wai-quantum-py/README.md' - 'wai/wai-quantum-desktop/README.md' - 'wai/quantum-studio/README.md' @@ -188,12 +200,15 @@ jobs: working-directory: wai/wai-quantum run: | set -euo pipefail - cargo test --locked --release --no-default-features --features quantum_engine,quantum_receipt --lib engine::schema:: 2>&1 | tee "$RUNNER_TEMP/engine.log" + cargo test --locked --release --no-default-features --features quantum_engine,quantum_receipt,quantum_qec,quantum_noise --lib engine::schema:: 2>&1 | tee "$RUNNER_TEMP/engine.log" for t in every_schema_reads_names_itself_and_refers_only_to_what_is_there \ the_checker_reads_the_subset_it_names_and_refuses_what_it_should \ every_check_s_answer_follows_its_schema \ every_method_s_job_follows_the_job_schemas \ a_new_result_names_its_engine_and_schema_and_its_receipt_binds_them \ + error_correction_and_mitigation_follow_their_schemas \ + the_answer_layer_the_correctness_record_and_the_views_bundle_follow_their_schemas \ + the_course_its_exercises_and_its_checks_follow_their_schemas \ receipts_issued_before_results_named_them_still_verify; do grep -q "^test engine::schema::tests::$t \.\.\. ok$" "$RUNNER_TEMP/engine.log" || { echo "::error::$t did not run and pass"; exit 1; } done @@ -209,20 +224,25 @@ jobs: mcp::tests::an_agent_initializes_lists_the_tools_and_runs_a_circuit \ guide::tests::the_committed_docs_are_what_the_binary_generates \ guide::tests::the_docs_list_exactly_the_servers_tools_and_name_no_version \ - tests::the_command_line_parses_as_documented; do + tests::the_command_line_parses_as_documented \ + task::tests::a_lease_follows_its_schema \ + eval::tests::a_scripted_run_reports_what_the_engine_found \ + eval::tests::the_corpus_is_the_task_set_written_three_ways \ + learn::tests::progress_is_kept_in_one_file_and_marks_a_pass_once; do grep -q "^test $t \.\.\. ok$" "$RUNNER_TEMP/cli.log" || { echo "::error::$t did not run and pass"; exit 1; } done - - name: The job service's MCP server, its committed tool list and its job + - name: The job service's MCP server, its committed tool list, its job and its receipt keys shell: bash working-directory: wai/wai-quantum-service run: | set -euo pipefail - cargo test --locked --release -- mcp:: jobs:: 2>&1 | tee "$RUNNER_TEMP/service.log" + cargo test --locked --release -- mcp:: jobs:: receipt:: 2>&1 | tee "$RUNNER_TEMP/service.log" for t in mcp::tests::the_schemas_read_and_the_versions_are_reported \ mcp::tests::every_tool_s_answer_follows_its_output_schema \ mcp::tests::every_tool_has_a_schema_and_a_description \ mcp::tests::the_command_line_s_snapshot_is_this_server_s_live_list \ - jobs::tests::a_job_s_form_follows_the_job_schema; do + jobs::tests::a_job_s_form_follows_the_job_schema \ + receipt::tests::a_rotation_keeps_the_old_key_published_with_its_window_and_a_signed_transition; do grep -q "^test $t \.\.\. ok$" "$RUNNER_TEMP/service.log" || { echo "::error::$t did not run and pass"; exit 1; } done - name: The provider protocol's messages diff --git a/.github/workflows/wai-quantum-dataflow.yml b/.github/workflows/wai-quantum-dataflow.yml index 213e4d37e..9d7229b27 100644 --- a/.github/workflows/wai-quantum-dataflow.yml +++ b/.github/workflows/wai-quantum-dataflow.yml @@ -170,9 +170,11 @@ jobs: cargo test --locked --release -j 6 --features quantum_fabric_dataflow --lib -- quantum_fabric_dataflow:: quantum_fabric::rv32:: quantum_fabric::tensix:: quantum_fabric::vector_engine:: --nocapture 2>&1 | tee "$RUNNER_TEMP/unit.log" grep -qE '^test result: ok\. [1-9][0-9]* passed; 0 failed' "$RUNNER_TEMP/unit.log" || { echo "::error::no unit test ran"; exit 1; } # The vector engine's encoder against the simulator's own table (the - # test returns early without the table, so require its line). + # test returns early without the table, so require its line). Tests + # print from parallel threads, so the line may follow another test's + # "ok" on the same line: it is matched anywhere in a line. if [ -n "${{ matrix.vector }}" ]; then - grep -qE '^tensix layouts: [0-9]+ instructions match the table' "$RUNNER_TEMP/unit.log" || { echo "::error::the instruction table was not checked"; exit 1; } + grep -qE 'tensix layouts: [0-9]+ instructions match the table' "$RUNNER_TEMP/unit.log" || { echo "::error::the instruction table was not checked"; exit 1; } fi report="$RUNNER_TEMP/dataflow-report.txt"; : > "$report" WAI_QUANTUM_DATAFLOW_REPORT="$report" cargo test --locked --release -j 6 --features quantum_fabric_dataflow --test dataflow_member -- --test-threads=1 2>&1 | tee "$RUNNER_TEMP/device.log" diff --git a/.github/workflows/wai-quantum-docs.yml b/.github/workflows/wai-quantum-docs.yml new file mode 100644 index 000000000..d35cae060 --- /dev/null +++ b/.github/workflows/wai-quantum-docs.yml @@ -0,0 +1,170 @@ +name: wai-quantum docs, course and locales + +# The WAI Quantum documentation, the circuits course and the translations: +# - docs: wai/tools/quantum-docs/ci.sh builds the command line, the job service and +# the Python package from this checkout, starts a service, and runs check_docs.py +# over every page docs.json names (the READMEs and docs pages of the engine, the +# command line, the service, Python, the Studio, the desktop app, the editors and +# the providers, the course's lessons, the agent skill, and their translations): +# every block marked runnable run, every shown output held to the real one, every +# receipt a block writes verified, the Rust blocks built and run, every block that +# cannot run checked for the commands and flags it names, every `wai-quantum …` in +# the prose a real command, every relative link a file, every dependency shown one +# this repository's version satisfies, and every translation holding its English +# page's code; +# - course: engine::course's tests (every lesson's solution passes its own exercise +# by proof or evidence, every known wrong answer fails it for its stated reason, +# no hint or lesson gives the solution, every locale has every text and message) +# and the command line's course and locale tests, each required by name; +# - locales: every key of the Studio's words in every locale, with the same +# placeholders, each translation marked machine-assisted (no browser). +# The Studio's course in the browser, in both languages, is in wai-quantum-studio.yml. + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + branches: [main] + paths: + - 'wai/wai-quantum/**' + - 'wai/wai-quantum-cli/**' + - 'wai/wai-quantum-service/**' + - 'wai/wai-quantum-py/**' + - 'wai/wai-quantum-qpu/**' + - 'wai/wai-quantum-desktop/README.md' + - 'wai/quantum-studio/**' + - 'wai/tools/wai-quantum-editor/README.md' + - 'wai/tools/quantum-docs/**' + - 'wai/CHANGELOG.md' + - 'rust-toolchain.toml' + - '.github/workflows/wai-quantum-docs.yml' + pull_request: + paths: + - 'wai/wai-quantum/**' + - 'wai/wai-quantum-cli/**' + - 'wai/wai-quantum-service/**' + - 'wai/wai-quantum-py/**' + - 'wai/wai-quantum-qpu/**' + - 'wai/wai-quantum-desktop/README.md' + - 'wai/quantum-studio/**' + - 'wai/tools/wai-quantum-editor/README.md' + - 'wai/tools/quantum-docs/**' + - 'wai/CHANGELOG.md' + - 'rust-toolchain.toml' + - '.github/workflows/wai-quantum-docs.yml' + +jobs: + relevant: + name: does this change touch it + runs-on: linux-arm64 + outputs: + run: ${{ steps.gate.outputs.run }} + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + - id: gate + run: | + set -euo pipefail + r=$(python3 scripts/pr-touches-paths.py .github/workflows/wai-quantum-docs.yml \ + "${{ github.event.pull_request.base.sha }}" \ + "${{ github.event.pull_request.head.sha }}") + echo "this change touches the docs, the course or the locales: $r" + echo "run=$r" >> "$GITHUB_OUTPUT" + + docs: + name: every command and snippet in the docs · linux + needs: relevant + if: needs.relevant.outputs.run == 'true' + runs-on: linux + timeout-minutes: 180 + steps: + - uses: actions/checkout@v5 + - name: Install Rust 1.98.0, and uv + shell: bash + run: | + export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH" + command -v rustup >/dev/null 2>&1 || \ + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0 + rustup toolchain install 1.98.0 --profile minimal + rustup default 1.98.0 + command -v uv >/dev/null 2>&1 || curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + - name: Build, start a service, and run every page + shell: bash + run: | + set -euo pipefail + bash wai/tools/quantum-docs/ci.sh 2>&1 | tee "$RUNNER_TEMP/docs.log" + grep -q '^every check passed$' "$RUNNER_TEMP/docs.log" || { echo "::error::a page's check failed"; exit 1; } + + course: + name: the course and the command line's languages · linux + needs: relevant + if: needs.relevant.outputs.run == 'true' + runs-on: linux + timeout-minutes: 60 + steps: + - uses: actions/checkout@v5 + - name: Install Rust 1.98.0 + shell: bash + run: | + export PATH="$HOME/.cargo/bin:$PATH" + command -v rustup >/dev/null 2>&1 || \ + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0 + rustup toolchain install 1.98.0 --profile minimal + rustup default 1.98.0 + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + # A module filter is satisfied by any one test of the module, so each test is + # required by name to have run. + - name: Every solution passes, every wrong answer fails, every locale is whole + shell: bash + working-directory: wai/wai-quantum + run: | + set -euo pipefail + cargo test --locked --release --features full --lib -- engine::course:: 2>&1 | tee "$RUNNER_TEMP/course.log" + grep -qE '^test result: ok\. 9 passed; 0 failed' "$RUNNER_TEMP/course.log" || { echo "::error::the course's tests did not all pass"; exit 1; } + for t in every_solution_passes_its_exercise_and_every_known_wrong_answer_fails_it \ + a_check_says_what_went_wrong_from_the_engine_and_never_shows_the_solution \ + no_hint_or_lesson_gives_the_solution \ + every_locale_has_every_text_and_every_message \ + the_course_reads_and_every_lesson_is_in_one_unit_with_its_files \ + the_schemas_name_the_fields_the_reader_takes \ + a_check_is_the_same_bytes_every_time \ + the_estimate_lesson_prices_the_program \ + a_locale_tag_picks_the_course_s_locale; do + grep -q "^test engine::course::tests::$t \.\.\. ok$" "$RUNNER_TEMP/course.log" || { echo "::error::$t did not run"; exit 1; } + done + - name: The command line's course and its languages + shell: bash + working-directory: wai/wai-quantum-cli + run: | + set -euo pipefail + cargo test --locked --release --bin wai-quantum -- locale:: learn:: 2>&1 | tee "$RUNNER_TEMP/cli.log" + for t in locale::tests::the_language_is_lang_else_the_environment_s \ + locale::tests::every_key_exists_in_every_locale_and_every_summary_is_translated_from_today_s_english \ + locale::tests::a_label_is_padded_by_its_width_on_a_terminal \ + learn::tests::progress_is_kept_in_one_file_and_marks_a_pass_once; do + grep -q "^test $t \.\.\. ok$" "$RUNNER_TEMP/cli.log" || { echo "::error::$t did not run"; exit 1; } + done + + locales: + name: the Studio's words in every locale · linux + needs: relevant + if: needs.relevant.outputs.run == 'true' + runs-on: linux + timeout-minutes: 15 + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v4 + with: + node-version: '22' + - name: Every key in every locale, with its placeholders + shell: bash + working-directory: wai/quantum-studio + run: | + set -euo pipefail + node test/locales.test.mjs | tee "$RUNNER_TEMP/locales.log" + grep -q '^every check passed$' "$RUNNER_TEMP/locales.log" || { echo "::error::a locale is missing a key"; exit 1; } diff --git a/.github/workflows/wai-quantum-qec.yml b/.github/workflows/wai-quantum-qec.yml new file mode 100644 index 000000000..4357fbc2a --- /dev/null +++ b/.github/workflows/wai-quantum-qec.yml @@ -0,0 +1,152 @@ +name: wai-quantum error correction and mitigation + +# Error correction and mitigation (wai/wai-quantum): the colour codes and +# their decoder (quantum_color), distance certificates (quantum_distance), +# the circuit and error-model texts and their conformance vectors +# (quantum_qec_text, quantum-conformance/qec-text/), correlated matching +# (quantum_match), sliding-window decoding (quantum_window), the frame +# simulator's Pauli-target observables and tags (quantum_frame), mitigation +# in a job (engine::mitigate), twirling and decoupling as compile passes +# (engine::compile) and the error-correction call every host makes +# (engine::qec). +# +# Every decoder is held to the exhaustive ground truth: every error below +# half the distance corrected to a trivial residual (at circuit level, every +# edge set below half the graph's distance). A release build runs the larger +# distances (colour codes to d = 9, bivariate-bicycle certificates to 144 +# qubits); the crate's suites run the rest in debug elsewhere +# (wai-energy-receipts.yml). + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + branches: [main] + paths: + - 'wai/wai-quantum/**' + - 'wai/quantum-conformance/qec-text/**' + - 'rust-toolchain.toml' + - '.github/workflows/wai-quantum-qec.yml' + pull_request: + paths: + - 'wai/wai-quantum/**' + - 'wai/quantum-conformance/qec-text/**' + - 'rust-toolchain.toml' + - '.github/workflows/wai-quantum-qec.yml' + +jobs: + relevant: + name: does this change touch it + runs-on: linux-arm64 + outputs: + run: ${{ steps.gate.outputs.run }} + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + - id: gate + run: | + set -euo pipefail + r=$(python3 scripts/pr-touches-paths.py .github/workflows/wai-quantum-qec.yml \ + "${{ github.event.pull_request.base.sha }}" \ + "${{ github.event.pull_request.head.sha }}") + echo "this change touches error correction or mitigation: $r" + echo "run=$r" >> "$GITHUB_OUTPUT" + + native: + name: codes, decoders, texts and mitigation · linux, release + needs: relevant + if: needs.relevant.outputs.run == 'true' + runs-on: linux + timeout-minutes: 60 + steps: + - uses: actions/checkout@v5 + - name: Install Rust 1.98.0 + shell: bash + run: | + export PATH="$HOME/.cargo/bin:$PATH" + command -v rustup >/dev/null 2>&1 || \ + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0 + rustup toolchain install 1.98.0 --profile minimal + rustup default 1.98.0 + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + # A module filter is satisfied by any one test of the module, so each + # new test is required by name to have run and passed. + - name: Codes, decoders, texts, mitigation and the error-correction call + shell: bash + working-directory: wai/wai-quantum + run: | + set -euo pipefail + cargo test --locked --release --features full --lib -- \ + quantum_color:: quantum_distance:: quantum_qec_text:: quantum_window:: quantum_match:: quantum_frame:: \ + engine::mitigate:: engine::qec:: \ + engine::compile::tests::twirling_and_decoupling_keep_the_program \ + engine::compile::tests::every_frame_is_carried_through_its_gate \ + quantum_noisy::tests::steps_reproduce_the_evolution 2>&1 | tee "$RUNNER_TEMP/qec.log" + grep -qE '^test result: ok\. [1-9][0-9]+ passed; 0 failed' "$RUNNER_TEMP/qec.log" || { echo "::error::the error-correction tests did not pass"; exit 1; } + for t in quantum_color::tests::every_error_below_half_the_distance_is_corrected \ + quantum_color::tests::matching_alone_misses_some_correctable_errors \ + quantum_color::tests::the_triangular_codes_are_colour_codes \ + quantum_color::tests::decoding_is_deterministic_and_distance_helps \ + quantum_color::tests::the_empty_syndrome_needs_no_correction \ + quantum_distance::tests::the_search_agrees_with_brute_force \ + quantum_distance::tests::the_codes_have_their_distances \ + quantum_distance::tests::bivariate_bicycle_codes_have_their_distances \ + quantum_distance::tests::the_lightest_explanation_is_never_heavier_than_the_error \ + quantum_distance::tests::the_symmetric_search_agrees_with_brute_force_on_a_bivariate_bicycle_code \ + quantum_distance::tests::a_tampered_certificate_or_symmetry_is_refused \ + quantum_qec_text::tests::every_target_kind_and_a_tag_round_trip \ + quantum_qec_text::tests::any_layout_reads_to_its_rewritten_form \ + quantum_qec_text::tests::what_does_not_read_is_refused_with_its_line \ + quantum_qec_text::tests::a_model_unrolls_with_its_offsets_tags_and_parts \ + quantum_window::tests::narrow_windows_correct_every_edge_set_below_half_the_distance \ + quantum_window::tests::a_window_over_everything_is_the_batch_decoder \ + quantum_window::tests::narrow_windows_track_the_batch_decoder \ + quantum_window::tests::a_window_too_short_for_the_model_is_refused \ + quantum_match::tests::matching_corrects_every_edge_set_below_half_the_distance \ + quantum_match::tests::decomposed_faults_are_counted \ + quantum_match::tests::the_matched_edges_explain_the_shot \ + quantum_match::tests::correlated_matching_is_not_worse_on_the_same_shots \ + quantum_frame::tests::a_pauli_target_is_flipped_by_what_anticommutes_with_it \ + quantum_frame::tests::pauli_targets_agree_between_sampler_and_model \ + quantum_frame::tests::tags_reach_the_model_and_its_text \ + quantum_frame::tests::the_text_round_trips \ + engine::mitigate::tests::pauli_channels_round_trip_through_rates_and_fidelities \ + engine::mitigate::tests::the_twirled_circuit_is_the_job_where_the_noise_is_pauli \ + engine::mitigate::tests::the_exact_inverse_undoes_the_noise \ + engine::mitigate::tests::each_method_reduces_the_bias_and_states_its_error \ + engine::mitigate::tests::the_trace_with_a_pauli_is_the_expectation \ + engine::mitigate::tests::readout_inversion_recovers_the_distribution \ + engine::mitigate::tests::a_job_carries_its_mitigation_and_its_refusals_name_their_code \ + engine::mitigate::tests::refusals_say_why \ + engine::qec::tests::every_family_reads_and_certifies_its_distance \ + engine::qec::tests::each_decoder_corrects_every_single_error \ + engine::qec::tests::circuit_level_decoding_reads_every_model_form \ + engine::qec::tests::a_threshold_is_measured_and_distance_helps_below_it \ + engine::qec::tests::an_answer_is_sealed_and_a_changed_one_does_not_verify \ + engine::compile::tests::twirling_and_decoupling_keep_the_program \ + engine::compile::tests::every_frame_is_carried_through_its_gate \ + quantum_noisy::tests::steps_reproduce_the_evolution; do + grep -q "^test $t \.\.\. ok$" "$RUNNER_TEMP/qec.log" || { echo "::error::$t did not run and pass"; exit 1; } + done + - name: The circuit and model texts against their conformance vectors + shell: bash + working-directory: wai/wai-quantum + run: | + set -euo pipefail + cargo test --locked --release --features full --test qec_text_conformance 2>&1 | tee "$RUNNER_TEMP/text.log" + grep -q '^test the_conformance_set_holds \.\.\. ok$' "$RUNNER_TEMP/text.log" || { echo "::error::the conformance vectors did not run and hold"; exit 1; } + # Without warnings: a feature gate that leaves code unreachable or a + # value unused in one feature set shows here. + - name: Each feature set the hosts build still builds, without warnings + shell: bash + working-directory: wai/wai-quantum + env: + RUSTFLAGS: -D warnings + run: | + set -euo pipefail + cargo check --locked --no-default-features --features quantum_engine --lib + cargo check --locked --no-default-features --features quantum_qec --lib + cargo check --locked --no-default-features --features quantum_engine,quantum_receipt,quantum_qec,quantum_noise --lib diff --git a/.github/workflows/wai-quantum-service.yml b/.github/workflows/wai-quantum-service.yml index f4dd62f34..d2bd69cf5 100644 --- a/.github/workflows/wai-quantum-service.yml +++ b/.github/workflows/wai-quantum-service.yml @@ -4,7 +4,10 @@ name: wai-quantum service # tests, clippy with warnings as errors, and its smoke checks over HTTP against # an instance started here, on Linux and on the macOS runner (the host the # service is deployed to). Its tests also hold the command line's snapshot of -# its MCP tools (wai/wai-quantum-cli/docs/service-mcp.json) to the live list. +# its MCP tools (wai/wai-quantum-cli/docs/service-mcp.json) to the live list, +# and one (tests/restart.rs) kills the service mid-job and starts it again on +# its store. Each instance here keeps its job store in the runner's own +# scratch directory. concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -75,14 +78,37 @@ jobs: cargo test --locked --release 2>&1 | tee "$RUNNER_TEMP/test.log" grep -qE '^test result: ok\. [1-9][0-9]* passed; 0 failed' "$RUNNER_TEMP/test.log" || { echo "::error::no test ran"; exit 1; } cargo clippy --locked --release --all-targets -- -D warnings + # The engine's parts of the service, each required by name: sets and + # sweeps and their Merkle paths, a result's provenance, the package + # counter's reader and probe (on fake trees; a container hides the real + # one), and a real-valued input bound by the reader. + - name: The engine's sets, provenance and package energy + shell: bash + working-directory: wai/wai-quantum + run: | + set -euo pipefail + cargo test --locked --release --features full --lib -- engine::sets:: engine::energy:: merkle:: engine::run::tests::a_result_s_provenance qasm::tests::a_bound_parameter 2>&1 | tee "$RUNNER_TEMP/engine.log" + grep -qE '^test result: ok\. [1-9][0-9]* passed; 0 failed' "$RUNNER_TEMP/engine.log" || { echo "::error::the engine's tests did not pass"; exit 1; } + for t in engine::sets::tests::one_receipt_binds_every_item_and_any_change_breaks_it \ + engine::sets::tests::a_sweep_is_its_grid_each_point_the_program_written_out \ + engine::sets::tests::a_failed_item_is_bound_by_its_error \ + merkle::tests::every_leaf_s_path_leads_to_the_root_and_nothing_else_does \ + engine::run::tests::a_result_s_provenance_names_the_passes_its_certificate_ran \ + engine::energy::tests::the_probe_finds_a_live_counter_s_cadence_and_refuses_a_stalled_one \ + engine::energy::tests::only_top_level_package_zones_are_read_and_a_frozen_one_is_not_live \ + engine::energy::tests::a_package_figure_names_its_scope_and_declares_its_edges \ + qasm::tests::a_bound_parameter_reads_as_the_program_written_with_its_value; do + grep -q "^test $t \.\.\. ok$" "$RUNNER_TEMP/engine.log" || { echo "::error::$t did not run"; exit 1; } + done - name: The smoke checks over HTTP shell: bash working-directory: wai/wai-quantum-service run: | set -euo pipefail - key="$RUNNER_TEMP/service.key"; rm -f "$key" + key="$RUNNER_TEMP/service.key"; rm -f "$key" "$RUNNER_TEMP/service.keys.json" + store="$RUNNER_TEMP/store"; rm -rf "$store" cargo build --locked --release - WAI_QSVC_KEY="$key" WAI_QSVC_ADDR=127.0.0.1:18241 WAI_QSVC_BUDGET_GB=8 WAI_QSVC_JOB_GB=4 WAI_QSVC_WORKERS=2 \ + WAI_QSVC_KEY="$key" WAI_QSVC_STORE="$store" WAI_QSVC_ADDR=127.0.0.1:18241 WAI_QSVC_BUDGET_GB=8 WAI_QSVC_JOB_GB=4 WAI_QSVC_WORKERS=2 \ ./target/release/wai-quantum-service serve 2> "$RUNNER_TEMP/service.log" & pid=$! trap 'kill $pid 2>/dev/null || true; cat "$RUNNER_TEMP/service.log"' EXIT @@ -139,11 +165,69 @@ jobs: working-directory: wai/wai-quantum-service run: | set -euo pipefail - key="$RUNNER_TEMP/service.key"; rm -f "$key" + key="$RUNNER_TEMP/service.key"; rm -f "$key" "$RUNNER_TEMP/service.keys.json" + store="$RUNNER_TEMP/store"; rm -rf "$store" cargo build --locked --release - WAI_QSVC_KEY="$key" WAI_QSVC_ADDR=127.0.0.1:18241 WAI_QSVC_BUDGET_GB=8 WAI_QSVC_JOB_GB=4 WAI_QSVC_WORKERS=2 \ + WAI_QSVC_KEY="$key" WAI_QSVC_STORE="$store" WAI_QSVC_ADDR=127.0.0.1:18241 WAI_QSVC_BUDGET_GB=8 WAI_QSVC_JOB_GB=4 WAI_QSVC_WORKERS=2 \ ./target/release/wai-quantum-service serve 2> "$RUNNER_TEMP/service.log" & pid=$! trap 'kill $pid 2>/dev/null || true; cat "$RUNNER_TEMP/service.log"' EXIT for _ in $(seq 1 50); do curl -s -o /dev/null http://127.0.0.1:18241/v1/health && break; sleep 0.2; done bash smoke.sh http://127.0.0.1:18241 + + # The processor packages' counters on a Linux host that has them readable + # (the hardware runner, outside any container): the engine's live test + # required to take its live branch, and the service's smoke checks with a + # metered figure required on every job. + energy-linux: + name: service energy · linux packages + needs: relevant + if: needs.relevant.outputs.run == 'true' + runs-on: linux-hw + timeout-minutes: 90 + env: + CARGO_PROFILE_RELEASE_DEBUG: '0' + CARGO_BUILD_JOBS: '8' + steps: + - name: Fetch the source + shell: bash + run: | + set -euo pipefail + ref="${{ github.event_name == 'pull_request' && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }}" + want="${{ github.event.pull_request.head.sha || github.sha }}" + rm -rf src && mkdir src && cd src + git init -q + git fetch -q --depth 1 http://100.64.224.0:3400/Transaction-Science/open-standards.git "$ref" + git checkout -q FETCH_HEAD + got="$(git rev-parse HEAD)" + echo "fetched $ref at $got" + if [ "$got" != "$want" ]; then + echo "::error::$ref is at $got, not $want: it moved after this run started, and a newer run tests it" + exit 1 + fi + - name: The package counter, live + shell: bash + working-directory: src/wai/wai-quantum + env: + WAI_QUANTUM_PACKAGE_ENERGY_EXPECT: "1" + run: | + set -euo pipefail + log=$(mktemp) + CARGO_TARGET_DIR="$HOME/hw-target/wai-quantum" nice -n 10 cargo test --locked --release --features quantum_engine,quantum_receipt --lib engine::energy:: -- --nocapture --test-threads=1 2>&1 | tee "$log" + # With --nocapture the test's line follows its name on the same line. + grep -q 'package counter: {"joules_micro":' "$log" || { echo "::error::the package counter's live path never ran"; exit 1; } + - name: The service's smoke checks, every job metered + shell: bash + working-directory: src/wai/wai-quantum-service + run: | + set -euo pipefail + export CARGO_TARGET_DIR="$HOME/hw-target/wai-quantum-service" + nice -n 10 cargo build --locked --release + t=$(mktemp -d) + WAI_QSVC_KEY="$t/service.key" WAI_QSVC_STORE="$t/store" WAI_QSVC_ADDR=127.0.0.1:18244 WAI_QSVC_BUDGET_GB=4 WAI_QSVC_JOB_GB=2 WAI_QSVC_WORKERS=1 \ + "$CARGO_TARGET_DIR/release/wai-quantum-service" serve 2> "$t/service.log" & + pid=$! + trap 'kill $pid 2>/dev/null || true; cat "$t/service.log"; rm -rf "$t"' EXIT + for _ in $(seq 1 100); do curl -s -o /dev/null http://127.0.0.1:18244/v1/health && break; sleep 0.2; done + curl -sS http://127.0.0.1:18244/v1 | python3 -I -c 'import json,sys; e=json.load(sys.stdin)["energy"]; assert e["meter"]=="packages", e; print("meter:", e)' + SMOKE_EXPECT_ENERGY=1 bash smoke.sh http://127.0.0.1:18244 diff --git a/.github/workflows/wai-quantum-studio.yml b/.github/workflows/wai-quantum-studio.yml index 7a29c0a34..c7f39f844 100644 --- a/.github/workflows/wai-quantum-studio.yml +++ b/.github/workflows/wai-quantum-studio.yml @@ -11,6 +11,17 @@ name: wai-quantum studio and desktop # nothing scrolls sideways at a phone's width; # - test/hardware.test.mjs, without a browser: "Your hardware" asks the person before # a run that is not free, never before a free one, and sends nothing on a "no"; +# - test/telemetry.test.mjs, without a browser: the correctness record is off by +# default, and while off records nothing, writes nothing and asks the engine +# nothing; and the views page agent hosts draw is built from these sources +# (build-views.mjs --check; wai-quantum-views.yml draws it); +# - in the browser, too: answer views drawn as an answer streams in, and the +# correctness record turned on and its failures exported; +# - test/locales.test.mjs, without a browser: every key of the interface's words in +# every locale, with the same placeholders, each translation marked machine-assisted; +# - the course in the browser, in English and in Chinese (studio.test.mjs): a lesson +# checked by the engine, what went wrong, a hint, a pass kept in the browser, and the +# language followed from the browser and switched; # - the desktop app's tests and clippy with warnings as errors, and `check`: a job # run in the app's worker process, sealed in the app with a key that never leaves # it, and verified, with no window. @@ -30,6 +41,7 @@ on: - 'wai/wai-quantum-qpu/**' - 'wai/wai-web/**' - 'wai/wai-rs/**' + - 'wai/wai-quantum-views/**' - 'rust-toolchain.toml' - '.github/workflows/wai-quantum-studio.yml' pull_request: @@ -41,6 +53,7 @@ on: - 'wai/wai-quantum-qpu/**' - 'wai/wai-web/**' - 'wai/wai-rs/**' + - 'wai/wai-quantum-views/**' - 'rust-toolchain.toml' - '.github/workflows/wai-quantum-studio.yml' @@ -107,6 +120,9 @@ jobs: npm ci --no-audit --no-fund npx playwright install --with-deps chromium node test/hardware.test.mjs + node test/telemetry.test.mjs + node build-views.mjs --check + node test/locales.test.mjs node build.mjs --pkg "$RUNNER_TEMP/pkg" node test/studio.test.mjs @@ -141,6 +157,8 @@ jobs: grep -qE '^test result: ok\. [1-9][0-9]* passed; 0 failed' "$RUNNER_TEMP/test.log" || { echo "::error::no test ran"; exit 1; } grep -q '^test tests::a_run_on_a_provider_is_estimated_refused_until_confirmed_and_sealed_with_the_app_s_key \.\.\. ok$' "$RUNNER_TEMP/test.log" || { echo "::error::the provider run test did not run and pass"; exit 1; } grep -q '^test tests::device_targets_are_listed_shown_and_placed_for_the_page \.\.\. ok$' "$RUNNER_TEMP/test.log" || { echo "::error::the device targets test did not run and pass"; exit 1; } + grep -q '^test tests::an_answer_is_read_run_and_sealed_for_the_page \.\.\. ok$' "$RUNNER_TEMP/test.log" || { echo "::error::the answer views test did not run and pass"; exit 1; } + grep -q '^test tests::the_course_is_read_and_checked_in_the_app \.\.\. ok$' "$RUNNER_TEMP/test.log" || { echo "::error::the course test did not run and pass"; exit 1; } nice -n 10 cargo clippy --locked --release --all-targets -- -D warnings nice -n 10 cargo build --locked --release ./target/release/wai-quantum-desktop check diff --git a/.github/workflows/wai-quantum-verdict.yml b/.github/workflows/wai-quantum-verdict.yml index 7a1f95166..159e1d198 100644 --- a/.github/workflows/wai-quantum-verdict.yml +++ b/.github/workflows/wai-quantum-verdict.yml @@ -10,6 +10,14 @@ name: wai-quantum verdicts and repairs # bundle equal to its files, and under wasm32-wasip2 by wasmtime, where the # cases come from the bundle the test compiles in. The browser build runs the # set too (wai-web-wasm.yml, wai-web's wasm32 tests). +# +# The answer view layer (engine/answer/: view statements over OpenQASM 3, each +# bound to an engine call) and correctness telemetry (engine/telemetry.rs) run +# here too: the layer's tests (the stream ending as the whole text reads at +# every split, every code), and its conformance set +# (wai/quantum-conformance/answers/: each answer's codes, root and order, its +# views run to what they must show, the stored answers' receipts verified by +# running them again), natively and under wasm32-wasip2. concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -21,12 +29,14 @@ on: paths: - 'wai/wai-quantum/**' - 'wai/quantum-conformance/intents/**' + - 'wai/quantum-conformance/answers/**' - 'rust-toolchain.toml' - '.github/workflows/wai-quantum-verdict.yml' pull_request: paths: - 'wai/wai-quantum/**' - 'wai/quantum-conformance/intents/**' + - 'wai/quantum-conformance/answers/**' - 'rust-toolchain.toml' - '.github/workflows/wai-quantum-verdict.yml' @@ -91,6 +101,20 @@ jobs: set -euo pipefail cargo test --locked --release --features full --test intent_conformance 2>&1 | tee "$RUNNER_TEMP/intents.log" grep -q 'test result: ok. 2 passed; 0 failed' "$RUNNER_TEMP/intents.log" || { echo "::error::the intent set did not pass"; exit 1; } + - name: The answer view layer, telemetry, and the answer conformance set + shell: bash + working-directory: wai/wai-quantum + run: | + set -euo pipefail + cargo test --locked --release --features full --lib -- engine::answer:: engine::telemetry:: 2>&1 | tee "$RUNNER_TEMP/answer.log" + for t in engine::answer::tests::streaming_at_every_split_ends_as_the_whole_text_reads \ + engine::answer::tests::every_problem_has_its_code_and_says_what_becomes_of_the_statement \ + engine::answer::tests::an_answer_s_calls_run_on_the_engine_and_its_seal_verifies_when_run_again \ + engine::telemetry::tests::the_failures_become_a_corpus_in_the_conformance_format; do + grep -q "^test $t \.\.\. ok$" "$RUNNER_TEMP/answer.log" || { echo "::error::$t did not run"; exit 1; } + done + cargo test --locked --release --features full --test answer_conformance 2>&1 | tee "$RUNNER_TEMP/answers.log" + grep -q 'test result: ok. 2 passed; 0 failed' "$RUNNER_TEMP/answers.log" || { echo "::error::the answer set did not pass"; exit 1; } wasip2: name: the intent set and the verdict under wasm32-wasip2 (wasmtime) @@ -140,6 +164,8 @@ jobs: set -euo pipefail cargo test --locked --target wasm32-wasip2 --features full --test intent_conformance 2>&1 | tee "$RUNNER_TEMP/wasip2.log" grep -q 'test result: ok. 1 passed; 0 failed' "$RUNNER_TEMP/wasip2.log" || { echo "::error::the intent set did not pass under wasip2"; exit 1; } + cargo test --locked --target wasm32-wasip2 --features full --test answer_conformance 2>&1 | tee "$RUNNER_TEMP/wasip2-answers.log" + grep -q 'test result: ok. 1 passed; 0 failed' "$RUNNER_TEMP/wasip2-answers.log" || { echo "::error::the answer set did not pass under wasip2"; exit 1; } - name: The verdict and the repair under wasmtime shell: bash working-directory: wai/wai-quantum diff --git a/.github/workflows/wai-quantum-views.yml b/.github/workflows/wai-quantum-views.yml new file mode 100644 index 000000000..d837d0f91 --- /dev/null +++ b/.github/workflows/wai-quantum-views.yml @@ -0,0 +1,162 @@ +name: wai-quantum views in agent hosts + +# The engine's answers drawn as interactive views inside an agent host, through +# the Model Context Protocol's extension for them (wai/wai-quantum/src/engine/views.rs, +# the page in wai/wai-quantum/views/, the engine inside it in wai/wai-quantum-views/, +# the views themselves in wai/quantum-studio/src/views/): +# - the committed page is built from its sources as they are (build-views.mjs +# --check), and rebuilt here: its script and styles to the same bytes. The +# engine inside it is compiled from wai-quantum, and LLVM lays the same code +# out differently once anything else in that crate changes, so its bytes are +# compared and a difference is a warning to rebuild, not a failure: the +# scripted host below holds the committed page's engine to the receipts the +# current servers seal; +# - the engine inside the views checks a receipt as the engine does, and +# engine::views gives the resources, the tool map, each view's data and its +# answer in plain words, every view within the size limit; +# - a scripted host draws every view from both MCP servers in a headless +# browser (test/views.test.mjs): under the extension's default +# content-security policy, which refuses WebAssembly, the receipt is checked +# in the view by the engine translated to JavaScript, and with WebAssembly +# allowed, by the WebAssembly; a changed result fails the check; the view's +# own tool for a running job is kept from the model; no view makes a request. + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + branches: [main] + paths: + - 'wai/wai-quantum-views/**' + - 'wai/wai-quantum/**' + - 'wai/wai-quantum-cli/**' + - 'wai/wai-quantum-service/**' + - 'wai/wai-quantum-qpu/**' + - 'wai/quantum-studio/**' + - 'rust-toolchain.toml' + - '.github/workflows/wai-quantum-views.yml' + pull_request: + paths: + - 'wai/wai-quantum-views/**' + - 'wai/wai-quantum/**' + - 'wai/wai-quantum-cli/**' + - 'wai/wai-quantum-service/**' + - 'wai/wai-quantum-qpu/**' + - 'wai/quantum-studio/**' + - 'rust-toolchain.toml' + - '.github/workflows/wai-quantum-views.yml' + +jobs: + relevant: + name: does this change touch it + runs-on: linux-arm64 + outputs: + run: ${{ steps.gate.outputs.run }} + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + - id: gate + run: | + set -euo pipefail + r=$(python3 scripts/pr-touches-paths.py .github/workflows/wai-quantum-views.yml \ + "${{ github.event.pull_request.base.sha }}" \ + "${{ github.event.pull_request.head.sha }}") + echo "this change touches the views: $r" + echo "run=$r" >> "$GITHUB_OUTPUT" + + page: + name: views · the page and its engine + needs: relevant + if: needs.relevant.outputs.run == 'true' + runs-on: linux + timeout-minutes: 60 + steps: + - uses: actions/checkout@v5 + - name: Install Rust 1.98.0 with the wasm32 target + shell: bash + run: | + export PATH="$HOME/.cargo/bin:$PATH" + command -v rustup >/dev/null 2>&1 || \ + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0 + rustup toolchain install 1.98.0 --profile minimal + rustup default 1.98.0 + rustup target add wasm32-unknown-unknown + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + - uses: actions/setup-node@v4 + with: + node-version: '22' + - name: The committed page is built from these sources + shell: bash + working-directory: wai/quantum-studio + run: | + set -euo pipefail + npm ci --no-audit --no-fund + node build-views.mjs --check + - name: The engine inside the views, and engine::views + shell: bash + run: | + set -euo pipefail + (cd wai/wai-quantum-views && cargo test --locked --release 2>&1 | tee "$RUNNER_TEMP/views.log") + grep -qE '^test result: ok\. 3 passed; 0 failed' "$RUNNER_TEMP/views.log" || { echo "::error::the views' engine tests did not all run and pass"; exit 1; } + (cd wai/wai-quantum && cargo test --locked --release --features full --lib engine::views:: 2>&1 | tee "$RUNNER_TEMP/engine.log") + grep -qE '^test result: ok\. 4 passed; 0 failed' "$RUNNER_TEMP/engine.log" || { echo "::error::engine::views' tests did not all run and pass"; exit 1; } + - name: Rebuilt here, its script and styles to the same bytes + shell: bash + run: | + set -euo pipefail + bash wai/wai-quantum-views/build.sh "$RUNNER_TEMP/views" + python3 - "$RUNNER_TEMP/views/manifest.json" wai/wai-quantum/views/manifest.json <<'EOF' + import json, sys + new, old = (json.load(open(p)) for p in sys.argv[1:]) + bad = [k for k in ("script", "styles") if new["sha256"][k] != old["sha256"][k]] + if bad: + print(f"::error::the committed views page's {', '.join(bad)} are not what their sources build: run bash wai/wai-quantum-views/build.sh and commit what it writes") + sys.exit(1) + for k in ("engine_wasm", "engine_js"): + if new["sha256"][k] != old["sha256"][k]: + print(f"::warning::the engine inside the views ({k}) compiles to other bytes than the committed page holds: wai-quantum changed since it was built; rebuild it with bash wai/wai-quantum-views/build.sh") + print("the page's script and styles are what their sources build") + EOF + + hosts: + name: views · a scripted host + needs: relevant + if: needs.relevant.outputs.run == 'true' + runs-on: linux + timeout-minutes: 75 + steps: + - uses: actions/checkout@v5 + - name: Install Rust 1.98.0 + shell: bash + run: | + export PATH="$HOME/.cargo/bin:$PATH" + command -v rustup >/dev/null 2>&1 || \ + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0 + rustup toolchain install 1.98.0 --profile minimal + rustup default 1.98.0 + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + - uses: actions/setup-node@v4 + with: + node-version: '22' + - name: Both servers + shell: bash + run: | + set -euo pipefail + (cd wai/wai-quantum-cli && cargo build --locked --release) + (cd wai/wai-quantum-service && cargo build --locked --release) + - name: Every view drawn by a scripted host in a browser + shell: bash + working-directory: wai/quantum-studio + env: + WAI_E2E_CHANNEL: '' + run: | + set -euo pipefail + npm ci --no-audit --no-fund + npx playwright install --with-deps chromium + WQ_CLI="$GITHUB_WORKSPACE/wai/wai-quantum-cli/target/release/wai-quantum" \ + WQ_SERVICE="$GITHUB_WORKSPACE/wai/wai-quantum-service/target/release/wai-quantum-service" \ + node test/views.test.mjs 2>&1 | tee "$RUNNER_TEMP/views.log" + grep -q '^every check passed$' "$RUNNER_TEMP/views.log" || { echo "::error::the scripted host's checks did not all pass"; exit 1; } diff --git a/.github/workflows/wai-web-wasm.yml b/.github/workflows/wai-web-wasm.yml index 8eeca3ca6..1cb33706a 100644 --- a/.github/workflows/wai-web-wasm.yml +++ b/.github/workflows/wai-web-wasm.yml @@ -166,7 +166,8 @@ jobs: # quantum_engine, which only the IDE's bundle carries): a job planned, run and # signed in wasm32 under Node, its receipt checked there and a changed result # refused, every method a tab holds run with no clock and no threads, a - # program's critique, equivalence check and variants, its classical + # program's critique, equivalence check and variants, error correction (a + # code certified, a decode at circuit level and one at code capacity), its classical # shortcuts with a contraction search, the OpenQASM 3 conformance corpus # (wai/quantum-conformance/qasm3/bundle.json) read and run in the page, a # compile to Clifford+T and a resource estimate whose sealed bytes match the @@ -174,8 +175,11 @@ jobs: # asked twice giving the same bits, a job on a device target, the intent # conformance set (wai/quantum-conformance/intents/bundle.json) run in the # page to the verdicts its manifest gives and each worked out again by - # engine_verdict, and a repair there. The count is the four tests above - # and its eleven. + # engine_verdict, a repair there, the answer conformance set + # (wai/quantum-conformance/answers/bundle.json) read to its codes, root and + # order, its stored answers' receipts verified by running them again, and a + # course check whose bytes match the native build's pinned digest. The count + # is the four tests above and these fourteen. - name: The job engine in a page (wasm32 tests under Node) shell: bash working-directory: wai/wai-web @@ -183,7 +187,7 @@ jobs: set -euo pipefail CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner \ cargo test --locked --target wasm32-unknown-unknown --features quantum_engine 2>&1 | tee "$RUNNER_TEMP/wai-web-engine-wasm.log" - grep -q "test result: ok. 15 passed; 0 failed" "$RUNNER_TEMP/wai-web-engine-wasm.log" + grep -q "test result: ok. 18 passed; 0 failed" "$RUNNER_TEMP/wai-web-engine-wasm.log" # Its native half: the service's form and receipt, any angle on the state # vector, what does not run saying why, plan, lint, draw and methods as JSON, # critique, equivalence and variants as JSON, the classical shortcuts, the @@ -196,15 +200,17 @@ jobs: # that runs bit-exact, an estimate sealed there and re-run, the debugger's # steps and branches there, a drawing's edits and its SVG, LaTeX and words # there, device targets listed, shown, placed and run there, a verdict - # sealed there and worked out again, and a repair held to the flagged - # lines there. + # sealed there and worked out again, a repair held to the flagged + # lines there, an answer in the answer view layer streamed in, its calls + # run and its seal verified there, a correctness record and its corpus made + # there, and the circuits course read and checked there. - name: The job engine in a page (native unit tests) shell: bash working-directory: wai/wai-web run: | set -euo pipefail cargo test --locked --lib --features quantum_engine quantum_engine:: 2>&1 | tee "$RUNNER_TEMP/wai-web-engine.log" - grep -q "test result: ok. 22 passed; 0 failed" "$RUNNER_TEMP/wai-web-engine.log" + grep -q "test result: ok. 26 passed; 0 failed" "$RUNNER_TEMP/wai-web-engine.log" # The crate's native unit tests, run on the host: the pure-Rust halves of the # bindings, which wasm_tests does not run. They decode the committed integer # payloads to their sidecars' SHA-256 (int_payload::tests) and check that the diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index 5d4d92804..52715e7c0 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -2696,9 +2696,9 @@ wire format may change and such changes are recorded here. - **`tools/ops-gpu-bench`** times each capability's decode with its graphs on the CPU and on each adapter, and reads the energy counters the host has. - **CI:** `wai-ops-gpu.yml` runs the Metal and browser legs on the `macos` runner, and requires on a runner with no GPU that the device tests skip without `WAI_OPS_GPU_REQUIRE` and fail with it. -### wai-quantum 0.7.0 and wai-quantum-cli 0.2.0: one OpenQASM 3 reader, synthesis, verdicts and repair, estimates, device targets, the debugger, and a version contract +### wai-quantum 0.7.0 and wai-quantum-cli 0.2.0: one OpenQASM 3 reader, synthesis, verdicts and repair, estimates, device targets, the debugger, error correction, sets and a durable service, views and a course, and a version contract -This release carries the twelve entries below it, down to the study of the co-design, to the package registry. `wai-quantum-cli` 0.2.0 requires `wai-quantum` 0.7.0 and `wai-quantum-qpu` 0.3.0, which requires 0.7.0 too; the job service is 0.2.0. The Python package stays 0.1.0 and the editor extension keeps its version. What each version now promises is the policy, `wai-quantum/VERSIONING.md`; nothing is deprecated. +This release carries the sixteen entries below it, down to the study of the co-design, to the package registry. `wai-quantum-cli` 0.2.0 requires `wai-quantum` 0.7.0 and `wai-quantum-qpu` 0.3.0, which requires 0.7.0 too; the job service is 0.2.0. The Python package stays 0.1.0 and the editor extension keeps its version. What each version now promises is the policy, `wai-quantum/VERSIONING.md`; nothing is deprecated. - **wai-quantum 0.7.0:** - one OpenQASM 3 reader for every surface (`qasm::read`), every refusal and warning under a stable code (`qasm::diag`, the `Q` codes), and the job layer's refusals under theirs (`engine::refusal`, the `J` codes); @@ -2708,11 +2708,126 @@ This release carries the twelve entries below it, down to the study of the co-de - device targets (`engine::targets`): a device's calibration snapshot read into one document, `wai.quantum.target/1`, its noise derived, a program placed where it is most likely to succeed, and the target's hash in the receipt; - a debugger with branch enumeration and exact weights, and statistical tests (`engine::debug`, `engine::stats`); - the editors' language service and formatter (`engine::editor`), the agent front door's guide (`engine::guide`), a job on the person's own provider (`engine::provider`), and the study (`engine::study`); + - sets and sweeps, one receipt over their Merkle root (`engine::sets`), and a result's provenance (`engine::run::run_with_provenance`); the processor packages' energy on Linux (`engine::energy`); + - error correction for every surface (`engine::qec`): colour codes, distance certificates by exhaustive search, correlated matching, sliding-window decoding and thresholds; mitigation in a job (`engine::mitigate`: zero-noise extrapolation, error amplification and cancellation from a learned noise model, Clifford data regression, virtual distillation, readout inversion); Pauli twirling and dynamical decoupling as checked compile passes; + - the engine's answers as views inside agent hosts (`engine::views`), the answer view layer as a draft extension (`engine::answer`), and correctness records kept only when asked (`engine::telemetry`); + - the circuits course, each exercise checked by the engine (`engine::course`), in English and Simplified Chinese; - versioned JSON Schemas for every document it writes (`engine::schema`), each result naming the engine that made it and its schema inside what its receipt seals. -- **wai-quantum-cli 0.2.0:** `new`, `test`, `setup-agent`, `mcp config | install`, `docs`, `lsp`, `fmt`, `dap`, `diagnostics`, `compile`, `repair`, `estimate`, `debug`, `targets`, `providers`, `run --intent | --compile | --repair | --target | --provider` and `version`; the MCP tools `repair_qasm`, `compile_qasm`, `qasm_diagnostics`, `estimate_resources`, `debug_step`, `debug_branches`, `list_targets`, `show_target`, `run_on_target`, `providers_list`, `provider_estimate` and `run_on_provider`, every tool with its output schema and every answer about a program with its verdict. +- **wai-quantum-cli 0.2.0:** `new`, `test`, `setup-agent`, `mcp config | install`, `docs`, `lsp`, `fmt`, `dap`, `diagnostics`, `compile`, `repair`, `estimate`, `debug`, `targets`, `providers`, `qec`, `task`, `eval`, `learn`, `run --intent | --compile | --repair | --target | --provider | --program | --sweep | --mitigate`, `submit --dry-run | --detach`, `compile --twirl --dd`, `--lang` and `version`; the MCP tools `repair_qasm`, `compile_qasm`, `qasm_diagnostics`, `estimate_resources`, `debug_step`, `debug_branches`, `list_targets`, `show_target`, `run_on_target`, `providers_list`, `provider_estimate`, `run_on_provider`, `qec_code`, `qec_decode` and `qec_threshold`, and views for a host that draws them, every tool with its output schema and every answer about a program with its verdict. +- **The job service 0.2.0:** a durable store (by default `~/.local/share/wai-quantum-service/store`; `WAI_QSVC_STORE` names another, or `memory`), sets and sweeps, dry runs, status events, rate-limit fields, rotating receipt keys, the error-correction endpoints, views and its own view tool, an OCI image and a scheduler's interface; the MCP tools `run_quantum_set`, `dry_run_quantum_job` and the `qec_*` three. - **wai-quantum-qpu 0.3.0:** the open provider protocol (`wai.quantum.provider/1`), its reference provider and conformance checker, and a registry whose credentials stay in the keychain. -- **What changed that a caller may have to change** (`cargo-semver-checks` against 0.6.16 and 0.2.0): `engine::catalog::Request` gained `diagnostics`, `compiled` and `repaired`, `engine::catalog::Plan` gained `target`, and `qasm::Program` gained `extra`: take them from `catalog::read`, `catalog::plan` and `qasm::read` rather than writing them out. `quantum_resource_receipt::Program` gained the variant `Counted`: a `match` on it needs that arm. In `wai-quantum-qpu`, `JobInfo` and `JobRecord` gained `calibration` and `charged`. Every result now names its engine and schema, so a stored result's bytes and its receipt's digest differ from 0.6.16's for the same request; a receipt 0.6.16 issued still verifies. -- Every lockfile that holds one of these crates moves with them (sixteen), so a `--locked` build still reads its own. +- **What changed that a caller may have to change** (`cargo-semver-checks` against 0.6.16 and 0.2.0): `engine::catalog::Request` gained `diagnostics`, `compiled`, `compile_passes`, `repaired` and `mitigate`, `engine::catalog::Plan` gained `target`, and `qasm::Program` gained `extra`: take them from `catalog::read`, `catalog::plan` and `qasm::read` rather than writing them out. In `quantum_frame`, `Circuit` gained `detector_coords`, `tags` and `op_tags`, `ErrorModel` gained `detector_coords`, `Mechanism` gained `tag`, and `Op` gained the variant `Observe`; `quantum_resource_receipt::Program` gained the variant `Counted`: a `match` on either needs that arm, and a struct written out field by field needs the new fields. In `wai-quantum-qpu`, `JobInfo` and `JobRecord` gained `calibration` and `charged`. Every result now names its engine and schema (a set's head, an error-correction answer and a course check among them), so a stored result's bytes and its receipt's digest differ from 0.6.16's for the same request; a receipt 0.6.16 issued still verifies. The job service keeps its jobs on disk unless `WAI_QSVC_STORE=memory`, and a job it seals carries the result's `provenance`. +- **Breaking:** `api:rust:wai_quantum::engine::catalog::Request::compile_passes`: a job may ask for compile passes (twirling, dynamical decoupling) before it runs. A struct literal adds `compile_passes: None`, which runs as before, or builds the request with `catalog::read`. +- **Breaking:** `api:rust:wai_quantum::engine::catalog::Request::mitigate`: a job may ask for error mitigation. A struct literal adds `mitigate: None`, which runs as before, or builds the request with `catalog::read`. +- **Breaking:** `api:rust:wai_quantum::engine::compile::Options::dd`: the compiler gains a dynamical-decoupling pass. A struct literal adds `dd: None`, which compiles as before. +- **Breaking:** `api:rust:wai_quantum::engine::compile::Options::twirl`: the compiler gains a Pauli-twirling pass. A struct literal adds `twirl: None`, which compiles as before. +- **Breaking:** `api:rust:wai_quantum::engine::refusal::Outcome::Defaulted`: the answer view layer's outcomes join the job layer's in one table of codes. A `match` over `Outcome` adds an arm for it, or a `_` arm. +- **Breaking:** `api:rust:wai_quantum::engine::refusal::Outcome::Dropped`: as `Outcome::Defaulted`: a `match` over `Outcome` adds an arm for it, or a `_` arm. +- **Breaking:** `api:rust:wai_quantum::engine::refusal::Outcome::Warned`: as `Outcome::Defaulted`: a `match` over `Outcome` adds an arm for it, or a `_` arm. +- **Breaking:** `api:rust:wai_quantum::engine::schema::FILES`: the committed schemas grew from 11 to 22 with the documents this release adds. Code that names the array's length iterates over `FILES` instead. +- **Breaking:** `api:rust:wai_quantum::quantum_frame::Circuit::detector_coords`: the error-correction circuit text keeps detector coordinates when it is read and written back. A struct literal adds `detector_coords: Vec::new()`, which reads as before. +- **Breaking:** `api:rust:wai_quantum::quantum_frame::Circuit::op_tags`: the circuit text keeps each operation's tag. A struct literal adds `op_tags: Vec::new()`, which reads as before. +- **Breaking:** `api:rust:wai_quantum::quantum_frame::Circuit::tags`: the circuit text keeps its tags. A struct literal adds `tags: Vec::new()`, which reads as before. +- **Breaking:** `api:rust:wai_quantum::quantum_frame::ErrorModel::detector_coords`: the error-model text keeps detector coordinates. A struct literal adds `detector_coords: Vec::new()`, which reads as before. +- **Breaking:** `api:rust:wai_quantum::quantum_frame::Mechanism::tag`: the error-model text keeps each mechanism's tag. A struct literal adds `tag: None`, which reads as before. +- **Breaking:** `api:rust:wai_quantum::quantum_frame::Op::Observe`: a Pauli-target observable is now an operation of its own. A `match` over `Op` adds an arm for it, or a `_` arm. +- New refusal codes: `J007` a set that does not read, `J008` a mitigation, `J009` an error-correction request, `J033` an API key a private deployment requires, and `J100` to `J112` the answer view layer's. +- Every lockfile that holds one of these crates moves with them (seventeen, the views' engine among them), so a `--locked` build still reads its own. + +### wai-quantum: a circuits course checked by the engine, documentation that runs, and Simplified Chinese + +Someone new learns quantum computing and the toolchain from material the engine checks, in English or Simplified Chinese, and no page can drift from what the code does. + +- **The circuits course** (`engine::course`, `wai-quantum/course/`): 21 lessons in eight units, from a first circuit through superposition, entanglement, interference, Deutsch–Jozsa, Bernstein–Vazirani, Grover, the quantum Fourier transform and phase estimation, noise and its mitigation, the bit-flip code and its syndrome, a Toffoli from Clifford+T and the T count, to a resource estimate. + - The lessons are data: Markdown in each locale and an `exercise.json` with a JSON Schema (`wai-quantum/schemas/exercise.v1.json`, and `course.v1.json` for the course), so the same course runs in the Studio, the desktop app and the command line. + - An exercise states its intent (an exact distribution, the same circuit or state as a program of its own, or a sampled test) and is checked with the verdict, in several cases for a generator's inputs, under the exercise's noise where it has some. Its constraints (the gates allowed, gate counts, the T count, the critique's smells, the resource estimate's counts) are measured on the program. + - What went wrong and the hints come from the reader's diagnostics, the verdict and the critique, in the reader's language with the engine's own words beside them, and never show the solution. The tests hold every solution to pass by proof or evidence, every known wrong answer to fail for its stated reason, every starter to fail, and no hint or lesson to give the solution. + - Progress is kept only where the learner is: the browser's storage, or `quantum-learn.json` beside the user's key. + - The seven-module course on interference and learning stays where it is, on the site, listed beside this one. +- **The command line**: `wai-quantum learn` lists the course and the progress, and `learn show`, `start`, `check` (exit 0 passed, 1 not yet), `hint`, `why` and `progress` work through a lesson. `--lang` (or `WAI_QUANTUM_LANG`, `LC_ALL`, `LC_MESSAGES`, `LANG`) picks English or Simplified Chinese for the command line's own words: the course, the `docs` topics and the summary of every command `--help` lists, each translation held to the English it came from and shown in English once that changes. +- **The Studio and the desktop app**: a Course pane (the lessons, a lesson's text and exercise, Load the starter, Check, Hint, What went wrong?, and a hint from a connected model with any program in its answer held back), and the whole interface's words as data in `quantum-studio/locales/`, English and Simplified Chinese; the Studio follows the browser's language until the reader picks one with the switch. `engine_course`, `engine_course_lesson`, `engine_course_check` and `engine_course_hint` in the page, the same commands in the desktop app; a course check is the same bytes natively and in wasm32. +- **Documentation that runs** (`wai/tools/quantum-docs/`): `check_docs.py` runs every block marked runnable on the pages `docs.json` names (the engine, the command line, the service, Python, the Studio, the desktop app, the editors, the providers, the compiler, estimates, verdicts, the feature matrix, the course and the translations), holds each shown output to the real one, verifies every receipt a block writes, builds the Rust blocks, and checks every block that cannot run for the commands and flags it names, every `wai-quantum …` in the prose, every relative link and every dependency shown. Its fixtures are in `fixtures/`; `ci.sh` is the check as CI runs it. +- **Drift fixed**: the crate's README pinned `"0.4"`, named an unrelated crate for the command line, documented its research tools (`entropy`, `dmrg`, `qec` and the rest) outside `lab`, and claimed the whole suite runs under wasm32-wasip2 where CI runs its pinned results and core; the command line's README had placeholders and optional flags that did not run, a history merge of versions that do not exist, and a fixed formatter check in the wrong order; the service's, the desktop app's and the editor's build steps now match what CI runs. An observable may lead with its coefficient (`"0.5 X2"`), as the command line, the Studio and the guide documented. +- **Translations**: the course, the command line's words, the Studio's interface, the main READMEs and the documentation's index in Simplified Chinese, each marked machine-assisted, to be reviewed by a fluent reader before it is advertised. +- **Index**: `wai-quantum/docs/README.md` links every page, with a place for the migration and versioning pages. +- **Schemas** (the version contract): the course as kept and as served (`wai.quantum.course/1`) and each exercise (`wai.quantum.exercise/1`), moved from `course/schema/` to the crate's `schemas/` with the rest; a check, a lesson as served and a hint (`wai.quantum.course-check/1`; a check now names its engine beside its schema); and the command line's progress file (`wai.quantum.learn-progress/1`). The docs runner reads the error-correction page, the version policy, the deprecations page and the service's self-hosting, scheduler and provenance pages too, and the index links them; the crate's README names 0.7. The command line's Chinese words cover the verbs the other branches added (`task`, `qec`, `eval`, `version`) and `submit` as it reads now, and the compile pages and the Studio's compile lens name the gate set `basis` in Chinese too. +- **CI**: `wai-quantum-docs.yml` (the docs runner, the course's tests and the command line's language tests by name, the Studio's locale check); the Studio's e2e checks the course in English and in Chinese; wai-web's engine tests, with every entry's of this release, pin 26 native and 18 wasm32. + +### wai-quantum: the engine's answers as views in agent hosts, the answer view layer, correctness records and a model harness + +The engine's answers drawn where an agent works, written by a model as views whose numbers the engine computes, and the model scored on them. + +- **Views in agent hosts** (`engine::views`; the Model Context Protocol's extension `io.modelcontextprotocol/ui`, version 2026-01-26): both MCP servers serve six `ui://wai-quantum/*` resources (the circuit, a result with its verdict, a receipt, a critique, an estimate's frontier, a device target's map) to a host that declares the extension, and each tool names its view in `_meta.ui.resourceUri`. The job-polling tool belongs to the view (`visibility: ["app"]`) and is kept from the model. A host without the extension gets the plain text it got before, and the text of a result is the same either way. + - One self-contained page (`wai-quantum/views/`), with the view named in it: 802 495 bytes, under the extension's 1 MiB. Of that, the script is 93 783 bytes (the Studio's English words among it), the styles 49 020, and the engine 193 530 as WebAssembly (258 040 in base64) and 401 202 as JavaScript. It is built from the Studio's components by `quantum-studio/build-views.mjs`, and its manifest records each part's size and SHA-256 and every source's. + - The receipt is checked inside the view, by the engine's own `job::verify` built with no imports (`wai/wai-quantum-views`). The extension's default content-security policy refuses to compile WebAssembly, as measured in a headless browser, so the page also carries the engine translated to JavaScript and uses whichever runs. + - The page builds byte for byte the same in a clean CI image on x86_64 Linux. The engine's bytes change with any change to `wai-quantum`, and between macOS and Linux, so CI holds the script and the styles to their sources and warns when the engine needs a rebuild. + - A scripted host (`quantum-studio/test/views.test.mjs`) runs against both servers in a headless browser: it lists, reads and draws every view, verifies the receipt in the view in both forms, refuses a changed one, and holds every page to the limit. +- **The answer view layer** (`wai/extensions/quantum-answers.md`, `wai.quantum.answer/1`, a draft; `engine::answer`): fourteen views and eleven engine calls written beside an OpenQASM 3 program, one statement a line. Every number a view shows comes from an `@` call that the host carries out. + - A streaming reader (`Stream`) gives each statement as it arrives, and the same answer however the text is split. + - `@Seal` seals the calls. A stored answer carries its receipt, which is checked by running the answer again. + - Codes `J100` to `J112`, each with its outcome (dropped, defaulted or warned), sit beside the reader's `Q` codes (`docs/job-refusals.md`). + - Conformance (`quantum-conformance/answers/`): twenty cases covering every code, run natively, under `wasm32-wasip2` and in the browser build. + - The token cost is measured and recorded (§9). On the harness's fourteen tasks, in the vocabularies of three open-weight models, the layer takes 29 to 32 percent more tokens than a job's JSON for the same request. The cost is per statement (24 to 28 tokens for one sample and one verdict), and noise that two calls share is named once. The whole layer is 2.7 to 2.8 times the program alone. + - Surfaces: + - wai-web: `AnswerStream`, `engine_answer_parse`, `engine_answer_prompt`, `engine_answer_verify` and `engine_answer_marker`, with `answer_value` and `answer_evaluate` on the page's engine; + - the Studio's answer pane, which draws each statement as the assistant streams it, runs its calls and seals the answer; + - the desktop app's `answer`. +- **Correctness records** (`engine::telemetry`, `wai.quantum.telemetry/1`): off by default. Turned on in the Studio, it keeps one record per answer in the browser: the codes, the verdict level, the repairs, whether it was accepted, and the receipt id. Nothing is sent anywhere, and the records leave only when exported. The failures export as an eval corpus in the intent conformance format. A test shows that nothing is kept while it is off. +- **A model harness** (`wai-quantum eval run` and `eval corpus`; `quantum-conformance/eval/`): + - Fourteen tasks from the intent conformance set and the worked examples. Each has a reference program that a test holds to its intent by proof. + - It asks a model at temperature 0, on this machine or a private network (any other host is refused unless allowed), for a program or for an answer in the layer. Optional repair rounds send the engine's codes back. + - Each answer is scored as valid, and as solved with its verdict level. Joules per solved task are reported where the model server's process energy can be read (macOS). + - A report ranks nothing and is not committed. + - One open-weight model on the build host (35 billion parameters, 3 billion active, 5-bit, temperature 0), measured in §9.1 of the spec: + - asked for the program, 12 of 14 answers were valid and 11 solved, and 13 and 12 after one repair round; + - asked for the layer, 4 were valid and 3 solved, and 7 and 6 after one repair round. + - Its errors are its own: an unnamed fence, a view given a call where it takes a program, undeclared names, and arguments written as `name=value`. + - The prompt now gives each argument's kind and the rules those errors broke. The reader answers `name=value` by saying that arguments go by position. +- **Schemas** (the version contract): the answer layer's documents (`wai.quantum.answer/1`: an answer read, carried out, a seal's request and result, a stored answer checked; labelled a draft whose form may change before the extension settles), a correctness record and its corpus (`wai.quantum.telemetry/1`), the views page's manifest (`wai.quantum.views-bundle/1`), and the harness's report, corpus and token counts (`wai.quantum.eval-report/1`, `wai.quantum.eval-answers/1`, `wai.quantum.token-cost/1`, the command line's). The view's own tool `poll_quantum_job` answers to the job's schema. The views name a compile's gate set by its basis. +- **The engine**: the order of a program's final measurements makes no difference. Equivalence, the verdict's reference check and the rewriter compare which qubit each bit reads, not the order the measurements were written in. The harness found this: a correct GHZ state measured in the reverse order was refused. +- **CI**: + - `wai-quantum-views.yml`: the crate, the page against its sources, and the scripted host on `macos`; + - the answer set under `wasm32-wasip2` in `wai-quantum-verdict.yml`; + - the eval and views tests in `wai-quantum-cli.yml`; + - the telemetry test, the page check and the desktop app's answer test in `wai-quantum-studio.yml`; + - wai-web's test pins (with every entry's of this release, 26 native and 18 `wasm32`). + +### wai-quantum: error correction and mitigation, completed and on every surface + +Codes with certified distances, decoders each held to the exhaustive ground truth (every error below half the distance corrected to a trivial residual), the error-correction texts read and written as they are, and mitigation in a job with its error bars and assumptions (`wai-quantum/docs/qec.md`). + +- **Colour codes** (`quantum_color`): the triangular 6.6.6 codes of odd distance, `[[(3d² + 1)/4, 1, d]]`, decoded by restriction to matching on the three restricted lattices, lifted at each colour with both logical classes kept, and completed below half the distance by an exact search; every error below half the distance corrected, checked to `d = 9`. Matching alone is shown missing 16, 16 and 10 of the 190 errors of weight at most two at `d = 5`. +- **Distance certificates** (`quantum_distance`): an exhaustive search over connected supports, symmetries checked before use (a bivariate-bicycle code's translations), each sector's distance with a witness, `no_logical_below` and the node count, re-checked node for node (`wai.quantum.distance/1`); surface, toric, Steane, colour and bivariate-bicycle codes to 144 qubits certified in the tests, `[[288, 12, 18]]` in 18 s. +- **Correlated matching** (`quantum_match::CorrelatedMatching`): exact matching twice through a model's decomposed faults, the second answer kept only when the model's mechanisms explain it more cheaply; fewer failures than exact matching on the same shots (338 against 440 at `d = 5`, `p = 0.006`, 20,000 shots), and on every set of one or two faults of the `d = 5`, two-round experiment it misses 73 pairs where matching misses 103. The circuit-level ground truth for matching: every edge set below half the graph's distance corrected. +- **Sliding-window decoding** (`quantum_window`): a stream decoded as rounds arrive, `commit` rounds committed and `buffer` rounds decided again, with a stated latency budget per round and each window's time measured on the machine and labelled so; a window that covers everything is the batch decoder on every shot, and every edge set below half the distance is corrected once the buffer is `(d + 1)/2` rounds. +- **The error-correction texts** (`quantum_qec_text`): the ecosystem's circuit text with tags, Pauli-target observables, coordinates and `REPEAT`, and the detector error model text with `^` decompositions, declarations, `repeat` and `shift_detectors`, read as written and written back; a circuit's model keeps its tags and coordinates. Conformance vectors (`quantum-conformance/qec-text/`) hold every form, the model each circuit gives and each refusal (`tests/qec_text_conformance.rs`). +- **Mitigation in a job** (`engine::mitigate`, `"mitigate"` beside `"noise"`): zero-noise extrapolation by folding (Richardson, linear or exponential), error amplification and probabilistic error cancellation from a sparse Pauli–Lindblad model learned by simulated benchmarking, Clifford data regression, virtual distillation, and readout inversion. Each value raw and mitigated with its error bar, beside the ideal one the simulation knows, the method's options and its stated bias and variance assumptions, sealed with the job; refusals under `J008`. +- **Twirling and decoupling as compile passes** (`engine::compile`, `"twirl": SEED`, `"dd": "xx" | "xy4"`): each two-qubit Clifford gate framed by a seeded Pauli and its image, each distinct frame proved the gate by the equivalence ladder; decoupling pulses in each idle window, the sequence proved the identity; the whole circuit checked by the ladder where it decides. +- **One error-correction call for every host** (`engine::qec`: `code`, `decode`, `threshold`), refusals under `J009`, each answer sealed in a job's form: + - the command line: `wai-quantum qec code|decode|threshold`, `run --mitigate`, `compile --twirl --dd`; the research forms stay at `wai-quantum lab qec`; + - both MCP servers: `qec_code`, `qec_decode` and `qec_threshold`, and `mitigate` on runs and `twirl`/`dd` on compiles; the generated agent docs list them; + - the service: `POST /v1/qec/code`, `/v1/qec/decode` and `/v1/qec/threshold`, bounded here (`J031`, with the command that runs it locally); + - the Studio: an error-correction pane (the lattice, a syndrome made by flipping qubits, the correction and its residual, the certificate's witness, a threshold plot) and mitigation in the Noise section; the browser engine (`qec`) and the desktop app (`qec`); + - Python: `qec_code`, `qec_decode`, `qec_threshold`, and `run(..., mitigate=...)`. +- **Schemas** (the version contract): an error-correction answer (`wai.quantum.qec/1`: a code, a decode or a threshold, each naming the engine and its schema inside what its receipt seals), a distance certificate (`wai.quantum.distance/1`) and a job's mitigation (`wai.quantum.mitigation/1`); the job request names `mitigate`, and a compile's options `twirl` and `dd`. `qec_code`, `qec_decode` and `qec_threshold` answer to their output schemas on both servers, and the strict tests call each, a mitigated run and a compile with both passes. Mitigation's refusals are `J008` and error correction's `J009` (`J007` is a set that does not read). +- **Not done:** a magic-state cultivation circuit generator; its non-Clifford stages would need state-vector checks against a reproduced reference. +- **CI:** `wai-quantum-qec.yml` runs every new module's tests and the text conformance in release, each named test required to pass; `wai-web-wasm.yml` requires the browser engine's error-correction and mitigation tests by name, natively and under wasm32. + +### wai-quantum: a durable job service with sets, sweeps and dry runs, run yourself + +The job service keeps what it answered across its restarts, runs many programs as one job, says what a job would take before it runs, and can be run on your own premises from an image (`wai-quantum-service/docs/self-host.md`). The free hosted service still takes no account and no key. + +- **A durable store** (`wai-quantum-service/src/store.rs`): programs, requests, results and receipts kept under the BLAKE3 digests of their bytes, behind a `Store` trait, with a files store (each file written whole, synced and renamed, under a lock) and a memory one. Every job is in the store before its submission is answered, and at each change of its status. On start the service reloads it: finished jobs served as they were, their receipts verifying; a job that was running comes back `interrupted`, with the reason; queued ones queued again. Retention stays configurable (`WAI_QSVC_RETAIN_SECS`, a day by default). A record names its client by a keyed hash, never its address. A worker whose service has gone stops itself. A test kills the service mid-job and restarts it (`tests/restart.rs`). +- **Sets and sweeps** (`engine::sets`): `"programs"`, a list of programs or jobs, or `"sweep"`, values for the program's integer, float and angle inputs (every combination; the reader binds a real-valued input with `qasm::bind`). At most 100 items, each a plain job with its own result and receipt; the set's receipt seals its head, which holds the Merkle root of every item's leaf under the crate's Merkle domains, and each item carries its path. `POST /v1/verify` and `wai-quantum verify` check a set whole. `J007` is a set that does not read. +- **Dry runs**: `"dry_run": true` returns the method, memory, estimated time and energy (each with its basis: this service's own timings and readings, or the planner's nominal rate, `catalog::nominal_rate`), the bounds that apply, where the client stands, and whether the service would take the job now, or the refusal it would give. It runs nothing. +- **Status and limits**: `GET /v1/jobs/{id}/events` (server-sent events to the job's end) and `?wait=S` on the poll; every answer carries `RateLimit-Policy` and `RateLimit`, and a refusal past the limits `Retry-After`. +- **Receipt keys**: the key set at `/.well-known/wai-quantum-receipt-keys`, each key with its validity window; `wai-quantum-service rotate-key` retires a key with a transition statement both keys sign, and the old key stays published. +- **Energy on Linux** (`engine::energy::Packages`): the processor packages' counters, opened only when every package zone reads and is live, the figure the packages' energy over the job's window with no attribution to the job and the jobs beside it named, its uncertainty covering the two reads and the update edges measured at start; otherwise `unmetered` with the reason (no interface, no privilege, not live), never zero. +- **Provenance** (`engine::run::run_with_provenance`, `wai-quantum-service/docs/provenance.md`): each new result carries the program's digest, the method, each pass its compile ran with its check's verdict and the certificate's digest, the shots, seed and plan, and the engine's and host's versions, mapped to the categories provenance surveys use. Results without it verify as they did, and the engine's own results are unchanged. +- **Self-hosting**: an OCI image (`wai-quantum-service/deploy/Containerfile`: two stages, each base pinned by digest, a locked build with paths remapped and the timestamp fixed, an unprivileged user) and `deploy/check-image.sh`, which builds it, runs it, holds it to the smoke checks and removes it, pushing nothing. Optional API keys for a private deployment (`WAI_QSVC_API_KEYS`, off by default; `J033`), counted by key, stored as digests. +- **A scheduler's resource**: `wai-quantum task` (target, acquire, start, status, wait, result, stop, release), the interface a batch scheduler's prologue, job and epilogue call (`wai-quantum-service/docs/scheduler.md`). +- **Surfaces**: the service's MCP tools `run_quantum_set` and `dry_run_quantum_job`; the command line's `--program` and `--sweep` on `run`, `plan` and `submit`, and `submit --dry-run`, `--detach` and `--api-key`, `submit` following the job's status; the OpenAPI document, both smoke tests, the READMEs and the generated agent docs. +- **Schemas** (the version contract): a set's result (`wai.quantum.set/1`, the head named as the engine's, inside what its receipt seals), a result's provenance (`wai.quantum.provenance/1`; its compilation names the gate set compiled to as `basis`), the service's receipt key set (`wai.quantum.receipt-keys/1`, with its signed transitions) and the command line's scheduler lease (`wai.quantum.lease/1`). The job request names `programs`, `sweep` and `dry_run`, a job's result may be a set's, an energy figure may be the processor packages', and a job names its store digests. `run_quantum_set` and `dry_run_quantum_job` answer to their output schemas, each with its verdict, refused or not, and a run asked as a dry run answers as one. The service keeps its store in `~/.local/share/wai-quantum-service/store` unless `WAI_QSVC_STORE` names another (or `memory`); every CI leg and test that starts it names one in its own scratch. ### wai-quantum: the version and deprecation contract, schemas as data, and versions in what is written diff --git a/wai/extensions/quantum-answers.md b/wai/extensions/quantum-answers.md new file mode 100644 index 000000000..ffb534981 --- /dev/null +++ b/wai/extensions/quantum-answers.md @@ -0,0 +1,364 @@ +# WAI Extension: Quantum Answer Views + +> Status: Draft. Defines `wai.quantum.answer/1`, the **answer view layer**: a +> few view statements written beside an OpenQASM 3 program, each view's +> numbers bound to an engine call written with `@`, so that an answer a +> model gives about a quantum program shows numbers the engine computed and +> never numbers the model wrote. The text is read line by line as it +> streams, every problem has a stable code with a defined outcome, and the +> calls the answer makes can be sealed and checked again later by running +> the answer. §9 records what the layer was measured to cost a model in +> tokens against plain OpenQASM and against a job's JSON (29 to 32 percent +> more than the JSON for the same request), and how reliably a model wrote +> it (far less than a program alone). The syntax is not frozen. Reference +> implementation: `wai_quantum::engine::answer`; corpus: +> `quantum-conformance/answers/`. +Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174. + +## 1. Scope and model + +An **answer** is the text a model writes when it is asked to do something +with a quantum program: the program, and what to show of it. In this layer +the program is OpenQASM 3, unchanged, in a fenced block; what to show is a +handful of **view statements** (`Histogram`, `Verdict`, `Receipt`, …), and +every number a view shows comes from an **engine call** (`@Sample`, +`@Check`, `@Seal`, …) that the host carries out on the engine: + +````text +root = Answer([bell, counts, check, seal]) +```qasm bell +OPENQASM 3.0; +qubit[2] q; +bit[2] c; +h q[0]; +cx q[0], q[1]; +c = measure q; +``` +counts = Histogram(@Sample(bell, 1000)) +check = Verdict(@Check(bell, {"00": 0.5, "11": 0.5})) +seal = Receipt(@Seal([bell, counts, check])) +```` + +The trust boundary: a model writes only this declarative text. It writes no +markup and no script, and the numbers it could get wrong (counts, +probabilities, expectation values, a verdict) are not in its text at all. +The host draws each view from the engine's answer to its call; a view +whose argument is a number the model wrote is refused (§6, `J104`). A +renderer MUST NOT execute anything in an answer other than the engine calls +of §5, and MUST NOT draw a number in a view that the engine did not give. + +In scope: the text (§2), statements (§3), the views (§4) and engine calls +(§5), the diagnostics (§6), reading as the text streams (§7), carrying the +calls out and sealing them (§8), the token comparison (§9) and conformance +(§10). Out of scope: how a host lays a view out, and the program language, +which is OpenQASM 3 as the engine's reader reads it. + +## 2. The text + +An answer is UTF-8 text, read a line at a time. A line ends at LF; a CR +before the LF is not part of the line. The last line need not end with LF. +Each line is one of: + +- **blank** (only white space), or a **comment** (its first non-space + characters `//`): nothing; +- a **statement**, `name = Expression` (§3), on one line; +- a **program**: a line ```` ```qasm NAME ```` (or ```` ```openqasm NAME ````) + opens it, and the next line that is exactly ```` ``` ```` (white space + aside) closes it. The lines between are the program's text, OpenQASM 3, + read by the engine's reader when the block closes. Its `NAME` is a + statement's name; +- a **marker**: a line that begins `]]>wai:`. `]]>wai:receipt {…}` carries the + receipt of a stored answer (§8.3). After an answer's first marker only + markers follow. + +Any other line is not read (`J100`). A **name** is a letter or `_`, then +letters, digits or `_`. + +The answer's **source** is its text before its first marker, each line +ending in LF (a CR before it dropped, and an LF added to a last line that has +none). Its **digest** is the BLAKE3 of the source, as lowercase hex. + +## 3. Statements + +```text +statement = name ws "=" ws expr [ws "//" comment] +expr = view | call | name | literal | list | object +view = Name "(" [expr *("," expr)] ")" +call = "@" Name "(" [expr *("," expr)] ")" +list = "[" [expr *("," expr)] "]" +object = "{" [key ":" expr *("," key ":" expr)] "}" +key = json-string | name +literal = json-number | json-string | "true" | "false" | "null" +``` + +A statement's value is a view, an engine call, or a literal (a **value**: +a number, a string, a list or an object, which an engine call's parameter +may name). Arguments are positional, in the order §4 and §5 give; `null` +leaves out an optional one. There are no named arguments: `seed=7` in a +call is a line that does not parse (`J100`), and a reader SHOULD say that +arguments go by position. A value that several calls take, such as the +noise a sample and a verdict both run under, is declared once and passed by +its name. A name used before the statement that declares +it is a **forward reference**, and is allowed. A name declared again +replaces the earlier declaration (`J110`): this is how an answer is edited, +by writing the changed statement again. + +The statement named `root` is the answer's layout, `root = Answer([...])`, +and SHOULD come first, so that a renderer can lay the answer out before its +parts arrive. With no root, the views are drawn in the order they are +written (`J107`). + +## 4. Views + +A view's first argument is what it shows: an engine call of the kind the +table names, written in place (`Histogram(@Sample(bell, 1000))`) or named +(`s = @Sample(bell, 1000)` then `Histogram(s)`). + +| View | Arguments | Shows | +|---|---|---| +| `Answer` | `children`: a list of views (a program in the list is drawn as its circuit) | the root: the views, in order | +| `Text` | `text`: a string | a line of prose; a number in it is the model's, and is warned of (`J111`) | +| `Circuit` | `program` | the program's circuit, from the engine's layout, with the circuit in words | +| `Histogram` | `@Sample` | the counts of a sample | +| `Expectation` | `@Expect` | Pauli expectation values, each with its accuracy | +| `Amplitudes` | `@Amplitudes` | amplitudes of basis states, with each one's probability and phase | +| `Bloch` | `@State` | one qubit's Bloch vector and purity | +| `Verdict` | `@Check` | how far the engine checked the program against its intent | +| `Equivalence` | `@Equivalent` | whether two programs are the same circuit, and how the engine knows | +| `Critique` | `@Critique` | the program's measures and smells | +| `Estimate` | `@Estimate` | a fault-tolerant resource estimate and its frontier | +| `Compile` | `@Compile` | the program compiled through checked passes, and the bound on its distance | +| `Plan` | `@Plan` | the method a run would take, and why | +| `Receipt` | `@Seal` | a seal over the statements it names, checked where it is shown | + +## 5. Engine calls + +Each call is the engine's own operation on the program it names. A +parameter (shots, a seed, a qubit, a tolerance, ε, a budget) MAY be a literal +or the name of a value statement; a call's program is always a program's +name. + +| Call | Arguments | The engine's operation | +|---|---|---| +| `@Sample` | `program`, `shots`, `seed`? (1), `noise`? | a job of that many shots at that seed (under that noise, an object of channel probabilities as a job's `noise`), planned and run: its method, why, the request and the result | +| `@Expect` | `program`, `observables` (a list of Pauli strings, as `"Z0 Z1"`) | a job asking those expectation values | +| `@Amplitudes` | `program`, `states` (bit strings, qubit n−1 first) | a job asking those amplitudes | +| `@State` | `program`, `qubit` | the debugger stepped through every gate, before the final measurements: the qubit's Bloch vector, its length and its purity. A program that measures in the middle is refused | +| `@Check` | `program`, `intent`, `tolerance`?, `noise`? | the verdict a job with that intent (and that noise) gets (`wai_quantum::engine::verdict`): a distribution of its measured bits as an object (`{"00": 0.5, "11": 0.5}`), a program it must equal (its name, or `{"equivalent": name}`), or one whose state it must prepare (`{"prepares": name}`) | +| `@Equivalent` | `program`, `reference` | the equivalence ladder: the same circuit up to a global phase, and how that was decided | +| `@Critique` | `program` | the critique: measures and smells | +| `@Estimate` | `program`, `qubit`? (a physical qubit's name), `budget`? | the resource estimate | +| `@Compile` | `program`, `epsilon`? | the compile, with its certificate | +| `@Plan` | `program` | the plan of a run of 1024 shots | +| `@Seal` | `statements`: a list of programs, calls, or views of a call | a receipt over what they give (§8.2) | + +A call the engine refuses is a refusal with its codes (the job layer's +`J…`, a program's `Q…`), for that view alone; the rest of the answer is drawn. + +## 6. Diagnostics + +Every problem has a code that never changes meaning once released. A +program's problems are the reader's own codes (`Q…`), at the answer's line. +A statement's are the job layer's, `J100` to `J112`, each with what becomes +of the statement: + +| Code | Outcome | Meaning | +|---|---|---| +| `J100` | dropped | A line that is not a statement, a program, a comment or a marker. | +| `J101` | dropped | A view the layer does not have. | +| `J102` | dropped | An `@` call the engine does not have. | +| `J103` | dropped | A required argument missing. | +| `J104` | dropped | An argument of the wrong kind: a number where a view takes the engine's call (a number in a view comes from the engine), a list entry the list does not take (left out of the list), a root that is not `Answer`. | +| `J105` | warned | More arguments than the view or call takes: the extra ones are left out. | +| `J106` | dropped | A name no statement declares, or one whose statement was left out (a program the reader refused, say): the statement that names it is left out, and a list that names it is drawn without it. | +| `J107` | defaulted | No root: the views in the order they are written. | +| `J108` | dropped | The text ends inside a program or a statement. | +| `J109` | dropped | Statements that name each other in a cycle. | +| `J110` | warned | A name declared again: the later declaration stands. | +| `J111` | warned | A number in a view's text, which no engine call gave (one with a decimal point, a per cent, or two digits or more). | +| `J112` | warned | A statement the root does not reach: it is not drawn. | + +`wai_quantum::engine::refusal` holds this table with the job layer's others +(`wai/wai-quantum/docs/job-refusals.md`). An **error** is a diagnostic whose +outcome is `dropped` or `refused`; the rest are notes. A host that sends an +answer's problems back to the model so that it can repair them SHOULD send +the errors alone, each as `line N: message [CODE]`, and never the notes. + +## 7. Reading as the text streams + +A reader takes the text in pieces as they arrive, in any split, and reads +each line as it completes. After each piece it can give a **snapshot**: each +statement read so far, **ready** (read, checked, and everything it names is +ready), **pending** (it names a statement that has not arrived) or +**dropped** (with the diagnostic that says why). The root is ready as soon as +its line arrives, and the snapshot's order holds every view it lists, those +that have not arrived among them, so a renderer lays the answer out first. A +view that is ready can be drawn, and its call carried out, at once. + +When the text has ended, the reader gives the **answer**: a last line +without its LF is read; a program or a statement the text ends inside is +dropped (`J108`); a name still undeclared is `J106`; the root's reach is +checked (`J112`). A reader MUST give, for the same text, the same answer +however it was split: the same statements, statuses, order, diagnostics and +digest as reading the whole text at once. + +## 8. Carrying the calls out, and sealing them + +### 8.1 Values + +A view's value is its call's; a `Circuit`'s is the engine's layout of the +program and its description in words; a program's is `{"qasm", "digest"}`, +its text and the BLAKE3 of it. A value MUST hold nothing the engine did not +compute: a run's time is the host's, and a value MUST NOT carry it, so that +running the answer again gives the same bytes. + +### 8.2 The seal + +`@Seal([a, b, …])` signs, with the host's key, as the job layer's receipt +does (Ed25519 over the BLAKE3 of the RFC 8785 canonical text), the request + +```json +{ "format": "wai.quantum.answer/1", "answer": "", "sealed": ["a", "b"] } +``` + +and the result `{"statements": {"a": , "b": }}`, the names +those of the statements listed that are ready. Its value is in a job's form, +`{"request", "result"}` with the receipt in the result, so any surface that +checks a job's receipt checks it. A host with no key refuses a seal. + +### 8.3 A stored answer + +A stored answer is its source followed by a marker line carrying its seal's +receipt: + +```text +]]>wai:receipt {"statement":"seal","receipt":{"alg":"ed25519","canon":"rfc8785","key":"…","digest":"…","signature":"…"}} +``` + +To check it, read the answer, carry the sealed statements out again, build +the request and the result of §8.2, and check the receipt over them. The +engine is deterministic, so the same answer gives the same bytes on every +build; a changed statement changes the digest or the values, and the +receipt does not verify. A receipt shows that the key sealed these values for +this answer; it does not show who holds the key. + +## 9. What the layer costs in tokens + +The corpus is `quantum-conformance/eval/answers.json`: the fourteen tasks of +the model harness's task set (`eval/tasks.json`), each answer written four +ways by `wai-quantum eval corpus`, and held by a test to the task set, every +answer in the layer reading with no diagnostic and running to its intents: + +| Form | What it carries | +|---|---| +| `plain` | the program alone, in a fenced block | +| `views` | the layer whole: the program, its circuit, a sample when it measures, a verdict for each intent, a seal | +| `views_request` | the layer with the sample and the verdicts alone | +| `job` | the request an agent sends a run tool, as JSON with no white space | + +`views_request` and `job` carry the same program and the same request, so +they are the comparison of the layer against JSON; `plain` is what a model +writes when it is asked for the program alone. Noise that a sample and a +verdict both run under is declared once in the layer and passed by name +(§3), as the JSON states it once. `eval/token_cost.py` counted every answer +with the vocabularies of three open-weight models, read from their GGUF +files by a tokenizer tool (`eval/token-cost.json`). Totals over the +fourteen answers: + +| Vocabulary | `plain` | `views` | `views_request` | `job` | `views_request` / `job` | +|---|---:|---:|---:|---:|---:| +| A, 49 159 tokens | 841 | 2 365 | 1 967 | 1 492 | 1.318 | +| B, 248 320 tokens | 834 | 2 312 | 1 928 | 1 491 | 1.293 | +| C, 262 144 tokens | 835 | 2 282 | 1 940 | 1 483 | 1.308 | + +The means per answer are 60 tokens for the program alone, 163 to 169 for the +layer whole, 138 to 141 for the layer with the request alone and 106 for +the JSON. The three vocabularies, one a fifth the size of the other two, +agree to within 3 percent on every ratio. + +What the comparison shows: + +- The layer costs more than JSON for the same request: 29 to 32 percent more + in every vocabulary, 31 to 34 tokens an answer on the mean. The cost is + per statement: the root's line, the fence's name, and each view's name and + call, 24 to 28 tokens for one sample and one verdict. Those tokens carry + what the JSON does not: which views to draw, and a name for each. +- A second verdict is a second statement: 54 to 57 tokens more than the JSON + on the task with two intents, one of them a reference program in its own + fence. The noisy task, its noise named once, costs 36 to 41 more; written + into each call, it cost 67 to 72 more. +- The layer whole, with the circuit and the seal, costs 2.7 to 2.8 times the + program alone and 1.54 to 1.59 times the JSON; the circuit and the seal + add 24 to 28 tokens to an answer with one verdict, 30 to 34 to one with + two. + +The layer is not cheaper than JSON in tokens, and this draft does not claim +it is. What it buys for the tokens is §1's: the numbers an answer shows are +the engine's, never the model's, and the answer is drawn as it streams. + +### 9.1 How well a model writes it + +`wai-quantum eval run` asked one open-weight model each of the fourteen +tasks at temperature 0: a mixture of experts of 35 billion parameters with +3 billion active, quantised to 5 bits and served on the build host with its +reasoning turned off. It was asked once for the program alone and once for +an answer in the layer, each with no repair round and with one, which sends +the engine's errors back for a corrected answer. **Valid** is an answer the +engine reads with no error (in the layer, no error in the layer either); +**solved** is one whose program holds the task's intent by proof or +evidence. Joules are the model server's CPU energy as the kernel counts it +for its process; the GPU the model runs on is not counted, so they compare +these runs with each other and do not measure a task's whole cost. + +| Asked for | Repair rounds | Valid | Solved | Joules per solved task | +|---|---:|---:|---:|---:| +| the program | 0 | 12 | 11 | 4.4 | +| the program | 1 | 13 | 12 | 4.0 | +| the layer | 0 | 4 | 3 | 17.6 | +| the layer | 1 | 7 | 6 | 10.6 | + +This model writes the layer far less reliably than it writes a program. Its +errors are its own, not the reader's: a program in a fence with no name, a +view given an engine call where it takes a program (`Circuit(@Compile(p))`), +a name the root or a seal lists that no statement declares, a text or a +circuit listed in a seal, arguments written as `seed=7`, and the prompt's +example copied with its program's name. An answer that is not valid still +draws, since a list leaves out what falls (§7) and only the statements in +error are missing, unless its program is lost, as in the four first answers +whose fence had no name or that ended inside the program. One repair round, with the codes and the lines, corrects +three of the ten. The prompt was revised after the first run on these tasks +to give each argument's kind and to state the rules those errors broke; the +first run scored the same, 4 valid and 3 solved with no repair and 7 and 6 +with one, and the errors moved rather than went. The prompt and the reader +are `answer::prompt()` and §6; whether a larger model or a model tuned on +the layer does better is not measured here, and a report ranks nothing. + +## 10. Conformance + +`quantum-conformance/answers/` holds answers, each with the codes of its +errors and warnings in line order, its root, its order, what its views show +when run, and, for stored answers, whether the receipt verifies when the +answer is run again. Every code of §6 has a case. A conforming reader MUST +give each case's codes, root and order, and MUST give the same answer +however the text is split (§7). A conforming host MUST give each view what +its manifest says when run, and MUST verify the stored cases as they say. +The reference implementation passes the set natively, under wasm32-wasip2 +and in the browser (wai-web's `engine_answer_parse` and +`engine_answer_verify`); its stored case was written natively and verifies +in each. + +## 11. Implementation + +- `wai_quantum::engine::answer`: the tables of §4 and §5 (`VIEWS`, + `CALLS`), the reader (`parse`, and `Stream` with `push`, `snapshot` and + `finish`), and `eval` (each call carried out, the seal, the marker, and a + stored answer's check). `answer::prompt()` describes the layer to a model + from the same tables, with its rules and a whole answer (`answer::EXAMPLE`) + that reads with no diagnostic. +- wai-web (feature `quantum_engine`): `AnswerStream`, `engine_answer_parse`, + `engine_answer_prompt`, `engine_answer_verify`, `engine_answer_marker`, and + on the page's engine `answer_value` and `answer_evaluate`. +- Quantum Studio draws each statement as it arrives, its calls carried out by + the engine in the page; the desktop app answers the same calls natively. +- `wai-quantum eval run --format views` asks a model to answer in the layer + and scores what it writes (§9.1). diff --git a/wai/quantum-conformance/README.md b/wai/quantum-conformance/README.md index cc0b00706..b28ba1d0d 100644 --- a/wai/quantum-conformance/README.md +++ b/wai/quantum-conformance/README.md @@ -17,7 +17,10 @@ Four trees, each a pinned corpus: | `accept/` + `accept/accept.json` | both | payloads a conforming reader accepts, at the edges of the same rules, each with how it reads: capability, `n_qubits`, op count, circuit hash and measurement (extension §2) | The OpenQASM 3 reader's corpus, programs with what the reader must do with -each, is `qasm3/`, with a README of its own. +each, is `qasm3/`, with a README of its own. The error-correction text formats +(noisy Clifford circuits and detector error models, read and written back byte +for byte, with what each must count and what must be refused) are `qec-text/`, +with a README of its own. An entry with no `capability` is a `wai.quantum.circuit` vector. The trees are the one reference all three surfaces are checked against, rather than being checked diff --git a/wai/quantum-conformance/answers/README.md b/wai/quantum-conformance/answers/README.md new file mode 100644 index 000000000..442a55bda --- /dev/null +++ b/wai/quantum-conformance/answers/README.md @@ -0,0 +1,51 @@ +# Answer conformance + +Answers in the answer view layer ([quantum-answers.md](../../extensions/quantum-answers.md), +`wai_quantum::engine::answer`), each with what the layer must make of it. + +| Path | What | +|---|---| +| `cases/*.wqa` | The answers: a program, a sample, a verdict and a seal; an intent named by a reference program; the equivalence ladder; an intent that fails; expectation values, amplitudes and a Bloch vector; an engine call named apart from its view; no root; and one case for each code of the layer (`J100` to `J112`) and for a program the reader refuses (`Q103`). `bell-crlf` is `bell` with CRLF line ends. `bell-stored` is `bell` with the receipt marker its seal gives under the key whose seed is 32 bytes of 3, and `bell-stored-changed` the same with one sample's shots changed. | +| `manifest.json` | Each case: its file, what it covers, and what it must give (the manifest's `expect` says what each field means). | +| `bundle.json` | The manifest's cases with every answer inline, for builds that read no files. | + +A case is held to the codes of its errors (the diagnostics whose outcome is +`dropped` or `refused`, the ones a model is sent back) and of its warnings, in +the order of their lines; to its root; and to the order its views are drawn +in. A case with `run` has its views carried out on the engine and held to what +they show: a sample's total and its outcomes, a verdict's level and whether it +holds, an equivalence's decision, expectation values, a Bloch vector, the +names a seal covers and its receipt. A stored case's receipt is checked by +running the answer again: the engine is deterministic, so the same answer +gives the same sealed bytes on every build, and a changed one does not verify. + +## Native + +```sh +cd wai/wai-quantum +cargo test --release --features full --test answer_conformance +``` + +Two tests: every case against its manifest, and the bundle (and the stored +cases) held equal to the files. After changing a case or the manifest, write +them: + +```sh +WAI_ANSWER_BUNDLE_WRITE=1 cargo test --release --features full --test answer_conformance the_bundle_is_the_corpus +``` + +## WASI (the edge) + +```sh +CARGO_TARGET_WASM32_WASIP2_RUNNER=wasmtime \ + cargo test --target wasm32-wasip2 --features full --test answer_conformance +``` + +## Browser + +wai-web's wasm32 tests (`--features quantum_engine`, under +`wasm-bindgen-test-runner`) read every case of the bundle with +`engine_answer_parse` and verify the stored cases with `engine_answer_verify`. + +CI: `wai-quantum-verdict.yml` (native and wasm32-wasip2) and +`wai-web-wasm.yml` (the browser build). diff --git a/wai/quantum-conformance/answers/bundle.json b/wai/quantum-conformance/answers/bundle.json new file mode 100644 index 000000000..4d668dbac --- /dev/null +++ b/wai/quantum-conformance/answers/bundle.json @@ -0,0 +1,462 @@ +{ + "format": "wai.quantum-answer-conformance-bundle/1", + "about": "manifest.json's cases with every answer inline, for builds that read no files (the browser, the edge); a test holds it equal to the files", + "cases": [ + { + "name": "bell", + "file": "cases/bell.wqa", + "covers": [ + "a well-formed answer: a program, a sample, a verdict and a seal" + ], + "expect": { + "errors": [], + "warnings": [], + "root": "root", + "order": [ + "bell", + "counts", + "check", + "seal" + ], + "run": { + "counts": { + "total": 1000, + "outcomes": [ + "00", + "11" + ] + }, + "check": { + "level": "proof", + "holds": true + }, + "seal": { + "sealed": [ + "bell", + "counts", + "check" + ] + } + } + }, + "answer": "root = Answer([bell, counts, check, seal])\n```qasm bell\nOPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n```\ncounts = Histogram(@Sample(bell, 1000))\ncheck = Verdict(@Check(bell, {\"00\": 0.5, \"11\": 0.5}))\nseal = Receipt(@Seal([bell, counts, check]))\n" + }, + { + "name": "bell-crlf", + "file": "cases/bell-crlf.wqa", + "covers": [ + "CRLF line ends read as LF: the same answer and the same digest" + ], + "expect": { + "errors": [], + "warnings": [], + "root": "root", + "order": [ + "bell", + "counts", + "check", + "seal" + ], + "same_as": "bell" + }, + "answer": "root = Answer([bell, counts, check, seal])\r\n```qasm bell\r\nOPENQASM 3.0;\r\nqubit[2] q;\r\nbit[2] c;\r\nh q[0];\r\ncx q[0], q[1];\r\nc = measure q;\r\n```\r\ncounts = Histogram(@Sample(bell, 1000))\r\ncheck = Verdict(@Check(bell, {\"00\": 0.5, \"11\": 0.5}))\r\nseal = Receipt(@Seal([bell, counts, check]))\r\n" + }, + { + "name": "ghz-prepares", + "file": "cases/ghz-prepares.wqa", + "covers": [ + "an intent named by a reference program whose state it must prepare" + ], + "expect": { + "errors": [], + "warnings": [], + "root": "root", + "order": [ + "ghz", + "check" + ], + "run": { + "check": { + "level": "proof", + "holds": true + } + } + }, + "answer": "root = Answer([ghz, check])\n```qasm ghz\nOPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[4] q;\nbit[4] c;\nh q[0];\ncx q[0], q[1];\ncx q[0], q[2];\ncx q[1], q[3];\nc = measure q;\n```\n```qasm chain\nOPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[4] q;\nbit[4] c;\nh q[0];\ncx q[0], q[1];\ncx q[1], q[2];\ncx q[2], q[3];\nc = measure q;\n```\ncheck = Verdict(@Check(ghz, {\"prepares\": chain}))\n" + }, + { + "name": "grover-equivalent", + "file": "cases/grover-equivalent.wqa", + "covers": [ + "the equivalence ladder, and a distribution of one outcome" + ], + "expect": { + "errors": [], + "warnings": [], + "root": "root", + "order": [ + "same", + "check" + ], + "run": { + "same": { + "verdict": "equivalent" + }, + "check": { + "level": "proof", + "holds": true + } + } + }, + "answer": "root = Answer([same, check])\n```qasm grover\nOPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q;\ncz q[0], q[1];\nh q;\nx q;\ncz q[0], q[1];\nx q;\nh q;\nc = measure q;\n```\n```qasm grover_cx\nOPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q;\nh q[1];\ncx q[0], q[1];\nh q[1];\nh q;\nx q;\nh q[1];\ncx q[0], q[1];\nh q[1];\nx q;\nh q;\nc = measure q;\n```\nsame = Equivalence(@Equivalent(grover, grover_cx))\ncheck = Verdict(@Check(grover, {\"11\": 1.0}))\n" + }, + { + "name": "wrong-distribution", + "file": "cases/wrong-distribution.wqa", + "covers": [ + "an intent that fails, decided by proof" + ], + "expect": { + "errors": [], + "warnings": [], + "root": "root", + "order": [ + "check" + ], + "run": { + "check": { + "level": "proof", + "holds": false + } + } + }, + "answer": "root = Answer([check])\n```qasm bell\nOPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n```\ncheck = Verdict(@Check(bell, {\"00\": 1.0}))\n" + }, + { + "name": "state-views", + "file": "cases/state-views.wqa", + "covers": [ + "a circuit, expectation values, amplitudes and a Bloch vector, each the engine's" + ], + "expect": { + "errors": [], + "warnings": [], + "root": "root", + "order": [ + "circ", + "z", + "amps", + "plus" + ], + "run": { + "z": { + "expectations": [ + 1.0, + 1.0 + ] + }, + "plus": { + "bloch_x": 1.0 + } + } + }, + "answer": "root = Answer([circ, z, amps, plus])\n```qasm p\nOPENQASM 3.0;\nqubit[2] q;\nh q[0];\ncx q[0], q[1];\n```\n```qasm one\nOPENQASM 3.0;\nqubit[1] q;\nh q[0];\n```\ncirc = Circuit(p)\nz = Expectation(@Expect(p, [\"Z0 Z1\", \"X0 X1\"]))\namps = Amplitudes(@Amplitudes(p, [\"00\", \"11\"]))\nplus = Bloch(@State(one, 0))\n" + }, + { + "name": "calls-named-apart", + "file": "cases/calls-named-apart.wqa", + "covers": [ + "an engine call as a statement of its own, named by the view; a forward reference; a value no view uses" + ], + "expect": { + "errors": [], + "warnings": [ + "J112" + ], + "root": "root", + "order": [ + "counts", + "check" + ], + "run": { + "counts": { + "total": 200, + "outcomes": [ + "00", + "11" + ] + } + } + }, + "answer": "root = Answer([counts, check])\ns = @Sample(bell, 200, 7)\ncounts = Histogram(s)\n```qasm bell\nOPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n```\nwant = {\"00\": 0.5, \"11\": 0.5}\ncheck = Verdict(@Check(bell, {\"00\": 0.5, \"11\": 0.5}, 1e-9))\n" + }, + { + "name": "no-root", + "file": "cases/no-root.wqa", + "covers": [ + "no root: the views in the order written" + ], + "expect": { + "errors": [], + "warnings": [ + "J107" + ], + "root": null, + "order": [ + "circ", + "counts" + ] + }, + "answer": "```qasm bell\nOPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n```\ncirc = Circuit(bell)\ncounts = Histogram(@Sample(bell, 100))\n" + }, + { + "name": "number-in-a-view", + "file": "cases/number-in-a-view.wqa", + "covers": [ + "a number where a view takes the engine's call" + ], + "expect": { + "errors": [ + "J106", + "J104" + ], + "warnings": [ + "J112" + ], + "root": "root", + "order": [] + }, + "answer": "root = Answer([counts])\n```qasm bell\nOPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n```\ncounts = Histogram(1000)\n" + }, + { + "name": "unknown-view-and-call", + "file": "cases/unknown-view-and-call.wqa", + "covers": [ + "a view and engine calls the layer does not have; the root listing an engine call, which it does not take, leaves it out" + ], + "expect": { + "errors": [ + "J104", + "J106", + "J101", + "J102", + "J102" + ], + "warnings": [ + "J112" + ], + "root": "root", + "order": [] + }, + "answer": "root = Answer([a, b, c])\n```qasm bell\nOPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n```\na = Barchart(@Sample(bell, 10))\nb = Histogram(@Run(bell, 10))\nc = @Simulate(bell)\n" + }, + { + "name": "missing-and-extra-arguments", + "file": "cases/missing-and-extra-arguments.wqa", + "covers": [ + "an argument missing, and one too many" + ], + "expect": { + "errors": [ + "J106", + "J103" + ], + "warnings": [ + "J105" + ], + "root": "root", + "order": [ + "b" + ] + }, + "answer": "root = Answer([a, b])\n```qasm bell\nOPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n```\na = Histogram(@Sample(bell))\nb = Circuit(bell, bell)\n" + }, + { + "name": "a-program-that-does-not-read", + "file": "cases/a-program-that-does-not-read.wqa", + "covers": [ + "the reader's refusal keeps its code; what names the program falls with it; a name nothing declares" + ], + "expect": { + "errors": [ + "J106", + "Q103", + "J106", + "J106" + ], + "warnings": [], + "root": "root", + "order": [] + }, + "answer": "root = Answer([counts, circ])\n```qasm bad\nOPENQASM 3.0;\nqubit[1] q;\nfoo q[0];\n```\ncounts = Histogram(@Sample(bad, 10))\ncirc = Circuit(nowhere)\n" + }, + { + "name": "truncated-program", + "file": "cases/truncated-program.wqa", + "covers": [ + "the text ends inside a program" + ], + "expect": { + "errors": [ + "J106", + "J106", + "J108" + ], + "warnings": [], + "root": "root", + "order": [] + }, + "answer": "root = Answer([bell, counts])\ncounts = Histogram(@Sample(bell, 10))\n```qasm bell\nOPENQASM 3.0;\nqubit[2] q;\nh q[0];\n" + }, + { + "name": "truncated-statement", + "file": "cases/truncated-statement.wqa", + "covers": [ + "the text ends inside a statement" + ], + "expect": { + "errors": [ + "J106", + "J108" + ], + "warnings": [], + "root": "root", + "order": [ + "bell" + ] + }, + "answer": "root = Answer([bell, counts])\n```qasm bell\nOPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n```\ncounts = Histogram(@Sample(bell, 10" + }, + { + "name": "cycle", + "file": "cases/cycle.wqa", + "covers": [ + "statements that name each other" + ], + "expect": { + "errors": [ + "J106", + "J109", + "J109" + ], + "warnings": [ + "J112" + ], + "root": "root", + "order": [] + }, + "answer": "root = Answer([a])\n```qasm bell\nOPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n```\na = Receipt(@Seal([b]))\nb = @Seal([a])\n" + }, + { + "name": "edit-by-redeclaring", + "file": "cases/edit-by-redeclaring.wqa", + "covers": [ + "a later declaration stands: an edit" + ], + "expect": { + "errors": [], + "warnings": [ + "J110" + ], + "root": "root", + "order": [ + "counts" + ], + "run": { + "counts": { + "total": 500, + "outcomes": [ + "00", + "11" + ] + } + } + }, + "answer": "root = Answer([counts])\n```qasm bell\nOPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n```\ncounts = Histogram(@Sample(bell, 10))\ncounts = Histogram(@Sample(bell, 500))\n" + }, + { + "name": "prose-and-unused", + "file": "cases/prose-and-unused.wqa", + "covers": [ + "a number in prose, and a view the root does not reach" + ], + "expect": { + "errors": [], + "warnings": [ + "J111", + "J112" + ], + "root": "root", + "order": [ + "note", + "circ" + ] + }, + "answer": "root = Answer([note, circ])\n```qasm bell\nOPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n```\nnote = Text(\"about 50% read 00, the rest 11\")\ncirc = Circuit(bell)\nextra = Plan(@Plan(bell))\n" + }, + { + "name": "lines-that-are-not-statements", + "file": "cases/lines-that-are-not-statements.wqa", + "covers": [ + "prose, a fence that opens no program, and a marker this layer does not read" + ], + "expect": { + "errors": [ + "J100", + "J100", + "J100" + ], + "warnings": [], + "root": "root", + "order": [ + "circ" + ] + }, + "answer": "root = Answer([circ])\n```qasm bell\nOPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n```\ncirc = Circuit(bell)\nThe program above makes a Bell pair.\n```python\n]]>wai:summary {}\n" + }, + { + "name": "bell-stored", + "file": "cases/bell-stored.wqa", + "covers": [ + "a stored answer with its receipt marker: run again, the receipt verifies" + ], + "expect": { + "errors": [], + "warnings": [], + "root": "root", + "order": [ + "bell", + "counts", + "check", + "seal" + ], + "stored": { + "valid": true + } + }, + "answer": "root = Answer([bell, counts, check, seal])\n```qasm bell\nOPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n```\ncounts = Histogram(@Sample(bell, 1000))\ncheck = Verdict(@Check(bell, {\"00\": 0.5, \"11\": 0.5}))\nseal = Receipt(@Seal([bell, counts, check]))\n]]>wai:receipt {\"statement\":\"seal\",\"receipt\":{\"alg\":\"ed25519\",\"canon\":\"rfc8785\",\"key\":\"ed4928c628d1c2c6eae90338905995612959273a5c63f93636c14614ac8737d1\",\"digest\":\"172dd72f18d77dc7ef99525fc61859062015f4a44a34d8ae5c76a7ba4a1b7c6d\",\"signature\":\"4e0288de0313b8f76b03bcbcfedf01989833fd63fd31e7bf0d981b75541c0362b92af555b161a552e6a4242e5a1793d9f90705ef8297db924a3abd577e6e8907\"}}\n" + }, + { + "name": "bell-stored-changed", + "file": "cases/bell-stored-changed.wqa", + "covers": [ + "the stored answer with a statement changed: the receipt does not verify" + ], + "expect": { + "errors": [], + "warnings": [], + "root": "root", + "order": [ + "bell", + "counts", + "check", + "seal" + ], + "stored": { + "valid": false + } + }, + "answer": "root = Answer([bell, counts, check, seal])\n```qasm bell\nOPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n```\ncounts = Histogram(@Sample(bell, 999))\ncheck = Verdict(@Check(bell, {\"00\": 0.5, \"11\": 0.5}))\nseal = Receipt(@Seal([bell, counts, check]))\n]]>wai:receipt {\"statement\":\"seal\",\"receipt\":{\"alg\":\"ed25519\",\"canon\":\"rfc8785\",\"key\":\"ed4928c628d1c2c6eae90338905995612959273a5c63f93636c14614ac8737d1\",\"digest\":\"172dd72f18d77dc7ef99525fc61859062015f4a44a34d8ae5c76a7ba4a1b7c6d\",\"signature\":\"4e0288de0313b8f76b03bcbcfedf01989833fd63fd31e7bf0d981b75541c0362b92af555b161a552e6a4242e5a1793d9f90705ef8297db924a3abd577e6e8907\"}}\n" + } + ] +} diff --git a/wai/quantum-conformance/answers/cases/a-program-that-does-not-read.wqa b/wai/quantum-conformance/answers/cases/a-program-that-does-not-read.wqa new file mode 100644 index 000000000..1e6ef8866 --- /dev/null +++ b/wai/quantum-conformance/answers/cases/a-program-that-does-not-read.wqa @@ -0,0 +1,8 @@ +root = Answer([counts, circ]) +```qasm bad +OPENQASM 3.0; +qubit[1] q; +foo q[0]; +``` +counts = Histogram(@Sample(bad, 10)) +circ = Circuit(nowhere) diff --git a/wai/quantum-conformance/answers/cases/bell-crlf.wqa b/wai/quantum-conformance/answers/cases/bell-crlf.wqa new file mode 100644 index 000000000..79f19b1c9 --- /dev/null +++ b/wai/quantum-conformance/answers/cases/bell-crlf.wqa @@ -0,0 +1,12 @@ +root = Answer([bell, counts, check, seal]) +```qasm bell +OPENQASM 3.0; +qubit[2] q; +bit[2] c; +h q[0]; +cx q[0], q[1]; +c = measure q; +``` +counts = Histogram(@Sample(bell, 1000)) +check = Verdict(@Check(bell, {"00": 0.5, "11": 0.5})) +seal = Receipt(@Seal([bell, counts, check])) diff --git a/wai/quantum-conformance/answers/cases/bell-stored-changed.wqa b/wai/quantum-conformance/answers/cases/bell-stored-changed.wqa new file mode 100644 index 000000000..4491f6b87 --- /dev/null +++ b/wai/quantum-conformance/answers/cases/bell-stored-changed.wqa @@ -0,0 +1,13 @@ +root = Answer([bell, counts, check, seal]) +```qasm bell +OPENQASM 3.0; +qubit[2] q; +bit[2] c; +h q[0]; +cx q[0], q[1]; +c = measure q; +``` +counts = Histogram(@Sample(bell, 999)) +check = Verdict(@Check(bell, {"00": 0.5, "11": 0.5})) +seal = Receipt(@Seal([bell, counts, check])) +]]>wai:receipt {"statement":"seal","receipt":{"alg":"ed25519","canon":"rfc8785","key":"ed4928c628d1c2c6eae90338905995612959273a5c63f93636c14614ac8737d1","digest":"172dd72f18d77dc7ef99525fc61859062015f4a44a34d8ae5c76a7ba4a1b7c6d","signature":"4e0288de0313b8f76b03bcbcfedf01989833fd63fd31e7bf0d981b75541c0362b92af555b161a552e6a4242e5a1793d9f90705ef8297db924a3abd577e6e8907"}} diff --git a/wai/quantum-conformance/answers/cases/bell-stored.wqa b/wai/quantum-conformance/answers/cases/bell-stored.wqa new file mode 100644 index 000000000..32b4af237 --- /dev/null +++ b/wai/quantum-conformance/answers/cases/bell-stored.wqa @@ -0,0 +1,13 @@ +root = Answer([bell, counts, check, seal]) +```qasm bell +OPENQASM 3.0; +qubit[2] q; +bit[2] c; +h q[0]; +cx q[0], q[1]; +c = measure q; +``` +counts = Histogram(@Sample(bell, 1000)) +check = Verdict(@Check(bell, {"00": 0.5, "11": 0.5})) +seal = Receipt(@Seal([bell, counts, check])) +]]>wai:receipt {"statement":"seal","receipt":{"alg":"ed25519","canon":"rfc8785","key":"ed4928c628d1c2c6eae90338905995612959273a5c63f93636c14614ac8737d1","digest":"172dd72f18d77dc7ef99525fc61859062015f4a44a34d8ae5c76a7ba4a1b7c6d","signature":"4e0288de0313b8f76b03bcbcfedf01989833fd63fd31e7bf0d981b75541c0362b92af555b161a552e6a4242e5a1793d9f90705ef8297db924a3abd577e6e8907"}} diff --git a/wai/quantum-conformance/answers/cases/bell.wqa b/wai/quantum-conformance/answers/cases/bell.wqa new file mode 100644 index 000000000..dcfe8dedb --- /dev/null +++ b/wai/quantum-conformance/answers/cases/bell.wqa @@ -0,0 +1,12 @@ +root = Answer([bell, counts, check, seal]) +```qasm bell +OPENQASM 3.0; +qubit[2] q; +bit[2] c; +h q[0]; +cx q[0], q[1]; +c = measure q; +``` +counts = Histogram(@Sample(bell, 1000)) +check = Verdict(@Check(bell, {"00": 0.5, "11": 0.5})) +seal = Receipt(@Seal([bell, counts, check])) diff --git a/wai/quantum-conformance/answers/cases/calls-named-apart.wqa b/wai/quantum-conformance/answers/cases/calls-named-apart.wqa new file mode 100644 index 000000000..a5ba64c8d --- /dev/null +++ b/wai/quantum-conformance/answers/cases/calls-named-apart.wqa @@ -0,0 +1,13 @@ +root = Answer([counts, check]) +s = @Sample(bell, 200, 7) +counts = Histogram(s) +```qasm bell +OPENQASM 3.0; +qubit[2] q; +bit[2] c; +h q[0]; +cx q[0], q[1]; +c = measure q; +``` +want = {"00": 0.5, "11": 0.5} +check = Verdict(@Check(bell, {"00": 0.5, "11": 0.5}, 1e-9)) diff --git a/wai/quantum-conformance/answers/cases/cycle.wqa b/wai/quantum-conformance/answers/cases/cycle.wqa new file mode 100644 index 000000000..b32d2fd64 --- /dev/null +++ b/wai/quantum-conformance/answers/cases/cycle.wqa @@ -0,0 +1,11 @@ +root = Answer([a]) +```qasm bell +OPENQASM 3.0; +qubit[2] q; +bit[2] c; +h q[0]; +cx q[0], q[1]; +c = measure q; +``` +a = Receipt(@Seal([b])) +b = @Seal([a]) diff --git a/wai/quantum-conformance/answers/cases/edit-by-redeclaring.wqa b/wai/quantum-conformance/answers/cases/edit-by-redeclaring.wqa new file mode 100644 index 000000000..3309528a6 --- /dev/null +++ b/wai/quantum-conformance/answers/cases/edit-by-redeclaring.wqa @@ -0,0 +1,11 @@ +root = Answer([counts]) +```qasm bell +OPENQASM 3.0; +qubit[2] q; +bit[2] c; +h q[0]; +cx q[0], q[1]; +c = measure q; +``` +counts = Histogram(@Sample(bell, 10)) +counts = Histogram(@Sample(bell, 500)) diff --git a/wai/quantum-conformance/answers/cases/ghz-prepares.wqa b/wai/quantum-conformance/answers/cases/ghz-prepares.wqa new file mode 100644 index 000000000..2ea6f609c --- /dev/null +++ b/wai/quantum-conformance/answers/cases/ghz-prepares.wqa @@ -0,0 +1,24 @@ +root = Answer([ghz, check]) +```qasm ghz +OPENQASM 3.0; +include "stdgates.inc"; +qubit[4] q; +bit[4] c; +h q[0]; +cx q[0], q[1]; +cx q[0], q[2]; +cx q[1], q[3]; +c = measure q; +``` +```qasm chain +OPENQASM 3.0; +include "stdgates.inc"; +qubit[4] q; +bit[4] c; +h q[0]; +cx q[0], q[1]; +cx q[1], q[2]; +cx q[2], q[3]; +c = measure q; +``` +check = Verdict(@Check(ghz, {"prepares": chain})) diff --git a/wai/quantum-conformance/answers/cases/grover-equivalent.wqa b/wai/quantum-conformance/answers/cases/grover-equivalent.wqa new file mode 100644 index 000000000..5fb486724 --- /dev/null +++ b/wai/quantum-conformance/answers/cases/grover-equivalent.wqa @@ -0,0 +1,35 @@ +root = Answer([same, check]) +```qasm grover +OPENQASM 3.0; +include "stdgates.inc"; +qubit[2] q; +bit[2] c; +h q; +cz q[0], q[1]; +h q; +x q; +cz q[0], q[1]; +x q; +h q; +c = measure q; +``` +```qasm grover_cx +OPENQASM 3.0; +include "stdgates.inc"; +qubit[2] q; +bit[2] c; +h q; +h q[1]; +cx q[0], q[1]; +h q[1]; +h q; +x q; +h q[1]; +cx q[0], q[1]; +h q[1]; +x q; +h q; +c = measure q; +``` +same = Equivalence(@Equivalent(grover, grover_cx)) +check = Verdict(@Check(grover, {"11": 1.0})) diff --git a/wai/quantum-conformance/answers/cases/lines-that-are-not-statements.wqa b/wai/quantum-conformance/answers/cases/lines-that-are-not-statements.wqa new file mode 100644 index 000000000..6b595f526 --- /dev/null +++ b/wai/quantum-conformance/answers/cases/lines-that-are-not-statements.wqa @@ -0,0 +1,13 @@ +root = Answer([circ]) +```qasm bell +OPENQASM 3.0; +qubit[2] q; +bit[2] c; +h q[0]; +cx q[0], q[1]; +c = measure q; +``` +circ = Circuit(bell) +The program above makes a Bell pair. +```python +]]>wai:summary {} diff --git a/wai/quantum-conformance/answers/cases/missing-and-extra-arguments.wqa b/wai/quantum-conformance/answers/cases/missing-and-extra-arguments.wqa new file mode 100644 index 000000000..d6f4a2c01 --- /dev/null +++ b/wai/quantum-conformance/answers/cases/missing-and-extra-arguments.wqa @@ -0,0 +1,11 @@ +root = Answer([a, b]) +```qasm bell +OPENQASM 3.0; +qubit[2] q; +bit[2] c; +h q[0]; +cx q[0], q[1]; +c = measure q; +``` +a = Histogram(@Sample(bell)) +b = Circuit(bell, bell) diff --git a/wai/quantum-conformance/answers/cases/no-root.wqa b/wai/quantum-conformance/answers/cases/no-root.wqa new file mode 100644 index 000000000..5de68781f --- /dev/null +++ b/wai/quantum-conformance/answers/cases/no-root.wqa @@ -0,0 +1,10 @@ +```qasm bell +OPENQASM 3.0; +qubit[2] q; +bit[2] c; +h q[0]; +cx q[0], q[1]; +c = measure q; +``` +circ = Circuit(bell) +counts = Histogram(@Sample(bell, 100)) diff --git a/wai/quantum-conformance/answers/cases/number-in-a-view.wqa b/wai/quantum-conformance/answers/cases/number-in-a-view.wqa new file mode 100644 index 000000000..ddeb03292 --- /dev/null +++ b/wai/quantum-conformance/answers/cases/number-in-a-view.wqa @@ -0,0 +1,10 @@ +root = Answer([counts]) +```qasm bell +OPENQASM 3.0; +qubit[2] q; +bit[2] c; +h q[0]; +cx q[0], q[1]; +c = measure q; +``` +counts = Histogram(1000) diff --git a/wai/quantum-conformance/answers/cases/prose-and-unused.wqa b/wai/quantum-conformance/answers/cases/prose-and-unused.wqa new file mode 100644 index 000000000..d09ecca40 --- /dev/null +++ b/wai/quantum-conformance/answers/cases/prose-and-unused.wqa @@ -0,0 +1,12 @@ +root = Answer([note, circ]) +```qasm bell +OPENQASM 3.0; +qubit[2] q; +bit[2] c; +h q[0]; +cx q[0], q[1]; +c = measure q; +``` +note = Text("about 50% read 00, the rest 11") +circ = Circuit(bell) +extra = Plan(@Plan(bell)) diff --git a/wai/quantum-conformance/answers/cases/state-views.wqa b/wai/quantum-conformance/answers/cases/state-views.wqa new file mode 100644 index 000000000..c972dc6f9 --- /dev/null +++ b/wai/quantum-conformance/answers/cases/state-views.wqa @@ -0,0 +1,16 @@ +root = Answer([circ, z, amps, plus]) +```qasm p +OPENQASM 3.0; +qubit[2] q; +h q[0]; +cx q[0], q[1]; +``` +```qasm one +OPENQASM 3.0; +qubit[1] q; +h q[0]; +``` +circ = Circuit(p) +z = Expectation(@Expect(p, ["Z0 Z1", "X0 X1"])) +amps = Amplitudes(@Amplitudes(p, ["00", "11"])) +plus = Bloch(@State(one, 0)) diff --git a/wai/quantum-conformance/answers/cases/truncated-program.wqa b/wai/quantum-conformance/answers/cases/truncated-program.wqa new file mode 100644 index 000000000..58972c1e4 --- /dev/null +++ b/wai/quantum-conformance/answers/cases/truncated-program.wqa @@ -0,0 +1,6 @@ +root = Answer([bell, counts]) +counts = Histogram(@Sample(bell, 10)) +```qasm bell +OPENQASM 3.0; +qubit[2] q; +h q[0]; diff --git a/wai/quantum-conformance/answers/cases/truncated-statement.wqa b/wai/quantum-conformance/answers/cases/truncated-statement.wqa new file mode 100644 index 000000000..2b2927484 --- /dev/null +++ b/wai/quantum-conformance/answers/cases/truncated-statement.wqa @@ -0,0 +1,10 @@ +root = Answer([bell, counts]) +```qasm bell +OPENQASM 3.0; +qubit[2] q; +bit[2] c; +h q[0]; +cx q[0], q[1]; +c = measure q; +``` +counts = Histogram(@Sample(bell, 10 \ No newline at end of file diff --git a/wai/quantum-conformance/answers/cases/unknown-view-and-call.wqa b/wai/quantum-conformance/answers/cases/unknown-view-and-call.wqa new file mode 100644 index 000000000..99f4eb5ac --- /dev/null +++ b/wai/quantum-conformance/answers/cases/unknown-view-and-call.wqa @@ -0,0 +1,12 @@ +root = Answer([a, b, c]) +```qasm bell +OPENQASM 3.0; +qubit[2] q; +bit[2] c; +h q[0]; +cx q[0], q[1]; +c = measure q; +``` +a = Barchart(@Sample(bell, 10)) +b = Histogram(@Run(bell, 10)) +c = @Simulate(bell) diff --git a/wai/quantum-conformance/answers/cases/wrong-distribution.wqa b/wai/quantum-conformance/answers/cases/wrong-distribution.wqa new file mode 100644 index 000000000..2765714d2 --- /dev/null +++ b/wai/quantum-conformance/answers/cases/wrong-distribution.wqa @@ -0,0 +1,10 @@ +root = Answer([check]) +```qasm bell +OPENQASM 3.0; +qubit[2] q; +bit[2] c; +h q[0]; +cx q[0], q[1]; +c = measure q; +``` +check = Verdict(@Check(bell, {"00": 1.0})) diff --git a/wai/quantum-conformance/answers/manifest.json b/wai/quantum-conformance/answers/manifest.json new file mode 100644 index 000000000..c97052bca --- /dev/null +++ b/wai/quantum-conformance/answers/manifest.json @@ -0,0 +1,451 @@ +{ + "format": "wai.quantum-answer-conformance/1", + "about": "Answers in the answer view layer (wai/extensions/quantum-answers.md), each with what the layer must make of it: the codes of its errors (the problems a model is sent back) and of its warnings, in the order of their lines; its root; the order its views are drawn in; and, run on the engine, what its views show. A stored answer's receipt is checked by running the answer again. bell-stored is bell with the marker its seal gives under the key whose seed is 32 bytes of 3, and bell-stored-changed the same with one sample's shots changed; a test writes both from bell.", + "expect": { + "errors": "the codes of the diagnostics whose outcome is dropped or refused, in line order", + "warnings": "the codes of the others (defaulted, warned, or a program's warning), in line order", + "root": "the root statement's name, or null", + "order": "the statements drawn, in order", + "run": "for a statement: total (the counts' sum), outcomes (the only outcomes seen), level and holds (a verdict), verdict (an equivalence), expectations (each value), bloch_x, sealed (the names a seal covers)", + "same_as": "the case whose answer this one must equal, its digest included", + "stored": "whether the receipt its marker carries verifies when the answer is run again" + }, + "cases": [ + { + "name": "bell", + "file": "cases/bell.wqa", + "covers": [ + "a well-formed answer: a program, a sample, a verdict and a seal" + ], + "expect": { + "errors": [], + "warnings": [], + "root": "root", + "order": [ + "bell", + "counts", + "check", + "seal" + ], + "run": { + "counts": { + "total": 1000, + "outcomes": [ + "00", + "11" + ] + }, + "check": { + "level": "proof", + "holds": true + }, + "seal": { + "sealed": [ + "bell", + "counts", + "check" + ] + } + } + } + }, + { + "name": "bell-crlf", + "file": "cases/bell-crlf.wqa", + "covers": [ + "CRLF line ends read as LF: the same answer and the same digest" + ], + "expect": { + "errors": [], + "warnings": [], + "root": "root", + "order": [ + "bell", + "counts", + "check", + "seal" + ], + "same_as": "bell" + } + }, + { + "name": "ghz-prepares", + "file": "cases/ghz-prepares.wqa", + "covers": [ + "an intent named by a reference program whose state it must prepare" + ], + "expect": { + "errors": [], + "warnings": [], + "root": "root", + "order": [ + "ghz", + "check" + ], + "run": { + "check": { + "level": "proof", + "holds": true + } + } + } + }, + { + "name": "grover-equivalent", + "file": "cases/grover-equivalent.wqa", + "covers": [ + "the equivalence ladder, and a distribution of one outcome" + ], + "expect": { + "errors": [], + "warnings": [], + "root": "root", + "order": [ + "same", + "check" + ], + "run": { + "same": { + "verdict": "equivalent" + }, + "check": { + "level": "proof", + "holds": true + } + } + } + }, + { + "name": "wrong-distribution", + "file": "cases/wrong-distribution.wqa", + "covers": [ + "an intent that fails, decided by proof" + ], + "expect": { + "errors": [], + "warnings": [], + "root": "root", + "order": [ + "check" + ], + "run": { + "check": { + "level": "proof", + "holds": false + } + } + } + }, + { + "name": "state-views", + "file": "cases/state-views.wqa", + "covers": [ + "a circuit, expectation values, amplitudes and a Bloch vector, each the engine's" + ], + "expect": { + "errors": [], + "warnings": [], + "root": "root", + "order": [ + "circ", + "z", + "amps", + "plus" + ], + "run": { + "z": { + "expectations": [ + 1.0, + 1.0 + ] + }, + "plus": { + "bloch_x": 1.0 + } + } + } + }, + { + "name": "calls-named-apart", + "file": "cases/calls-named-apart.wqa", + "covers": [ + "an engine call as a statement of its own, named by the view; a forward reference; a value no view uses" + ], + "expect": { + "errors": [], + "warnings": [ + "J112" + ], + "root": "root", + "order": [ + "counts", + "check" + ], + "run": { + "counts": { + "total": 200, + "outcomes": [ + "00", + "11" + ] + } + } + } + }, + { + "name": "no-root", + "file": "cases/no-root.wqa", + "covers": [ + "no root: the views in the order written" + ], + "expect": { + "errors": [], + "warnings": [ + "J107" + ], + "root": null, + "order": [ + "circ", + "counts" + ] + } + }, + { + "name": "number-in-a-view", + "file": "cases/number-in-a-view.wqa", + "covers": [ + "a number where a view takes the engine's call" + ], + "expect": { + "errors": [ + "J106", + "J104" + ], + "warnings": [ + "J112" + ], + "root": "root", + "order": [] + } + }, + { + "name": "unknown-view-and-call", + "file": "cases/unknown-view-and-call.wqa", + "covers": [ + "a view and engine calls the layer does not have; the root listing an engine call, which it does not take, leaves it out" + ], + "expect": { + "errors": [ + "J104", + "J106", + "J101", + "J102", + "J102" + ], + "warnings": [ + "J112" + ], + "root": "root", + "order": [] + } + }, + { + "name": "missing-and-extra-arguments", + "file": "cases/missing-and-extra-arguments.wqa", + "covers": [ + "an argument missing, and one too many" + ], + "expect": { + "errors": [ + "J106", + "J103" + ], + "warnings": [ + "J105" + ], + "root": "root", + "order": [ + "b" + ] + } + }, + { + "name": "a-program-that-does-not-read", + "file": "cases/a-program-that-does-not-read.wqa", + "covers": [ + "the reader's refusal keeps its code; what names the program falls with it; a name nothing declares" + ], + "expect": { + "errors": [ + "J106", + "Q103", + "J106", + "J106" + ], + "warnings": [], + "root": "root", + "order": [] + } + }, + { + "name": "truncated-program", + "file": "cases/truncated-program.wqa", + "covers": [ + "the text ends inside a program" + ], + "expect": { + "errors": [ + "J106", + "J106", + "J108" + ], + "warnings": [], + "root": "root", + "order": [] + } + }, + { + "name": "truncated-statement", + "file": "cases/truncated-statement.wqa", + "covers": [ + "the text ends inside a statement" + ], + "expect": { + "errors": [ + "J106", + "J108" + ], + "warnings": [], + "root": "root", + "order": [ + "bell" + ] + } + }, + { + "name": "cycle", + "file": "cases/cycle.wqa", + "covers": [ + "statements that name each other" + ], + "expect": { + "errors": [ + "J106", + "J109", + "J109" + ], + "warnings": [ + "J112" + ], + "root": "root", + "order": [] + } + }, + { + "name": "edit-by-redeclaring", + "file": "cases/edit-by-redeclaring.wqa", + "covers": [ + "a later declaration stands: an edit" + ], + "expect": { + "errors": [], + "warnings": [ + "J110" + ], + "root": "root", + "order": [ + "counts" + ], + "run": { + "counts": { + "total": 500, + "outcomes": [ + "00", + "11" + ] + } + } + } + }, + { + "name": "prose-and-unused", + "file": "cases/prose-and-unused.wqa", + "covers": [ + "a number in prose, and a view the root does not reach" + ], + "expect": { + "errors": [], + "warnings": [ + "J111", + "J112" + ], + "root": "root", + "order": [ + "note", + "circ" + ] + } + }, + { + "name": "lines-that-are-not-statements", + "file": "cases/lines-that-are-not-statements.wqa", + "covers": [ + "prose, a fence that opens no program, and a marker this layer does not read" + ], + "expect": { + "errors": [ + "J100", + "J100", + "J100" + ], + "warnings": [], + "root": "root", + "order": [ + "circ" + ] + } + }, + { + "name": "bell-stored", + "file": "cases/bell-stored.wqa", + "covers": [ + "a stored answer with its receipt marker: run again, the receipt verifies" + ], + "expect": { + "errors": [], + "warnings": [], + "root": "root", + "order": [ + "bell", + "counts", + "check", + "seal" + ], + "stored": { + "valid": true + } + } + }, + { + "name": "bell-stored-changed", + "file": "cases/bell-stored-changed.wqa", + "covers": [ + "the stored answer with a statement changed: the receipt does not verify" + ], + "expect": { + "errors": [], + "warnings": [], + "root": "root", + "order": [ + "bell", + "counts", + "check", + "seal" + ], + "stored": { + "valid": false + } + } + } + ] +} diff --git a/wai/quantum-conformance/eval/README.md b/wai/quantum-conformance/eval/README.md new file mode 100644 index 000000000..54ab5045b --- /dev/null +++ b/wai/quantum-conformance/eval/README.md @@ -0,0 +1,20 @@ +# The model harness's task set + +What `wai-quantum eval run` asks a model, and the corpus the answer view +layer's token cost is measured on. + +| Path | What | +|---|---| +| `tasks.json` | The tasks, in the conformance format: each what a model is asked (`ask`), the intent its program is held to, the run, and a reference program a test holds to the intent by proof. Built from the intent conformance set's cases ([`../intents/`](../intents/)) and the worked examples (`wai_quantum::engine::guide::examples`), each named in `from`, with a few more of the first states of a circuits course. | +| `answers.json` | Each task's answer written four ways (`wai-quantum eval corpus`): the program alone in a fenced block, the answer view layer whole (the circuit, a sample, a verdict for each intent, a seal) and with the request alone (the sample and the verdicts), and the request an agent sends a run tool as JSON. The last two carry the same program and the same request. A test holds it to the task set, and every answer in the layer reads with no diagnostic and runs to its intents. | +| `token_cost.py` | Counts the tokens of each answer in each form with a model's own vocabulary (read from its GGUF file by a tokenizer tool), and reports the totals, the means and each form against the program alone. Standard library only. | +| `token-cost.json` | Its report on `answers.json` for three open-weight models' vocabularies; the answer view layer's specification records it (§9). | + +```sh +wai-quantum eval run --tasks tasks.json --model http://127.0.0.1:8080/v1 --energy-pid PID --out report.json +wai-quantum eval corpus --tasks tasks.json --out answers.json +python3 -I token_cost.py answers.json --tokenizer A=MODEL.gguf --tokenizer B=… --tool TOKENIZER --out token-cost.json +``` + +A report is one model on one task set and ranks nothing; reports are kept +where they are written, not here. diff --git a/wai/quantum-conformance/eval/answers.json b/wai/quantum-conformance/eval/answers.json new file mode 100644 index 000000000..1f15586ca --- /dev/null +++ b/wai/quantum-conformance/eval/answers.json @@ -0,0 +1,104 @@ +{ + "format": "wai.quantum.eval-answers/1", + "about": "Each task's answer written four ways, for measuring what each costs a model in tokens: plain, the program alone in a fenced block (what to compute and show is not in it); views, the answer view layer (the program, its circuit, a sample when it measures, a verdict for each intent and a seal); views_request, the layer with the sample and the verdicts alone; job, the request an agent sends a run tool as JSON (the program as an escaped string, the run and the intents). views_request and job carry the same program and the same request. Written by wai-quantum eval corpus; a test holds it to the task set.", + "answers": [ + { + "name": "plus", + "plain": "```qasm\nOPENQASM 3.0;\nqubit[1] q;\nbit[1] c;\nh q[0];\nc = measure q;\n```\n", + "views": "root = Answer([circuit, counts, check, seal])\n```qasm prog\nOPENQASM 3.0;\nqubit[1] q;\nbit[1] c;\nh q[0];\nc = measure q;\n```\ncircuit = Circuit(prog)\ncounts = Histogram(@Sample(prog, 100))\ncheck = Verdict(@Check(prog, {\"0\": 0.5, \"1\": 0.5}))\nseal = Receipt(@Seal([prog, counts, check]))\n", + "views_request": "root = Answer([counts, check])\n```qasm prog\nOPENQASM 3.0;\nqubit[1] q;\nbit[1] c;\nh q[0];\nc = measure q;\n```\ncounts = Histogram(@Sample(prog, 100))\ncheck = Verdict(@Check(prog, {\"0\": 0.5, \"1\": 0.5}))\n", + "job": "{\"qasm\":\"OPENQASM 3.0;\\nqubit[1] q;\\nbit[1] c;\\nh q[0];\\nc = measure q;\\n\",\"shots\":100,\"intent\":[{\"distribution\":{\"0\":0.5,\"1\":0.5}}]}" + }, + { + "name": "bell", + "plain": "```qasm\nOPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n```\n", + "views": "root = Answer([circuit, counts, check, seal])\n```qasm prog\nOPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n```\ncircuit = Circuit(prog)\ncounts = Histogram(@Sample(prog, 100))\ncheck = Verdict(@Check(prog, {\"00\": 0.5, \"11\": 0.5}))\nseal = Receipt(@Seal([prog, counts, check]))\n", + "views_request": "root = Answer([counts, check])\n```qasm prog\nOPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n```\ncounts = Histogram(@Sample(prog, 100))\ncheck = Verdict(@Check(prog, {\"00\": 0.5, \"11\": 0.5}))\n", + "job": "{\"qasm\":\"OPENQASM 3.0;\\ninclude \\\"stdgates.inc\\\";\\nqubit[2] q;\\nbit[2] c;\\nh q[0];\\ncx q[0], q[1];\\nc = measure q;\\n\",\"shots\":100,\"intent\":[{\"distribution\":{\"00\":0.5,\"11\":0.5}}]}" + }, + { + "name": "bell-odd-parity", + "plain": "```qasm\nOPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nx q[0];\nc = measure q;\n```\n", + "views": "root = Answer([circuit, counts, check, seal])\n```qasm prog\nOPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nx q[0];\nc = measure q;\n```\ncircuit = Circuit(prog)\ncounts = Histogram(@Sample(prog, 100))\ncheck = Verdict(@Check(prog, {\"01\": 0.5, \"10\": 0.5}))\nseal = Receipt(@Seal([prog, counts, check]))\n", + "views_request": "root = Answer([counts, check])\n```qasm prog\nOPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nx q[0];\nc = measure q;\n```\ncounts = Histogram(@Sample(prog, 100))\ncheck = Verdict(@Check(prog, {\"01\": 0.5, \"10\": 0.5}))\n", + "job": "{\"qasm\":\"OPENQASM 3.0;\\nqubit[2] q;\\nbit[2] c;\\nh q[0];\\ncx q[0], q[1];\\nx q[0];\\nc = measure q;\\n\",\"shots\":100,\"intent\":[{\"distribution\":{\"01\":0.5,\"10\":0.5}}]}" + }, + { + "name": "ghz-3", + "plain": "```qasm\nOPENQASM 3.0;\nqubit[3] q;\nbit[3] c;\nh q[0];\ncx q[0], q[1];\ncx q[1], q[2];\nc = measure q;\n```\n", + "views": "root = Answer([circuit, counts, check, seal])\n```qasm prog\nOPENQASM 3.0;\nqubit[3] q;\nbit[3] c;\nh q[0];\ncx q[0], q[1];\ncx q[1], q[2];\nc = measure q;\n```\ncircuit = Circuit(prog)\ncounts = Histogram(@Sample(prog, 100))\ncheck = Verdict(@Check(prog, {\"000\": 0.5, \"111\": 0.5}))\nseal = Receipt(@Seal([prog, counts, check]))\n", + "views_request": "root = Answer([counts, check])\n```qasm prog\nOPENQASM 3.0;\nqubit[3] q;\nbit[3] c;\nh q[0];\ncx q[0], q[1];\ncx q[1], q[2];\nc = measure q;\n```\ncounts = Histogram(@Sample(prog, 100))\ncheck = Verdict(@Check(prog, {\"000\": 0.5, \"111\": 0.5}))\n", + "job": "{\"qasm\":\"OPENQASM 3.0;\\nqubit[3] q;\\nbit[3] c;\\nh q[0];\\ncx q[0], q[1];\\ncx q[1], q[2];\\nc = measure q;\\n\",\"shots\":100,\"intent\":[{\"distribution\":{\"000\":0.5,\"111\":0.5}}]}" + }, + { + "name": "ghz-4-prepares", + "plain": "```qasm\nOPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[4] q;\nbit[4] c;\nh q[0];\ncx q[0], q[1];\ncx q[0], q[2];\ncx q[1], q[3];\nc = measure q;\n```\n", + "views": "root = Answer([circuit, counts, check_1, check_2, seal])\n```qasm prog\nOPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[4] q;\nbit[4] c;\nh q[0];\ncx q[0], q[1];\ncx q[0], q[2];\ncx q[1], q[3];\nc = measure q;\n```\n```qasm reference\nOPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[4] q;\nbit[4] c;\nh q[0];\ncx q[0], q[1];\ncx q[1], q[2];\ncx q[2], q[3];\nc = measure q;\n```\ncircuit = Circuit(prog)\ncounts = Histogram(@Sample(prog, 100))\ncheck_1 = Verdict(@Check(prog, {\"prepares\": reference}))\ncheck_2 = Verdict(@Check(prog, {\"0000\": 0.5, \"1111\": 0.5}))\nseal = Receipt(@Seal([prog, counts, check_1, check_2]))\n", + "views_request": "root = Answer([counts, check_1, check_2])\n```qasm prog\nOPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[4] q;\nbit[4] c;\nh q[0];\ncx q[0], q[1];\ncx q[0], q[2];\ncx q[1], q[3];\nc = measure q;\n```\n```qasm reference\nOPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[4] q;\nbit[4] c;\nh q[0];\ncx q[0], q[1];\ncx q[1], q[2];\ncx q[2], q[3];\nc = measure q;\n```\ncounts = Histogram(@Sample(prog, 100))\ncheck_1 = Verdict(@Check(prog, {\"prepares\": reference}))\ncheck_2 = Verdict(@Check(prog, {\"0000\": 0.5, \"1111\": 0.5}))\n", + "job": "{\"qasm\":\"OPENQASM 3.0;\\ninclude \\\"stdgates.inc\\\";\\nqubit[4] q;\\nbit[4] c;\\nh q[0];\\ncx q[0], q[1];\\ncx q[0], q[2];\\ncx q[1], q[3];\\nc = measure q;\\n\",\"shots\":100,\"intent\":[{\"prepares\":\"OPENQASM 3.0;\\ninclude \\\"stdgates.inc\\\";\\nqubit[4] q;\\nbit[4] c;\\nh q[0];\\ncx q[0], q[1];\\ncx q[1], q[2];\\ncx q[2], q[3];\\nc = measure q;\\n\"},{\"distribution\":{\"0000\":0.5,\"1111\":0.5}}]}" + }, + { + "name": "uniform-3", + "plain": "```qasm\nOPENQASM 3.0;\nqubit[3] q;\nbit[3] c;\nh q;\nc = measure q;\n```\n", + "views": "root = Answer([circuit, counts, check, seal])\n```qasm prog\nOPENQASM 3.0;\nqubit[3] q;\nbit[3] c;\nh q;\nc = measure q;\n```\ncircuit = Circuit(prog)\ncounts = Histogram(@Sample(prog, 100))\ncheck = Verdict(@Check(prog, {\"000\": 0.125, \"001\": 0.125, \"010\": 0.125, \"011\": 0.125, \"100\": 0.125, \"101\": 0.125, \"110\": 0.125, \"111\": 0.125}))\nseal = Receipt(@Seal([prog, counts, check]))\n", + "views_request": "root = Answer([counts, check])\n```qasm prog\nOPENQASM 3.0;\nqubit[3] q;\nbit[3] c;\nh q;\nc = measure q;\n```\ncounts = Histogram(@Sample(prog, 100))\ncheck = Verdict(@Check(prog, {\"000\": 0.125, \"001\": 0.125, \"010\": 0.125, \"011\": 0.125, \"100\": 0.125, \"101\": 0.125, \"110\": 0.125, \"111\": 0.125}))\n", + "job": "{\"qasm\":\"OPENQASM 3.0;\\nqubit[3] q;\\nbit[3] c;\\nh q;\\nc = measure q;\\n\",\"shots\":100,\"intent\":[{\"distribution\":{\"000\":0.125,\"001\":0.125,\"010\":0.125,\"011\":0.125,\"100\":0.125,\"101\":0.125,\"110\":0.125,\"111\":0.125}}]}" + }, + { + "name": "grover-2", + "plain": "```qasm\nOPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q;\ncz q[0], q[1];\nh q;\nx q;\ncz q[0], q[1];\nx q;\nh q;\nc = measure q;\n```\n", + "views": "root = Answer([circuit, counts, check, seal])\n```qasm prog\nOPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q;\ncz q[0], q[1];\nh q;\nx q;\ncz q[0], q[1];\nx q;\nh q;\nc = measure q;\n```\ncircuit = Circuit(prog)\ncounts = Histogram(@Sample(prog, 100))\ncheck = Verdict(@Check(prog, {\"11\": 1.0}))\nseal = Receipt(@Seal([prog, counts, check]))\n", + "views_request": "root = Answer([counts, check])\n```qasm prog\nOPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q;\ncz q[0], q[1];\nh q;\nx q;\ncz q[0], q[1];\nx q;\nh q;\nc = measure q;\n```\ncounts = Histogram(@Sample(prog, 100))\ncheck = Verdict(@Check(prog, {\"11\": 1.0}))\n", + "job": "{\"qasm\":\"OPENQASM 3.0;\\ninclude \\\"stdgates.inc\\\";\\nqubit[2] q;\\nbit[2] c;\\nh q;\\ncz q[0], q[1];\\nh q;\\nx q;\\ncz q[0], q[1];\\nx q;\\nh q;\\nc = measure q;\\n\",\"shots\":100,\"intent\":[{\"distribution\":{\"11\":1.0}}]}" + }, + { + "name": "rotation-three-quarters", + "plain": "```qasm\nOPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nrx(pi/3) q[0];\nc = measure q;\n```\n", + "views": "root = Answer([circuit, counts, check, seal])\n```qasm prog\nOPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nrx(pi/3) q[0];\nc = measure q;\n```\ncircuit = Circuit(prog)\ncounts = Histogram(@Sample(prog, 100))\ncheck = Verdict(@Check(prog, {\"0\": 0.75, \"1\": 0.25}))\nseal = Receipt(@Seal([prog, counts, check]))\n", + "views_request": "root = Answer([counts, check])\n```qasm prog\nOPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nrx(pi/3) q[0];\nc = measure q;\n```\ncounts = Histogram(@Sample(prog, 100))\ncheck = Verdict(@Check(prog, {\"0\": 0.75, \"1\": 0.25}))\n", + "job": "{\"qasm\":\"OPENQASM 3.0;\\ninclude \\\"stdgates.inc\\\";\\nqubit[1] q;\\nbit[1] c;\\nrx(pi/3) q[0];\\nc = measure q;\\n\",\"shots\":100,\"intent\":[{\"distribution\":{\"0\":0.75,\"1\":0.25}}]}" + }, + { + "name": "w-3", + "plain": "```qasm\nOPENQASM 3.0;\nqubit[3] q;\nbit[3] c;\nry(1.9106332362490186) q[0];\nch q[0], q[1];\ncx q[1], q[2];\ncx q[0], q[1];\nx q[0];\nc = measure q;\n```\n", + "views": "root = Answer([circuit, counts, check, seal])\n```qasm prog\nOPENQASM 3.0;\nqubit[3] q;\nbit[3] c;\nry(1.9106332362490186) q[0];\nch q[0], q[1];\ncx q[1], q[2];\ncx q[0], q[1];\nx q[0];\nc = measure q;\n```\ncircuit = Circuit(prog)\ncounts = Histogram(@Sample(prog, 100))\ncheck = Verdict(@Check(prog, {\"001\": 0.3333333333333333, \"010\": 0.3333333333333333, \"100\": 0.3333333333333333}, 0.000000001))\nseal = Receipt(@Seal([prog, counts, check]))\n", + "views_request": "root = Answer([counts, check])\n```qasm prog\nOPENQASM 3.0;\nqubit[3] q;\nbit[3] c;\nry(1.9106332362490186) q[0];\nch q[0], q[1];\ncx q[1], q[2];\ncx q[0], q[1];\nx q[0];\nc = measure q;\n```\ncounts = Histogram(@Sample(prog, 100))\ncheck = Verdict(@Check(prog, {\"001\": 0.3333333333333333, \"010\": 0.3333333333333333, \"100\": 0.3333333333333333}, 0.000000001))\n", + "job": "{\"qasm\":\"OPENQASM 3.0;\\nqubit[3] q;\\nbit[3] c;\\nry(1.9106332362490186) q[0];\\nch q[0], q[1];\\ncx q[1], q[2];\\ncx q[0], q[1];\\nx q[0];\\nc = measure q;\\n\",\"shots\":100,\"intent\":[{\"distribution\":{\"001\":0.3333333333333333,\"010\":0.3333333333333333,\"100\":0.3333333333333333},\"tolerance\":1e-9}]}" + }, + { + "name": "flip-the-last", + "plain": "```qasm\nOPENQASM 3.0;\nqubit[3] q;\nbit[3] c;\nx q[2];\nc = measure q;\n```\n", + "views": "root = Answer([circuit, counts, check, seal])\n```qasm prog\nOPENQASM 3.0;\nqubit[3] q;\nbit[3] c;\nx q[2];\nc = measure q;\n```\ncircuit = Circuit(prog)\ncounts = Histogram(@Sample(prog, 100))\ncheck = Verdict(@Check(prog, {\"100\": 1.0}))\nseal = Receipt(@Seal([prog, counts, check]))\n", + "views_request": "root = Answer([counts, check])\n```qasm prog\nOPENQASM 3.0;\nqubit[3] q;\nbit[3] c;\nx q[2];\nc = measure q;\n```\ncounts = Histogram(@Sample(prog, 100))\ncheck = Verdict(@Check(prog, {\"100\": 1.0}))\n", + "job": "{\"qasm\":\"OPENQASM 3.0;\\nqubit[3] q;\\nbit[3] c;\\nx q[2];\\nc = measure q;\\n\",\"shots\":100,\"intent\":[{\"distribution\":{\"100\":1.0}}]}" + }, + { + "name": "cz-from-cx", + "plain": "```qasm\nOPENQASM 3.0;\nqubit[2] q;\nh q[1];\ncx q[0], q[1];\nh q[1];\n```\n", + "views": "root = Answer([circuit, counts, check, seal])\n```qasm prog\nOPENQASM 3.0;\nqubit[2] q;\nh q[1];\ncx q[0], q[1];\nh q[1];\n```\n```qasm reference\nOPENQASM 3.0;\nqubit[2] q;\ncz q[0], q[1];\n```\ncircuit = Circuit(prog)\ncounts = Histogram(@Sample(prog, 100))\ncheck = Verdict(@Check(prog, {\"equivalent\": reference}))\nseal = Receipt(@Seal([prog, counts, check]))\n", + "views_request": "root = Answer([counts, check])\n```qasm prog\nOPENQASM 3.0;\nqubit[2] q;\nh q[1];\ncx q[0], q[1];\nh q[1];\n```\n```qasm reference\nOPENQASM 3.0;\nqubit[2] q;\ncz q[0], q[1];\n```\ncounts = Histogram(@Sample(prog, 100))\ncheck = Verdict(@Check(prog, {\"equivalent\": reference}))\n", + "job": "{\"qasm\":\"OPENQASM 3.0;\\nqubit[2] q;\\nh q[1];\\ncx q[0], q[1];\\nh q[1];\\n\",\"shots\":100,\"intent\":[{\"equivalent\":\"OPENQASM 3.0;\\nqubit[2] q;\\ncz q[0], q[1];\\n\"}]}" + }, + { + "name": "swap-from-cx", + "plain": "```qasm\nOPENQASM 3.0;\nqubit[2] q;\ncx q[0], q[1];\ncx q[1], q[0];\ncx q[0], q[1];\n```\n", + "views": "root = Answer([circuit, counts, check, seal])\n```qasm prog\nOPENQASM 3.0;\nqubit[2] q;\ncx q[0], q[1];\ncx q[1], q[0];\ncx q[0], q[1];\n```\n```qasm reference\nOPENQASM 3.0;\nqubit[2] q;\nswap q[0], q[1];\n```\ncircuit = Circuit(prog)\ncounts = Histogram(@Sample(prog, 100))\ncheck = Verdict(@Check(prog, {\"equivalent\": reference}))\nseal = Receipt(@Seal([prog, counts, check]))\n", + "views_request": "root = Answer([counts, check])\n```qasm prog\nOPENQASM 3.0;\nqubit[2] q;\ncx q[0], q[1];\ncx q[1], q[0];\ncx q[0], q[1];\n```\n```qasm reference\nOPENQASM 3.0;\nqubit[2] q;\nswap q[0], q[1];\n```\ncounts = Histogram(@Sample(prog, 100))\ncheck = Verdict(@Check(prog, {\"equivalent\": reference}))\n", + "job": "{\"qasm\":\"OPENQASM 3.0;\\nqubit[2] q;\\ncx q[0], q[1];\\ncx q[1], q[0];\\ncx q[0], q[1];\\n\",\"shots\":100,\"intent\":[{\"equivalent\":\"OPENQASM 3.0;\\nqubit[2] q;\\nswap q[0], q[1];\\n\"}]}" + }, + { + "name": "merged-rotations", + "plain": "```qasm\nOPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nh q;\nrz(0.7) q[0];\ncx q[0], q[1];\n```\n", + "views": "root = Answer([circuit, counts, check, seal])\n```qasm prog\nOPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nh q;\nrz(0.7) q[0];\ncx q[0], q[1];\n```\n```qasm reference\nOPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nh q;\nrz(0.3) q[0];\nrz(0.4) q[0];\ncx q[0], q[1];\n```\ncircuit = Circuit(prog)\ncounts = Histogram(@Sample(prog, 100))\ncheck = Verdict(@Check(prog, {\"equivalent\": reference}))\nseal = Receipt(@Seal([prog, counts, check]))\n", + "views_request": "root = Answer([counts, check])\n```qasm prog\nOPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nh q;\nrz(0.7) q[0];\ncx q[0], q[1];\n```\n```qasm reference\nOPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nh q;\nrz(0.3) q[0];\nrz(0.4) q[0];\ncx q[0], q[1];\n```\ncounts = Histogram(@Sample(prog, 100))\ncheck = Verdict(@Check(prog, {\"equivalent\": reference}))\n", + "job": "{\"qasm\":\"OPENQASM 3.0;\\ninclude \\\"stdgates.inc\\\";\\nqubit[2] q;\\nh q;\\nrz(0.7) q[0];\\ncx q[0], q[1];\\n\",\"shots\":100,\"intent\":[{\"equivalent\":\"OPENQASM 3.0;\\ninclude \\\"stdgates.inc\\\";\\nqubit[2] q;\\nh q;\\nrz(0.3) q[0];\\nrz(0.4) q[0];\\ncx q[0], q[1];\\n\"}]}" + }, + { + "name": "noisy-bell", + "plain": "```qasm\nOPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n```\n", + "views": "root = Answer([circuit, counts, check, seal])\n```qasm prog\nOPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n```\ncircuit = Circuit(prog)\nnoise = {\"depolarizing_1q\": 0.01, \"depolarizing_2q\": 0.03, \"readout\": 0.02}\ncounts = Histogram(@Sample(prog, 4000, null, noise))\ncheck = Verdict(@Check(prog, {\"00\": 0.5, \"11\": 0.5}, 0.1, noise))\nseal = Receipt(@Seal([prog, counts, check]))\n", + "views_request": "root = Answer([counts, check])\n```qasm prog\nOPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n```\nnoise = {\"depolarizing_1q\": 0.01, \"depolarizing_2q\": 0.03, \"readout\": 0.02}\ncounts = Histogram(@Sample(prog, 4000, null, noise))\ncheck = Verdict(@Check(prog, {\"00\": 0.5, \"11\": 0.5}, 0.1, noise))\n", + "job": "{\"qasm\":\"OPENQASM 3.0;\\ninclude \\\"stdgates.inc\\\";\\nqubit[2] q;\\nbit[2] c;\\nh q[0];\\ncx q[0], q[1];\\nc = measure q;\\n\",\"shots\":4000,\"noise\":{\"depolarizing_1q\":0.01,\"depolarizing_2q\":0.03,\"readout\":0.02},\"intent\":[{\"distribution\":{\"00\":0.5,\"11\":0.5},\"tolerance\":0.1}]}" + } + ] +} diff --git a/wai/quantum-conformance/eval/tasks.json b/wai/quantum-conformance/eval/tasks.json new file mode 100644 index 000000000..a7d6e167d --- /dev/null +++ b/wai/quantum-conformance/eval/tasks.json @@ -0,0 +1,439 @@ +{ + "format": "wai.quantum-intent-conformance-bundle/1", + "about": "The model harness's task set (wai-quantum eval run --tasks): each case what a model is asked (ask), the intent its program is held to, the run, and a reference program (qasm) that a test holds to the intent by proof. Built from the intent conformance set's cases (wai/quantum-conformance/intents/) and the worked examples (wai_quantum::engine::guide::examples), each named in from, with a few more of the first states of a circuits course.", + "cases": [ + { + "name": "plus", + "from": [ + "new: the first state of the circuits curriculum" + ], + "covers": [ + "Put one qubit in an equal superposition of 0 and 1, so that half the shots read 0 and half read 1." + ], + "ask": "Put one qubit in an equal superposition of 0 and 1, so that half the shots read 0 and half read 1. Use 1 qubit. Measure every qubit at the end into a classical bit of the same index (c = measure q;); outcomes are written with the highest bit first.", + "qubits": 1, + "qasm": "OPENQASM 3.0;\nqubit[1] q;\nbit[1] c;\nh q[0];\nc = measure q;\n", + "run": { + "shots": 100 + }, + "intent": [ + { + "distribution": { + "0": 0.5, + "1": 0.5 + } + } + ], + "expect": { + "holds": true, + "levels": [ + "proof", + "evidence" + ] + } + }, + { + "name": "bell", + "from": [ + "intents: bell-distribution", + "examples: bell" + ], + "covers": [ + "Prepare a Bell pair on two qubits: half the shots read 00 and half 11, never 01 or 10." + ], + "ask": "Prepare a Bell pair on two qubits: half the shots read 00 and half 11, never 01 or 10. Use 2 qubits. Measure every qubit at the end into a classical bit of the same index (c = measure q;); outcomes are written with the highest bit first.", + "qubits": 2, + "qasm": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n", + "run": { + "shots": 100 + }, + "intent": [ + { + "distribution": { + "00": 0.5, + "11": 0.5 + } + } + ], + "expect": { + "holds": true, + "levels": [ + "proof", + "evidence" + ] + } + }, + { + "name": "bell-odd-parity", + "from": [ + "new: a Bell state of odd parity" + ], + "covers": [ + "Prepare the Bell state (|01⟩ + |10⟩)/√2 on two qubits: half the shots read 01 and half 10, never 00 or 11." + ], + "ask": "Prepare the Bell state (|01⟩ + |10⟩)/√2 on two qubits: half the shots read 01 and half 10, never 00 or 11. Use 2 qubits. Measure every qubit at the end into a classical bit of the same index (c = measure q;); outcomes are written with the highest bit first.", + "qubits": 2, + "qasm": "OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nx q[0];\nc = measure q;\n", + "run": { + "shots": 100 + }, + "intent": [ + { + "distribution": { + "01": 0.5, + "10": 0.5 + } + } + ], + "expect": { + "holds": true, + "levels": [ + "proof", + "evidence" + ] + } + }, + { + "name": "ghz-3", + "from": [ + "intents: ghz-prepares-the-chain-s-state, at three qubits" + ], + "covers": [ + "Prepare a three-qubit GHZ state: half the shots read 000 and half 111." + ], + "ask": "Prepare a three-qubit GHZ state: half the shots read 000 and half 111. Use 3 qubits. Measure every qubit at the end into a classical bit of the same index (c = measure q;); outcomes are written with the highest bit first.", + "qubits": 3, + "qasm": "OPENQASM 3.0;\nqubit[3] q;\nbit[3] c;\nh q[0];\ncx q[0], q[1];\ncx q[1], q[2];\nc = measure q;\n", + "run": { + "shots": 100 + }, + "intent": [ + { + "distribution": { + "000": 0.5, + "111": 0.5 + } + } + ], + "expect": { + "holds": true, + "levels": [ + "proof", + "evidence" + ] + } + }, + { + "name": "ghz-4-prepares", + "from": [ + "intents: ghz-prepares-the-chain-s-state", + "examples: ghz" + ], + "covers": [ + "Prepare the four-qubit GHZ state (|0000⟩ + |1111⟩)/√2 from |0000⟩, in as few layers of gates as you can." + ], + "ask": "Prepare the four-qubit GHZ state (|0000⟩ + |1111⟩)/√2 from |0000⟩, in as few layers of gates as you can. Use 4 qubits. Measure every qubit at the end into a classical bit of the same index (c = measure q;); outcomes are written with the highest bit first.", + "qubits": 4, + "qasm": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[4] q;\nbit[4] c;\nh q[0];\ncx q[0], q[1];\ncx q[0], q[2];\ncx q[1], q[3];\nc = measure q;\n", + "run": { + "shots": 100 + }, + "intent": [ + { + "prepares": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[4] q;\nbit[4] c;\nh q[0];\ncx q[0], q[1];\ncx q[1], q[2];\ncx q[2], q[3];\nc = measure q;\n", + "name": "the GHZ state by a chain of CNOTs" + }, + { + "distribution": { + "0000": 0.5, + "1111": 0.5 + } + } + ], + "expect": { + "holds": true, + "levels": [ + "proof", + "evidence" + ] + } + }, + { + "name": "uniform-3", + "from": [ + "new: the uniform superposition" + ], + "covers": [ + "Put three qubits in the uniform superposition: every one of the eight outcomes equally likely." + ], + "ask": "Put three qubits in the uniform superposition: every one of the eight outcomes equally likely. Use 3 qubits. Measure every qubit at the end into a classical bit of the same index (c = measure q;); outcomes are written with the highest bit first.", + "qubits": 3, + "qasm": "OPENQASM 3.0;\nqubit[3] q;\nbit[3] c;\nh q;\nc = measure q;\n", + "run": { + "shots": 100 + }, + "intent": [ + { + "distribution": { + "000": 0.125, + "001": 0.125, + "010": 0.125, + "011": 0.125, + "100": 0.125, + "101": 0.125, + "110": 0.125, + "111": 0.125 + } + } + ], + "expect": { + "holds": true, + "levels": [ + "proof", + "evidence" + ] + } + }, + { + "name": "grover-2", + "from": [ + "intents: grover-two-intents", + "examples: grover" + ], + "covers": [ + "Run one round of Grover's search over the four basis states of two qubits, marking 11, so that every shot reads 11." + ], + "ask": "Run one round of Grover's search over the four basis states of two qubits, marking 11, so that every shot reads 11. Use 2 qubits. Measure every qubit at the end into a classical bit of the same index (c = measure q;); outcomes are written with the highest bit first.", + "qubits": 2, + "qasm": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q;\ncz q[0], q[1];\nh q;\nx q;\ncz q[0], q[1];\nx q;\nh q;\nc = measure q;\n", + "run": { + "shots": 100 + }, + "intent": [ + { + "distribution": { + "11": 1.0 + } + } + ], + "expect": { + "holds": true, + "levels": [ + "proof", + "evidence" + ] + } + }, + { + "name": "rotation-three-quarters", + "from": [ + "intents: compiled-first" + ], + "covers": [ + "Rotate one qubit so that it reads 0 with probability 3/4 and 1 with probability 1/4." + ], + "ask": "Rotate one qubit so that it reads 0 with probability 3/4 and 1 with probability 1/4. Use 1 qubit. Measure every qubit at the end into a classical bit of the same index (c = measure q;); outcomes are written with the highest bit first.", + "qubits": 1, + "qasm": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nrx(pi/3) q[0];\nc = measure q;\n", + "run": { + "shots": 100 + }, + "intent": [ + { + "distribution": { + "0": 0.75, + "1": 0.25 + } + } + ], + "expect": { + "holds": true, + "levels": [ + "proof", + "evidence" + ] + } + }, + { + "name": "w-3", + "from": [ + "new: the W state" + ], + "covers": [ + "Prepare the three-qubit W state (|001⟩ + |010⟩ + |100⟩)/√3: each of 001, 010 and 100 read a third of the time, and nothing else." + ], + "ask": "Prepare the three-qubit W state (|001⟩ + |010⟩ + |100⟩)/√3: each of 001, 010 and 100 read a third of the time, and nothing else. Use 3 qubits. Measure every qubit at the end into a classical bit of the same index (c = measure q;); outcomes are written with the highest bit first.", + "qubits": 3, + "qasm": "OPENQASM 3.0;\nqubit[3] q;\nbit[3] c;\nry(1.9106332362490186) q[0];\nch q[0], q[1];\ncx q[1], q[2];\ncx q[0], q[1];\nx q[0];\nc = measure q;\n", + "run": { + "shots": 100 + }, + "intent": [ + { + "distribution": { + "001": 0.3333333333333333, + "010": 0.3333333333333333, + "100": 0.3333333333333333 + }, + "tolerance": 1e-09 + } + ], + "expect": { + "holds": true, + "levels": [ + "proof", + "evidence" + ] + } + }, + { + "name": "flip-the-last", + "from": [ + "new: one qubit flipped" + ], + "covers": [ + "On three qubits, flip only q[2] to 1, so that every shot reads 100." + ], + "ask": "On three qubits, flip only q[2] to 1, so that every shot reads 100. Use 3 qubits. Measure every qubit at the end into a classical bit of the same index (c = measure q;); outcomes are written with the highest bit first.", + "qubits": 3, + "qasm": "OPENQASM 3.0;\nqubit[3] q;\nbit[3] c;\nx q[2];\nc = measure q;\n", + "run": { + "shots": 100 + }, + "intent": [ + { + "distribution": { + "100": 1.0 + } + } + ], + "expect": { + "holds": true, + "levels": [ + "proof", + "evidence" + ] + } + }, + { + "name": "cz-from-cx", + "from": [ + "intents: the same circuit as a reference (equivalent)" + ], + "covers": [ + "Write the controlled-Z gate between q[0] and q[1] using only h and cx gates (no cz), with no measurement." + ], + "ask": "Write the controlled-Z gate between q[0] and q[1] using only h and cx gates (no cz), with no measurement. Use 2 qubits.", + "qubits": 2, + "qasm": "OPENQASM 3.0;\nqubit[2] q;\nh q[1];\ncx q[0], q[1];\nh q[1];\n", + "run": { + "shots": 100 + }, + "intent": [ + { + "equivalent": "OPENQASM 3.0;\nqubit[2] q;\ncz q[0], q[1];\n", + "name": "cz" + } + ], + "expect": { + "holds": true, + "levels": [ + "proof", + "evidence" + ] + } + }, + { + "name": "swap-from-cx", + "from": [ + "intents: the same circuit as a reference (equivalent)" + ], + "covers": [ + "Write a swap of q[0] and q[1] using only cx gates, with no measurement." + ], + "ask": "Write a swap of q[0] and q[1] using only cx gates, with no measurement. Use 2 qubits.", + "qubits": 2, + "qasm": "OPENQASM 3.0;\nqubit[2] q;\ncx q[0], q[1];\ncx q[1], q[0];\ncx q[0], q[1];\n", + "run": { + "shots": 100 + }, + "intent": [ + { + "equivalent": "OPENQASM 3.0;\nqubit[2] q;\nswap q[0], q[1];\n", + "name": "swap" + } + ], + "expect": { + "holds": true, + "levels": [ + "proof", + "evidence" + ] + } + }, + { + "name": "merged-rotations", + "from": [ + "intents: rotations-merge" + ], + "covers": [ + "Write the same circuit as this one with the two rz gates on q[0] merged into one gate:" + ], + "ask": "Write the same circuit as this one with the two rz gates on q[0] merged into one gate:\nOPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nh q;\nrz(0.3) q[0];\nrz(0.4) q[0];\ncx q[0], q[1];\n Use 2 qubits.", + "qubits": 2, + "qasm": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nh q;\nrz(0.7) q[0];\ncx q[0], q[1];\n", + "run": { + "shots": 100 + }, + "intent": [ + { + "equivalent": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nh q;\nrz(0.3) q[0];\nrz(0.4) q[0];\ncx q[0], q[1];\n", + "name": "the program with its two rotations" + } + ], + "expect": { + "holds": true, + "levels": [ + "proof", + "evidence" + ] + } + }, + { + "name": "noisy-bell", + "from": [ + "examples: noisy-bell" + ], + "covers": [ + "Prepare a Bell pair on two qubits, to be run under gate errors and readout flips: the engine checks that its noisy distribution is within 0.1 of the ideal pair." + ], + "ask": "Prepare a Bell pair on two qubits, to be run under gate errors and readout flips: the engine checks that its noisy distribution is within 0.1 of the ideal pair. Use 2 qubits. Measure every qubit at the end into a classical bit of the same index (c = measure q;); outcomes are written with the highest bit first.", + "qubits": 2, + "qasm": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n", + "run": { + "shots": 4000, + "noise": { + "depolarizing_1q": 0.01, + "depolarizing_2q": 0.03, + "readout": 0.02 + } + }, + "intent": [ + { + "distribution": { + "00": 0.5, + "11": 0.5 + }, + "tolerance": 0.1 + } + ], + "expect": { + "holds": true, + "levels": [ + "proof", + "evidence" + ] + } + } + ] +} diff --git a/wai/quantum-conformance/eval/token-cost.json b/wai/quantum-conformance/eval/token-cost.json new file mode 100644 index 000000000..34fa4cd93 --- /dev/null +++ b/wai/quantum-conformance/eval/token-cost.json @@ -0,0 +1,382 @@ +{ + "format": "wai.quantum.token-cost/1", + "about": "The tokens a task set's answers take, each written four ways: plain (the program alone, in a fenced block), views (the answer view layer: the program, its circuit, a sample when it measures, a verdict for each intent, a seal), views_request (the layer with the sample and the verdicts alone) and job (the request an agent sends a run tool, as JSON). views_request and job carry the same program and the same request; views adds the circuit and the seal; plain carries the program alone.", + "answers": 14, + "bytes": { + "plain": 1402, + "views": 5119, + "views_request": 3932, + "job": 2857 + }, + "tokenizers": [ + { + "name": "A", + "vocabulary": 49159, + "totals": { + "plain": 841, + "views": 2365, + "views_request": 1967, + "job": 1492 + }, + "mean": { + "plain": 60.1, + "views": 168.9, + "views_request": 140.5, + "job": 106.6 + }, + "against_plain": { + "plain": 1.0, + "views": 2.812, + "views_request": 2.339, + "job": 1.774 + }, + "views_request_against_job": 1.318, + "per_answer": [ + { + "name": "plus", + "plain": 42, + "views": 120, + "views_request": 92, + "job": 64 + }, + { + "name": "bell", + "plain": 61, + "views": 141, + "views_request": 113, + "job": 85 + }, + { + "name": "bell-odd-parity", + "plain": 58, + "views": 138, + "views_request": 110, + "job": 82 + }, + { + "name": "ghz-3", + "plain": 62, + "views": 144, + "views_request": 116, + "job": 88 + }, + { + "name": "ghz-4-prepares", + "plain": 81, + "views": 280, + "views_request": 246, + "job": 189 + }, + { + "name": "uniform-3", + "plain": 40, + "views": 198, + "views_request": 170, + "job": 130 + }, + { + "name": "grover-2", + "plain": 85, + "views": 156, + "views_request": 128, + "job": 102 + }, + { + "name": "rotation-three-quarters", + "plain": 56, + "views": 136, + "views_request": 108, + "job": 80 + }, + { + "name": "w-3", + "plain": 98, + "views": 248, + "views_request": 220, + "job": 183 + }, + { + "name": "flip-the-last", + "plain": 42, + "views": 114, + "views_request": 86, + "job": 60 + }, + { + "name": "cz-from-cx", + "plain": 45, + "views": 147, + "views_request": 119, + "job": 83 + }, + { + "name": "swap-from-cx", + "plain": 53, + "views": 155, + "views_request": 127, + "job": 91 + }, + { + "name": "merged-rotations", + "plain": 57, + "views": 194, + "views_request": 166, + "job": 130 + }, + { + "name": "noisy-bell", + "plain": 61, + "views": 194, + "views_request": 166, + "job": 125 + } + ] + }, + { + "name": "B", + "vocabulary": 248320, + "totals": { + "plain": 834, + "views": 2312, + "views_request": 1928, + "job": 1491 + }, + "mean": { + "plain": 59.6, + "views": 165.1, + "views_request": 137.7, + "job": 106.5 + }, + "against_plain": { + "plain": 1.0, + "views": 2.772, + "views_request": 2.312, + "job": 1.788 + }, + "views_request_against_job": 1.293, + "per_answer": [ + { + "name": "plus", + "plain": 42, + "views": 117, + "views_request": 90, + "job": 64 + }, + { + "name": "bell", + "plain": 60, + "views": 137, + "views_request": 110, + "job": 84 + }, + { + "name": "bell-odd-parity", + "plain": 58, + "views": 135, + "views_request": 108, + "job": 82 + }, + { + "name": "ghz-3", + "plain": 62, + "views": 141, + "views_request": 114, + "job": 88 + }, + { + "name": "ghz-4-prepares", + "plain": 80, + "views": 273, + "views_request": 240, + "job": 186 + }, + { + "name": "uniform-3", + "plain": 40, + "views": 195, + "views_request": 168, + "job": 130 + }, + { + "name": "grover-2", + "plain": 84, + "views": 152, + "views_request": 125, + "job": 101 + }, + { + "name": "rotation-three-quarters", + "plain": 54, + "views": 131, + "views_request": 104, + "job": 78 + }, + { + "name": "w-3", + "plain": 98, + "views": 245, + "views_request": 218, + "job": 186 + }, + { + "name": "flip-the-last", + "plain": 42, + "views": 111, + "views_request": 84, + "job": 60 + }, + { + "name": "cz-from-cx", + "plain": 45, + "views": 144, + "views_request": 117, + "job": 84 + }, + { + "name": "swap-from-cx", + "plain": 53, + "views": 152, + "views_request": 125, + "job": 92 + }, + { + "name": "merged-rotations", + "plain": 56, + "views": 189, + "views_request": 162, + "job": 129 + }, + { + "name": "noisy-bell", + "plain": 60, + "views": 190, + "views_request": 163, + "job": 127 + } + ] + }, + { + "name": "C", + "vocabulary": 262144, + "totals": { + "plain": 835, + "views": 2282, + "views_request": 1940, + "job": 1483 + }, + "mean": { + "plain": 59.6, + "views": 163.0, + "views_request": 138.6, + "job": 105.9 + }, + "against_plain": { + "plain": 1.0, + "views": 2.733, + "views_request": 2.323, + "job": 1.776 + }, + "views_request_against_job": 1.308, + "per_answer": [ + { + "name": "plus", + "plain": 42, + "views": 115, + "views_request": 91, + "job": 64 + }, + { + "name": "bell", + "plain": 60, + "views": 135, + "views_request": 111, + "job": 84 + }, + { + "name": "bell-odd-parity", + "plain": 58, + "views": 133, + "views_request": 109, + "job": 82 + }, + { + "name": "ghz-3", + "plain": 62, + "views": 139, + "views_request": 115, + "job": 88 + }, + { + "name": "ghz-4-prepares", + "plain": 80, + "views": 271, + "views_request": 241, + "job": 186 + }, + { + "name": "uniform-3", + "plain": 40, + "views": 193, + "views_request": 169, + "job": 130 + }, + { + "name": "grover-2", + "plain": 84, + "views": 150, + "views_request": 126, + "job": 101 + }, + { + "name": "rotation-three-quarters", + "plain": 55, + "views": 130, + "views_request": 106, + "job": 79 + }, + { + "name": "w-3", + "plain": 98, + "views": 243, + "views_request": 219, + "job": 183 + }, + { + "name": "flip-the-last", + "plain": 42, + "views": 109, + "views_request": 85, + "job": 60 + }, + { + "name": "cz-from-cx", + "plain": 45, + "views": 141, + "views_request": 117, + "job": 83 + }, + { + "name": "swap-from-cx", + "plain": 53, + "views": 149, + "views_request": 125, + "job": 91 + }, + { + "name": "merged-rotations", + "plain": 56, + "views": 186, + "views_request": 162, + "job": 128 + }, + { + "name": "noisy-bell", + "plain": 60, + "views": 188, + "views_request": 164, + "job": 124 + } + ] + } + ] +} diff --git a/wai/quantum-conformance/eval/token_cost.py b/wai/quantum-conformance/eval/token_cost.py new file mode 100644 index 000000000..9e330ab24 --- /dev/null +++ b/wai/quantum-conformance/eval/token_cost.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +"""What a task set's answers cost a model in tokens, written four ways. + + python3 -I token_cost.py answers.json --tokenizer NAME=MODEL.gguf [...] --tool PATH [--out FILE] + +answers.json is what `wai-quantum eval corpus` writes: each task's answer as +plain OpenQASM (the program alone, in a fenced block), in the answer view +layer (whole, and with the request alone), and as a job's JSON. For each tokenizer, the vocabulary of a model read +from its GGUF file by a tokenizer tool (one that takes `-m MODEL -f FILE --ids +--no-bos --log-disable` and prints the ids as a list), this counts the tokens of +every answer in each form, and reports the totals, the per-answer means, each +form against plain, and the bytes. A tokenizer's size is its vocabulary's, read +from the GGUF file's tokenizer.ggml.tokens. Standard library only; it reads the +model files and writes nothing but its report. +""" +import argparse +import json +import os +import struct +import subprocess +import sys +import tempfile + +FORMS = ["plain", "views", "views_request", "job"] + + +def vocabulary(path): + """The number of tokens in a GGUF file's tokenizer.ggml.tokens, or None.""" + sizes = {0: 1, 1: 1, 2: 2, 3: 2, 4: 4, 5: 4, 6: 4, 7: 1, 10: 8, 11: 8, 12: 8} + with open(path, "rb") as f: + if f.read(4) != b"GGUF": + return None + (version,) = struct.unpack("/.json`, one file for each part of the interface, English the source and Simplified Chinese beside it (`locales/index.js` gathers them). The Studio follows the browser's language until you pick one with the switch in the header; your choice is kept in this browser. A host may pin one with `data-lang` on the mount element. The engine's own messages (a verdict's statement, a diagnostic) stay as the engine writes them. The Chinese words are a machine-assisted translation, marked so in each file and in the footer, to be reviewed by a fluent reader. `node test/locales.test.mjs` holds every locale to every key, with the same placeholders. + ## Step through "Step through", under the circuit, runs the program one gate, measurement or reset at a time on the engine's debugger, in this browser (or natively on the desktop). It steps on a method where every number is exact for it: the bit-exact state vector when every angle is a dyadic multiple of 2π, else double precision, and under the job's noise the density matrix, beside the noiseless state on the same path. Up to 16 qubits are stepped, or 8 under noise; a wider program is refused with the reason. @@ -86,6 +102,12 @@ A proposal is verified only when it reads, keeps the rules and passes the check "Estimate", under the compile lens, says what a fault-tolerant machine would need to run the program, on the physical qubit chosen and within the error budget given (the probability the run may fail). It gives the program's logical counts first: its logical qubits (with the ancillas a gate under several controls takes), measurements, `T` gates, Toffolis and rotations, the `T` gates in the rotations' synthesised words, and the `T` depth and total depth; then each rotation's share of the budget and how many `T` gates its word took, and the code model the distance is chosen by (on a gate-based qubit, fitted to the engine's own exact-matching or union-find decoder, or the planar model's assumed one). The estimate is the headline: physical qubits, `T` factories and the share of qubits they take, runtime and code distance. The frontier plots every estimate no other beats on both physical qubits and runtime, runtime across and qubits up on log scales, and lists them in a table; pointing at a point, or focusing it, reads it out. Every estimate is sealed with this browser's key (the desktop app's, in the app) in the resource-estimate receipt family, and the line under the table says whether the seals verify and whether the request re-run here gives every figure. An estimate is marked stale once the program changes. Every number and what it assumes is on [the estimate page](../wai-quantum/docs/estimate.md). +## Error correction + +"Error correction", a closed section under the result, opens on a code chosen from a list: the rotated surface code, the triangular colour code, the toric code, the Steane code, and two bivariate-bicycle codes. Its lattice is drawn with each check and qubit, and its distance certificate stated under it: in each sector, the least weight of a logical found, and every lighter support ruled out by an exhaustive search; "Show witness" rings a logical of that weight on the lattice. Clicking a qubit (or Enter on it) flips it, and its syndrome lights the checks it violates; "Decode" corrects the syndrome with the decoder chosen, draws the correction, and says whether the residual is a product of stabilizers (the logical state unharmed) or a logical error. Under "Threshold", "Measure" samples logical failure rates at distances 3, 5 and 7 over three physical rates and plots them on log scales, each point with its failures, shots and standard error. Every answer is sealed with this browser's key (the desktop app's, in the app) and checked here. What each decoder guarantees is on [the error-correction page](../wai-quantum/docs/qec.md). + +With noise set, "Mitigate", in the Noise section, mitigates the run's expectation values (zero-noise extrapolation, error amplification or cancellation from a learned noise model, Clifford data regression, virtual distillation, or readout inversion). The result shows each value raw and mitigated with its error bar, beside the ideal one the simulation knows, with the method's bias and error-bar assumptions and the circuits and shots it took. + ## What is new, and Found The engine says what is new in a program: the known construction it is, found again; a known result reached by a different route; or how far it is from the nearest program it was compared with. The known constructions are GHZ by a chain, by doubling and by fan-out, the uniform superposition and the quantum Fourier transform. The distance is structural, from 0 (built alike) to 1 (nothing shared), and it is the engine's, not a search of the literature. @@ -172,6 +194,21 @@ The engine is the model's tool, not an afterthought: `node test/assistant-live.mjs URL` drives the assistant against a real model server and prints what it did. It is not part of CI, which has no model. +## Answer views + +Under **Answer views**, an answer in the answer view layer ([quantum-answers.md](../extensions/quantum-answers.md)) is drawn as it arrives: the program in a fenced `qasm` block, and views whose numbers come from engine calls written with `@` (`counts = Histogram(@Sample(bell, 1000))`, `check = Verdict(@Check(bell, {"00": 0.5, "11": 0.5}))`, `seal = Receipt(@Seal([bell, counts, check]))`). Paste one and choose **Draw**, or ask the connected model for one: it is told the layer, streams its answer, and each view is drawn as soon as its statement and what it names have arrived, a program's circuit growing gate by gate while its lines stream in. + +- The text goes through the engine's own reader (`AnswerStream` in the page, the native one on the desktop), so what is drawn is what the layer says. +- Every number in a view is the engine's, from its call carried out here; a view whose argument is a number the model wrote is refused with `J104`, and every other problem is listed with its line and code. A view whose call the engine refuses shows the refusal, for that view alone. +- A `Receipt` is the seal of the statements it names, checked here. **Save with its receipt** saves the answer with the marker line that lets anyone check it again by running it. + +## Correctness record + +Under the assistant, **Correctness record** keeps, when you turn it on, one record of each answer the model gives (an assistant's proposal, or an answer in the layer): the diagnostic codes it drew, the level the engine checked it to and whether its intent held, the repairs made, whether you took it (**Apply** and **Take it**) or set it aside (**Not this**), and its receipt. It is off by default: until you turn it on nothing is recorded and nothing is written, not even a key in this browser's storage. Nothing is ever sent anywhere. **Export the record** saves it to a file; **Export the failures as an eval corpus** saves the answers that did not hold, or that you set aside, in the conformance format, which `wai-quantum eval run --tasks` takes. + +## Views for agent hosts + +[`src/views/`](src/views/) is the same interface as six views an agent host draws through the Model Context Protocol's extension for interactive views (`io.modelcontextprotocol/ui`, version 2026-01-26): a result with its verdict, a receipt, a circuit, a critique, an estimate's frontier and a device target's map, each from the Studio's components (`circuit-svg.js`, `verdict-view.js`, `result-parts.js`, `device.js`, `estimate-view.js`, `critique-view.js`). `build-views.mjs` puts them into one self-contained page with the engine inside it (`wai/wai-quantum-views`), as WebAssembly and translated to JavaScript, which `wai/wai-quantum-views/build.sh` writes to `wai/wai-quantum/views/` for both MCP servers to serve; `node build-views.mjs --check` says whether that page is built from the sources as they are. ## What is stable The Studio is an application. What it promises is the engine's, through what it gives: a job in the engine's job JSON, which follows its schemas, its result naming the engine that made it and its schema, and a receipt that verifies in every later version; and the history files `wai-quantum history verify` checks. What each WAI Quantum version promises, and how a change is announced: [the policy](https://git.transaction.science/Transaction-Science/open-standards/src/branch/main/wai/wai-quantum/VERSIONING.md); what is deprecated: [the deprecations](https://git.transaction.science/Transaction-Science/open-standards/src/branch/main/wai/wai-quantum/DEPRECATIONS.md). @@ -198,11 +235,17 @@ A host mounts the Studio on an element: ``` -`data-embedded` tells the Studio the host page has its own heading, so the Studio's name is not a second `h1`. `data-engine-pkg` names the engine's browser package: wai-web built with the feature `quantum_engine`. `node build.mjs --pkg DIR` copies such a package into `dist/pkg/` for `index.html`. On the desktop, `window.__TAURI__` is present and the Studio uses the native engine instead. +`data-embedded` tells the Studio the host page has its own heading, so the Studio's name is not a second `h1`. `data-lang` (`en` or `zh-CN`) pins the language. `data-engine-pkg` names the engine's browser package: wai-web built with the feature `quantum_engine`. `node build.mjs --pkg DIR` copies such a package into `dist/pkg/` for `index.html`. On the desktop, `window.__TAURI__` is present and the Studio uses the native engine instead. ## Testing it -```sh +`node test/telemetry.test.mjs` holds the correctness record to its rule without a browser: while it is off, nothing is recorded, nothing is written to storage and the engine is not asked. + +`WQ_CLI=PATH/wai-quantum [WQ_SERVICE=PATH/wai-quantum-service] node test/views.test.mjs` is a scripted agent host in the installed browser: it finds each tool's view, reads the six pages (each within 1 MiB), and draws each in a sandboxed frame under the extension's default content-security policy and under that policy with WebAssembly allowed; the receipt is checked in the view (as JavaScript and as WebAssembly), a changed result fails the check, the model is told the check, the service's running job is followed by the view, and no view makes a request of its own. + +The Studio itself: + +```sh norun=ci:wai-quantum-studio.yml node build.mjs --pkg PATH/TO/PKG && node test/studio.test.mjs ``` @@ -220,6 +263,11 @@ The test runs the Studio in the installed browser: - stepping through teleportation on the bit-exact method: the slider forward and back to the same bits, the next step named and marked, each qubit's Bloch sphere and phase disk, the amplitudes, a reduced state, the four branches a quarter each, one chosen and the state it leaves, and the noisy state against the noiseless one; - editing by the drawing: a gate dropped from the palette onto a wire, one dragged to another wire, moved and deleted from the keyboard, each recorded as your edit, and a gate of a broadcast refused with its line; - the circuit described in the engine's words, and the drawing saved as SVG and as LaTeX; +- the course, in English: a lesson's starter loaded and checked, what went wrong, a hint with its pointer, the solution passed and kept in the browser, a rule and a generator's cases each with their verdict; +- the course in Chinese from the browser's language, the switch to English and back, and the layout at a phone's width with the course open; - the layout at a phone's width. +- the correctness record, off by default and nothing kept while off; turned on, a proposal recorded with its level, "Not this" kept, and the failures exported as a corpus; +- answer views: an answer drawn from its text, its numbers the engine's, a number the model wrote refused with `J104`, the seal checked here, and an answer the scripted model streams drawn view by view before the stream ends; + `STUDIO_SKIP_SERVICE=1` leaves the service step out. It saves screenshots of both colour schemes in `test/screenshots/`. diff --git a/wai/quantum-studio/README.zh-CN.md b/wai/quantum-studio/README.zh-CN.md new file mode 100644 index 000000000..d4d31aa72 --- /dev/null +++ b/wai/quantum-studio/README.zh-CN.md @@ -0,0 +1,240 @@ + + +# Quantum Studio + +> 本文为机器辅助翻译,尚待熟练的读者审校。英文原文:[README.md](README.md)。 + +一个 OpenQASM 程序编辑器,可以在三个地方运行程序: + +- **这个浏览器**:借助编译为 WebAssembly 的量子作业服务引擎,在一个 worker 中运行; +- **这台计算机**:借助以原生方式构建的同一个引擎,在[桌面应用](../wai-quantum-desktop/)中运行; +- **作业服务**:通过 HTTP 运行。 + +在桌面应用中,**你的硬件**是第四个地方:你自己的某个提供方(`wai-quantum providers add`);运行前先向它询问估算,不免费的运行之前会显示一个对话框;凭据由应用持有,从不由页面持有。在浏览器标签页中,这个切换开关只会说明此类运行须经由桌面应用或命令行进行(`wai-quantum run --provider NAME`),因为浏览器标签页不得持有提供方的凭据。 + +每个地方返回的都是同样的作业:请求、所用的方法及选择它的原因、结果,以及结果的回执。每次运行都会请求其判定,结果以徽章显示引擎检查所达到的级别,并附上其陈述,由回执封存;若结果来自不提供判定的主机,或运行被拒绝,其判定就在本设备上得出,并注明未经封存。Studio 以同样的方式显示它们,并在其运行所在的机器上检查每一份回执。同一个请求配上同一个种子,在每个地方都给出相同的计数。 + +编辑器: + +- 高亮显示 OpenQASM; +- 补全标准门和程序自身的寄存器; +- 在相应的行上标出引擎的诊断; +- 随着你输入,画出线路并显示计划。 + +- 让你通过线路图编辑线路(见下文); +- 用引擎的措辞把线路读给屏幕阅读器,并把线路图保存为 SVG 或 LaTeX。 + +Ctrl+Enter(macOS 上为 ⌘+Enter)运行程序,Escape 取消运行。程序和运行历史保存在本设备上。“复制链接”(Copy link)把程序放进 URL 片段中,任何服务器都看不到它。 + +## 课程 + +**课程**(Course)打开量子线路课程([`wai-quantum/course/`](../wai-quantum/course/README.md)):21 课,从第一个线路到资源估算,与 `wai-quantum learn` 运行的是同一门课程。每一课由一段简短的文字和一道练习组成: + +- **载入起始程序**(Load the starter)把练习的起始程序放进编辑器;你的草稿保留在“版本”(Versions)中。 +- **检查**(Check)用引擎对照练习运行编辑器中的程序(在这个浏览器中,或在桌面上以原生方式):给出每种情况的判定及其徽章,每条规则(允许的门、门的数量、T 门数、评析中的问题模式、估算的计数)以 ✓ 或 ✗ 标示,以及“已通过”或“尚未通过”。 +- **哪里出了问题?**(What went wrong?)用你的语言说明读取器的诊断、判定和评析发现了什么,并在旁边附上引擎的原文。 +- **提示**(Hint)逐条给出本课的提示,并附上引擎指出的“从哪里看起”。 +- **请助手给一条提示**(Ask the assistant for a hint):在已连接模型时,向它请求一条提示,并告诉它不要写出程序;它回答中能通过这道练习的程序会被扣下,代码也不会显示。 + +这些都不会显示答案。进度(哪些课已通过、检查和提示各用了多少次)只保存在这个浏览器中,窗格上也如此注明。旁边链接了 *Quantum, reconsidered*,一门关于干涉与学习的课程。 + +## 语言 + +界面上的文字是数据:`locales//.json`,界面的每个部分一个文件,英文为源文本,简体中文与之并列(由 `locales/index.js` 汇集)。在你用页眉中的切换开关选定一种语言之前,Studio 跟随浏览器的语言;你的选择保存在这个浏览器中。宿主页面可以在挂载元素上用 `data-lang` 固定一种语言。引擎自身的消息(判定的陈述、诊断)保持引擎写出时的原样。中文文字为机器辅助翻译,在每个文件和页脚中均已注明,尚待熟练的读者审校。`node test/locales.test.mjs` 要求每种语言都具备每一个键,且占位符相同。 + +## 单步执行 + +线路下方的“单步执行”(Step through)在引擎的调试器上运行程序,每次执行一个门、一次测量或一次重置,在这个浏览器中进行(或在桌面上以原生方式进行)。它所用的方法对每个数值都是精确的:当每个角度都是 2π 的二进有理数倍时,用逐位精确(bit-exact)的态矢量,否则用双精度态矢量;在作业的噪声下则用密度矩阵,并在同一路径上给出无噪声态作对照。最多可单步执行 16 个量子比特,有噪声时最多 8 个;更宽的程序会被拒绝,并给出原因。 + +- **滑块**及其旁边的按钮用于前进和后退。后退时,程序从引擎保存的某个状态重新运行,因此同一步看两次得到的是相同的比特,其摘要也表明这一点。下一步会注明其所在的行,门则在线路图上标出。 +- **量子比特**(Qubits):每个量子比特的相位圆盘(填充表示读到 1 的概率,指针表示绕 Z 轴的相位,外环表示纯度)及其 Bloch 球(一幅从一侧看去的简单图),下方附有引擎给出的数值。旁边给出前一半量子比特与其余量子比特之间的纠缠。 +- **振幅**(Amplitudes):每个基态的振幅、概率和相位,概率最大的排在最前,每个都配有一个圆盘,其面积表示概率,指针表示相位。对于混合态,则给出概率。 +- **约化态**(Reduced state):你勾选的量子比特的密度矩阵,每个元素按其大小以深浅着色,并附有其纯度和熵。 +- **与无噪声态对照**(Against the noiseless state):有噪声时,把有噪声态的每个概率与无噪声态的相应概率并列,并给出两者之间的保真度。 +- **分支**(Branches):程序中途每次测量的每个结果,以树的形式给出,每个都附有其概率和权重:对于无噪声的 Clifford 程序,这些都是精确的分数。每片叶子给出该分支上最终测量的分布。选择一个分支,路径就会走上它;若未选择,每次测量都取概率较大的结果。 + +显示的每个数值都来自引擎;图形只负责把它们摆放出来。 + +## 通过线路图编辑 + +线路图上方有一个门面板。把一个门拖到某条线上,或者先选中它再点击某个位置:它会放到那条线上(双量子比特门放在那条线及其下一条线上),位于放置点右侧所画的各门之前。把已画出的门上下拖动,可把它移到其他线上;左右拖动,可在其他门之间移动它。用键盘时,按 Tab 移到某个门:↑ 和 ↓ 在线之间移动它,← 和 → 把它移到前一个门之前或后一个门之后,Delete 删除它(“删除门”(Delete gate)也可以)。 + +引擎通过语法树,在程序自身的文本中完成每一次编辑,因此程序保留其名称、排版和注释。编辑器把它记录为你的编辑,就像你亲手输入的一样。不是由一条语句单独产生的门(寄存器上一次广播中的一个门、程序自定义门中的一个门,或循环中的一个门)不能在线路图上移动或删除:Studio 会指出应修改的那一行。生成器的线路取决于其输入,因此要在其文本中编辑。 + +## 线路的文字描述与文件 + +线路图的描述(供屏幕阅读器朗读)是引擎对线路的叙述:先是其量子比特和比特,然后是每一列,每个门及其在每个量子比特上的角色,每次测量及其比特,以及按图例写法给出的每个条件。“SVG”按引擎写出的样子保存线路图,这是一张独立的图像,颜色随其所在位置的文字颜色而变;“LaTeX”则把它保存为一幅纯 TikZ 图,只需要 `\usepackage{tikz}`。 + +## 设备 + +在**设备**(Device)下选择一个设备目标:内置目标之一(合成的,采用已公开格式的形态)、在此导入的目标(**导入…**(Import…)读取引擎能读取的任何格式的校准快照或目标快照,保存在这个浏览器中),或者在桌面应用中,用 `wai-quantum targets import` 保存的目标。耦合图中,每个耦合对按其双量子比特门误差着色,每个量子比特按其读出误差、单量子比特门误差或 T1 着色。颜色为同一色调的五档,按该设备自身数值的分位数划分;最差的耦合对画得更粗,每个标记在悬停时给出其数值,**以表格显示**(As a table)则把它们列出。选定设备后,运行会被编译为该设备的门,放置到最可能成功的量子比特和路由上,并在其噪声下模拟;噪声字段暂不起作用,因为噪声取自设备本身。计划选定的布局会在运行之前画在耦合图上。结果中的**设备**区块列出目标及其哈希(写入回执)、放置方式及其预期成功率(与交换次数最少的放置方式的预期成功率并列)、噪声,以及计数与理想计数相差多少。这是设备的一个模型,结果也如此注明。在作业服务上,内置目标按名称传递,其他目标则以其文档传递。 + +## 生成器 + +编辑器用引擎自己的读取器读取程序,每个入口用的都是这一个读取器:它覆盖 OpenQASM 3.1 的全部内容,每个问题都在其所在行上用读取器的代码(`[Q103]`)标出;`engine_diagnostic_codes()` 给出每个代码的含义。 + +声明了整数输入(`input uint n;`)的程序就是一个生成器:它的 `for` 循环和针对整数的 `if` 会为每个取值写出一条线路。Studio 在编辑器下方为每个输入显示一个字段,并按这些取值读取程序。lint 和评析读取的是生成器本身,因此问题或问题模式保留在生成器自身的行上。线路图、计划、运行(在这里或在作业服务上)、视角(lens)和备选方案(Alternatives)使用的则是它写出的线路。这些取值按名称保存在这个浏览器中。 + +## 工作台 + +工作台(Workbench)在一族线路中搜索代价最低的成员,引擎评判每一个建议。编辑器中的程序是参照。规格说明了: + +- 每个成员必须做到什么:从 |0…0⟩ 制备出相同的态,或者是同一条线路; +- 要检查的规模(`n = 3, 5, 8`); +- 允许的门,以及门可以连接任意一对量子比特,还是只能连接一条直线上的相邻量子比特; +- 代价:评析各项度量的加权值,在各规模上求和; +- 图上绘制的两项度量。 + +“开始”(Start)在每个规模上检查参照,并以它为种子建立一个存档。存档在图的每个单元中保留代价最低的已验证生成器。有三个提出者向存档写入,每项发现都注明了是谁提出的: + +- **读者**:“提交编辑器中的程序”(Submit the editor's program); +- **内置的盲目搜索**:“搜索 ×100”(Search ×100),对存档中已有内容做带种子的编辑。它是一个想法必须胜过的基线; +- **模型**(在已连接模型时):“询问模型 ×3”(Ask the model ×3)。它会被告知规格,并看到已找到的代价最低者,以及引擎测得的代价。 + +一个建议只有在能被读取、遵守规则,并在每个规模上都通过检查时,才算通过验证。每个保留的条目都用这个浏览器的密钥封存(在桌面应用中则用应用的密钥)。点击一个点会显示它的代价、检查方式、封存签名及来源;“在编辑器中打开”(Open in editor)把它放进编辑器。存档保存在这个浏览器中,重新打开页面时会恢复。“下载”(Download)把规格及其存档并排保存(`NAME.json` 和 `NAME.archive.json`),这样 `wai-quantum discover verify NAME.json` 就能检查每一个封存签名。在桌面应用中,搜索还会统计所用的秒数及其进程消耗的焦耳数。 + +## 重写 + +线路下方的“重写”(Rewrites)列出程序所容许的、保持其含义不变的每一种重写:相互抵消的门、可以合并的旋转门、夹在两个 Hadamard 门之间而变为其共轭的门、越过与之对易的部分而移动的门、互换位置的两个门。引擎在每个重写所涉及的量子比特上检查它。指向某个重写,会在线路图上标出其涉及的门;“采纳”(Take)把它作为引擎的建议提出,与其他建议一样接受审阅;列表随程序的变化而更新。在备选方案中,与草稿是同一条线路的候选会说明草稿如何变成它:一串经过检查的重写。 + +## 编译 + +线路下方的“编译”(Compile)让程序依次经过引擎的各遍处理(每一遍都在其作用之处经过检查),在预算 `ε` 之内编译到 Clifford+T(或编译到标准门,不做任何近似),级别可为 0、1 或 2。每一遍是表中的一行:处理前后的门、CNOT、`T` 门和旋转门的数量,以及它的检查结果(`proof`、`exact`,或附有距离的 `approximate`)。表格上方的一行说明:输出与程序相差多远(不计全局相位)、预算及其在各旋转门之间的分配、在稠密酉矩阵上(最多 8 个量子比特)对照程序测得的整个输出的距离,以及用这个浏览器的密钥(在桌面应用中则用应用的密钥)封存的证书是否在此处验证通过。最后一遍之后的线路画在表格下方;选择某一遍,就画出该遍之后的线路。“使用”(Use)把编译后的程序作为引擎的建议提出,与其他建议一样接受审阅。程序一旦改动,该次编译就会被标记为过时。各遍、各级别以及如何理解 `ε`,见[编译器页面](../wai-quantum/docs/compiler.md)。 + +编译视角下方的“估算”(Estimate)说明一台容错量子计算机在所选的物理量子比特上、在给定的错误预算(允许这次运行失败的概率)之内运行该程序需要什么。它先给出程序的逻辑计数:逻辑量子比特(包括多控制门所需的辅助量子比特)、测量、`T` 门、Toffoli 门和旋转门、各旋转门合成所得门序列中的 `T` 门,以及 `T` 深度和总深度;然后给出每个旋转门在预算中所占的份额及其门序列用了多少个 `T` 门,以及用来选择码距的码模型(对于基于门的量子比特,拟合到引擎自己的精确匹配或并查集解码器,或采用平面模型所假定的模型)。估算本身是重点:物理量子比特数、`T` 工厂及其占用的量子比特份额、运行时间和码距。前沿图画出所有不被其他估算在物理量子比特数和运行时间两方面同时胜过的估算,横轴为运行时间、纵轴为量子比特数,均为对数刻度,并在一张表中列出它们;指向某个点或使其获得焦点,就会读出该点。每个估算都在资源估算回执族中用这个浏览器的密钥(在桌面应用中则用应用的密钥)封存,表格下方的一行说明这些封存签名是否验证通过,以及在此处重新运行请求是否能得出每一个数字。程序一旦改动,估算就会被标记为过时。每个数字及其假设,见[估算页面](../wai-quantum/docs/estimate.md)。 + +## 新颖之处与“发现” + +引擎会说明一个程序有什么新颖之处:它是被再次找到的某个已知构造;它经由另一条路径得到某个已知结果;或者它与所比较的程序中最近的那个相距多远。已知构造包括:以链式、倍增式和扇出式构造的 GHZ 态,均匀叠加态,以及量子傅里叶变换。这个距离是结构上的,从 0(构造相同)到 1(毫无共同之处),它由引擎给出,而不是文献检索的结果。 + +- **备选方案**:每张卡片都说明它相对已知构造和草稿有何新颖之处。经由另一条路径得到已知结果的卡片会被标出。 +- **工作台**:每个保留的生成器都标明了它是什么,其条目在显示它时还列出它在哪些代价未计入的度量上优于参照。只违反规格中的一条规则(某个门,或直线相邻的限制)、若去掉这条规则就能通过验证的候选,称为异常。它永远不会被保留:日志会说明它的代价是多少,而“去掉这条规则再试”(Try without that rule)会放宽规格,在放宽后的规格下建立一个新的存档。 +- **发现**(Found)保存值得再看一眼的内容:通往已知结果的另一条路径、异常、与所比较的一切都相距甚远的已验证条目。每一项都可以在编辑器中打开、附上备注保存,或者忽略。它的计数显示有多少项被标记、多少项被保存。它保存在这个浏览器中。 + +## 你自己的方式 + +在“版本”下,“你自己的方式”(Your own way)说明你的各个版本是否正在向模型的建议趋同:你最新的版本在构造方式上与它们相距多远,以及程序中有多少出自模型。“独自探索一段时间”(Explore on my own for a while)会在你工作期间暂停模型: + +- 助手只提问,不提供它的程序; +- 备选方案中的“探索”(Explore)和工作台中的“询问模型”暂停等待; +- 你创建的版本会被记为独自完成,这一行会把它们与建议之间的距离和其余版本的距离并列。 + +“结束”(End)让模型重新提出建议。这一模式保存在这个浏览器中。 + +## 研究 + +“研究”(Study)窗格让你参与一项关于人们如何借助引擎进行设计的研究,或者为自己保留一份记录: + +- **功能**(Features):Studio 的每项功能都可以关闭。在你参与研究期间,研究的条件会设定并锁定这些功能,以便每一项都能与对照组比较。 +- **载入研究方案**(Load a protocol):由研究者提供给你,在产生任何数据之前就已封存。在你作出选择之前,可以看到它研究的是什么。“参与”(Take part)根据这个浏览器的密钥分配你的条件,因此条件在你开始之前就已确定。 +- **经你要求后才保存,且只保存在这个浏览器中:** + - 你应用了“改进”(Improve)给出的哪些候选,以及引擎认为其中最好的那个; + - 你在工作台中打开了哪个保留的生成器; + - 你关于归属感的回答(“这有多少是你的?”,五个题项,每项从 1 到 7); + - 你在助手暂停时完成的探测任务,由引擎评分。 +- **我的记录**(My record):引擎对你所做的东西和所做的事的度量,每一项都根据它们重新计算。 +- **导出我的记录**(Export my record):用这个浏览器的密钥封存你的记录,保存为一个文件,交给研究者。不会发送任何内容。“退出研究”(Leave the study)会把这些功能交还给你。 + +## 你的品味 + +工作台中的“你的品味”(Your taste)从你自己的选择中学习你的偏好: + +- **比较两个**(Compare two)显示你的档案最拿不准的两个保留生成器。引擎会证明它们作用相同,因此这一选择关乎品味,而从不关乎正确性。“选这个”(This one)记下这次选择,“关于”(About)说明选择所依据的方面:经济性、清晰性、洞见、精确性或实用性。 +- **也来自备选方案**:与草稿是同一条线路的备选方案,若被整体采纳或整体拒绝,就构成两者之间的一次选择。只采纳了一部分的则不说明任何偏好。一旦档案中有了选择,“改进”就会按你的档案对其候选排序。 +- **按我的品味排序**(Rank by my taste)对保留的生成器排序。每个生成器都显示其典型度(它的构造与已知构造有多接近);档案从不学习典型度,只有旋钮能把排序从典型移向新颖。排序会指出某个方面或代价在哪里把另一个生成器排在首位,以及排在前三的生成器在构造上是否比按代价排在前三的更相似。 +- **档案**(Profiles):每个名称都是一份独立的档案,保存在这个浏览器中;没有任何东西会把它们合并。摘要说明档案预测对了你的多少次选择,每一次都仅根据之前的选择来预测。 + +## 版本 + +“版本”窗格记录程序的每一处改动及其作者: + +- 读者输入的内容归读者所有,在其停顿时、运行之前,以及其他任何东西改动程序之前记录下来; +- 建议归其提出者所有:助手的程序、备选方案(引擎的或模型的)、从工作台打开的生成器; +- 修正归引擎所有; +- 示例、文件、链接或较早的运行,则记为程序的来源。 + +**建议逐处采纳**。建议的改动连同其所在的行一一列出,每处都带一个复选框,在读者作出选择之前,程序保持不变。只有被采纳的改动才会进入程序。版本会保留完整的建议以及哪些改动被采纳,因此被拒绝的建议仍然可读。 + +**工作线**。各个版本被画成若干条工作线。“打开”(Open)把一个较早的版本放进编辑器,在它之上所做的改动会开始一条独立的工作线。“并入”(Bring in)把另一条工作线的末端版本对照程序逐处展示,被采纳的改动记录为两者的合并。 + +**出自谁之手**。“谁写的”(Who wrote it)在编辑器的行号栏中标出每一行、在线路图上标出每个门出自谁之手:读者、助手、某个智能体、引擎、其来源,或者模型写出后又经读者编辑的行,这样的行出自两者之手。窗格显示每位贡献者的门和行。 + +**在回执上**。运行的请求携带编辑器中那个版本的归属记录:它的摘要,以及出自每一方之手的门和行。回执将其绑定,结果则显示“作者”(written by),以及在此处根据各版本重新计算出的归属记录。各版本保存在这个浏览器中,每个都用浏览器的密钥封存(在桌面应用中则用应用的密钥)。“下载”保存 `program.qasm` 和 `program.history.json`,可用 `wai-quantum history verify program.qasm` 检查。 + +嵌入 Studio 的页面可以用 `window.quantumStudio.propose(qasm, {kind, name}, note)` 把一个程序作为某人的建议提出;读者作出选择后,它即完成(resolve)。 + +## 针对建议的规则 + +编辑器下方的“针对建议的规则”(Rules for proposals)规定任何建议都不得改动的内容: + +- **锁定的行**:“锁定选中的行”(Hold the selected lines)在程序中用 `// @lock` 和 `// @unlock` 标记它们。它们在编辑器中以底色显示,其中的门在线路图上标出。 +- **门**:建议可以添加的门,以及要避免的门。 +- **量子比特**:“锁定量子比特声明”(Hold the qubit declarations)。 +- **要避免的文本**(Text to avoid)。 +- **一个区域**:“把下一个建议限定在选中的行内”(Keep the next proposal to the selected lines),该建议一经决定即解除。 + +引擎会逐处对照这些规则检查每一个建议:助手的程序、备选方案、修正、来自工作台的生成器。违反规则的改动会在审阅中连同该规则一起显示,且不能被采纳。“版本”中的该版本把它记录为已阻止。每一处改动都违反规则的备选方案会被略去,并注明原因。每次请求都会把这些规则告诉模型。你自己输入的内容,你可以随意改动。 + +## 助手 + +“助手”(Assistant)窗格与这台机器上的语言模型对话:任何具有 chat-completions 接口和工具调用功能的服务器,例如 JouleClaw 运行时或 llama.cpp 服务器。地址填写在“模型…”(Model…)下,默认为 `http://127.0.0.1:8080/v1`。除非地址指向别处,否则没有任何内容离开这台机器。在桌面应用中,调用由应用自己发出,因此不会受到浏览器的源(origin)规则的阻碍。 + +引擎是模型的工具,而不是事后补上的附属品: + +- **它的工具就是 Studio 的引擎**:读取程序、lint、绘制、计划、运行,以及说明程序有何新颖之处。它的第一轮必须调用其中之一,因此每个回答都从引擎出发。只有当引擎这样说时,它才可以称一个程序为新颖的,而且只能以引擎给出的距离来表述,而不是文献检索的结果。 +- **它提出的每个程序在提供之前都会经过检查**。引擎在这里读取它。无法解析的程序会连同引擎给出的原因退回给模型一次,以便更正。能够解析的程序会按模型所用的设置运行,因此它引用的计数就是显示的计数。然后“采纳”(Apply)把它放进编辑器。 +- **每个建议都以徽章显示其判定**:引擎检查它所达到的级别(被拒绝、已读取、已运行、作为证据检查、作为证明检查),并附上引擎的陈述。模型被告知要给 `run_job` 提供一个意图并引用判定,绝不能超出判定去称一个程序有效。 +- **它只能陈述引擎返回的数字**。它的文字中若有任何工具都未返回过的数字,会被标为“不是来自运行”(Not from a run)。 +- **回答是纯文本**。模型若仍写出 LaTeX,会以 Unicode 显示。 + +`node test/assistant-live.mjs URL` 让助手对接一个真实的模型服务器运行,并打印它做了什么。它不属于 CI,因为 CI 中没有模型。 + +## 托管 + +用 `npm ci && node build.mjs` 构建。它会写出 `dist/`: + +| 文件 | 内容 | +|---|---| +| `studio.js` | 打包后的界面 | +| `worker.js` | 引擎的 worker | +| `studio.css` | Studio 的样式 | +| `tokens.css` | 站点的设计令牌,供 Studio 独立运行时使用 | +| `index.html` | 独立运行的页面 | + +宿主页面把 Studio 挂载到一个元素上: + +```html +
+ +``` + +`data-embedded` 告诉 Studio 宿主页面有自己的标题,这样 Studio 的名称就不会成为第二个 `h1`。`data-lang`(`en` 或 `zh-CN`)固定语言。`data-engine-pkg` 指定引擎的浏览器包:以 `quantum_engine` 特性构建的 wai-web。`node build.mjs --pkg DIR` 把这样的包复制到 `dist/pkg/` 中,供 `index.html` 使用。在桌面上,存在 `window.__TAURI__`,Studio 会改用原生引擎。 + +## 测试 + +```sh norun=ci:wai-quantum-studio.yml +node build.mjs --pkg PATH/TO/PKG && node test/studio.test.mjs +``` + +测试在已安装的浏览器中运行 Studio: + +- 在输入程序时进行 lint、绘制和计划; +- 在浏览器中运行,并在浏览器中检查其回执; +- 错误标在其所在的行上; +- 在态矢量上运行一个旋转门; +- 同一个作业在线上的作业服务上运行,给出与这里运行相同的计数,逐次一致; +- Ctrl+Enter; +- 程序和历史在重新载入后保留; +- 估算窗格:三控制门经由一个辅助量子比特计数,在所选量子比特上的估算,前沿的绘制与列表,其封存签名的验证,以及在页面中重新运行请求; +- 助手,对接一个按脚本应答的模型服务器:在引擎上执行的工具调用,引擎无法读取的建议连同原因被退回,更正后的建议在提供之前经过检查和运行,以及“采纳”; +- 用逐位精确的方法单步执行量子隐形传态:滑块前进再后退回到相同的比特,下一步的注明和标记,每个量子比特的 Bloch 球和相位圆盘,振幅,一个约化态,各占四分之一的四个分支,选定其中一个及其留下的态,以及有噪声态与无噪声态的对照; +- 通过线路图编辑:从门面板拖放到某条线上的门,拖到另一条线上的门,用键盘移动和删除的门,每一项都记录为你的编辑,以及广播中的一个门被拒绝并给出其所在行; +- 用引擎的措辞描述的线路,以及保存为 SVG 和 LaTeX 的线路图; +- 英文的课程:载入并检查一课的起始程序,哪里出了问题,附带引擎指引的提示,答案通过并保存在浏览器中,一条规则和一个生成器的各种情况各自附有判定; +- 根据浏览器语言显示的中文课程,切换到英文再切换回来,以及打开课程时在手机宽度下的布局; +- 手机宽度下的布局。 + +`STUDIO_SKIP_SERVICE=1` 会跳过作业服务这一步。测试会把两种配色方案的截图保存在 `test/screenshots/` 中。 diff --git a/wai/quantum-studio/build-views.mjs b/wai/quantum-studio/build-views.mjs new file mode 100644 index 000000000..cf347e438 --- /dev/null +++ b/wai/quantum-studio/build-views.mjs @@ -0,0 +1,124 @@ +// Build the views an agent host draws (src/views/) into one self-contained +// page: the view's script (the Studio's components, bundled), the Studio's +// styles, and the engine inside the view (wai/wai-quantum-views) twice, as +// WebAssembly (base64) and as the same build translated to JavaScript. The page +// loads nothing and connects nowhere. The servers serve it for each of the six +// views, naming the view in its . +// +// node build-views.mjs --wasm VIEWS.wasm --js VIEWS.mjs --out DIR +// +// writes DIR/wai-quantum-views.html and DIR/manifest.json (each part's size and +// SHA-256, and every source the page is built from with its SHA-256). Run it +// through wai/wai-quantum-views/build.sh, which builds the two engines first. +// +// node build-views.mjs --check +// +// says whether the committed page (wai/wai-quantum/views/) is built from the +// sources as they are: every source it names unchanged, and no source added. +import { build, transform } from 'esbuild'; +import { createHash } from 'node:crypto'; +import { mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'; +import { dirname, join, relative } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const here = dirname(fileURLToPath(import.meta.url)); +const repo = join(here, '..', '..'); +const arg = (k) => { const i = process.argv.indexOf(k); return i > 0 ? process.argv[i + 1] : null; }; +const [wasmPath, jsPath, out] = [arg('--wasm'), arg('--js'), arg('--out')]; +const checking = process.argv.includes('--check'); +if (!checking && (!wasmPath || !jsPath || !out)) { + console.error('usage: node build-views.mjs --wasm VIEWS.wasm --js VIEWS.mjs --out DIR'); + process.exit(2); +} +// The most a view may be: the servers' tests hold every view to it. +export const LIMIT = 1 << 20; +// The name the servers replace with the view's. +export const PLACEHOLDER = '__WAI_QUANTUM_VIEW__'; + +const sha = (b) => createHash('sha256').update(b).digest('hex'); +const bytes = (s) => Buffer.byteLength(s, 'utf8'); + +// The views are in English: the Studio's components they share read their +// words through src/i18n.js, and here its locales are English alone, so the +// page carries no other language's words. +const englishOnly = { + name: 'english-only', + setup(b) { + b.onResolve({ filter: /\/locales\/index\.js$/ }, () => ({ path: 'english-only', namespace: 'wai-views-locales' })); + b.onLoad({ filter: /.*/, namespace: 'wai-views-locales' }, () => { + const dir = join(here, 'locales', 'en'); + const areas = readdirSync(dir).filter((f) => f.endsWith('.json')).sort(); + const names = areas.map((f, i) => `en${i}`); + return { + resolveDir: dir, + loader: 'js', + contents: `${areas.map((f, i) => `import ${names[i]} from './${f}';`).join('\n')}\nexport const AREAS = ${JSON.stringify(areas.map((f) => f.replace(/\.json$/, '')))};\nexport default { en: Object.assign({}, ${names.join(', ')}) };\n`, + }; + }); + }, +}; +// The view's script, and every module it bundles. +const app = await build({ absWorkingDir: here, entryPoints: [join(here, 'src/views/main.js')], bundle: true, format: 'esm', target: ['es2022'], minify: true, legalComments: 'none', write: false, metafile: true, logLevel: 'warning', plugins: [englishOnly] }); +const script = app.outputFiles[0].text; +const styleFiles = ['src/tokens.css', 'src/studio.css', 'src/views/views.css']; +// Every source, relative to the repository. +const sourcesOf = (inputs) => [ + ...Object.keys(inputs).filter((p) => !p.startsWith('wai-views-locales:')).map((p) => join(here, p)), + ...styleFiles.map((f) => join(here, f)), + join(here, 'build-views.mjs'), + ...['src/lib.rs', 'Cargo.toml', 'Cargo.lock', 'build.sh'].map((f) => join(repo, 'wai/wai-quantum-views', f)), +].map((p) => relative(repo, p).split('\\').join('/')).sort(); +if (checking) { + const committed = JSON.parse(readFileSync(join(repo, 'wai/wai-quantum/views/manifest.json'), 'utf8')); + const now = sourcesOf(app.metafile.inputs); + const stale = now.filter((p) => committed.sources[p] !== sha(readFileSync(join(repo, p)))); + const gone = Object.keys(committed.sources).filter((p) => !now.includes(p)); + if (stale.length || gone.length) { + console.error(`the committed views page is not built from these sources: ${[...stale.map((p) => `${p} changed`), ...gone.map((p) => `${p} no longer used`)].join('; ')}. Rebuild it: bash wai/wai-quantum-views/build.sh`); + process.exit(1); + } + console.log(`the committed views page is built from these ${now.length} sources`); + process.exit(0); +} +// The engine as JavaScript, made a factory: it runs only when a host's policy refuses the WebAssembly. +const engineJs = (await transform(readFileSync(jsPath, 'utf8'), { format: 'iife', globalName: '__wqEngine', minify: true, legalComments: 'none', target: 'es2022' })).code; +const factory = `globalThis.wqEngineJs=function(){${engineJs};return __wqEngine};`; +const wasm = readFileSync(wasmPath); +const css = (await transform(styleFiles.map((f) => readFileSync(join(here, f), 'utf8')).join('\n'), { loader: 'css', minify: true, legalComments: 'none' })).code; + +// Text inside a + + + +`; +if (html.split(PLACEHOLDER).length !== 2) throw new Error('the page must name its view in one place'); +if (bytes(html) > LIMIT) throw new Error(`the page is ${bytes(html)} bytes, past the limit of ${LIMIT}`); + +const sources = sourcesOf(app.metafile.inputs); +const manifest = { + format: 'wai.quantum.views-bundle/1', + about: 'The page every view of WAI Quantum is served from, the view named in its . Built by wai/wai-quantum-views/build.sh; do not edit it by hand.', + placeholder: PLACEHOLDER, + limit_bytes: LIMIT, + bytes: { page: bytes(html), script: bytes(script), styles: bytes(css), engine_wasm: wasm.length, engine_wasm_base64: Math.ceil(wasm.length / 3) * 4, engine_js: bytes(factory) }, + sha256: { page: sha(html), script: sha(script), styles: sha(css), engine_wasm: sha(wasm), engine_js: sha(factory) }, + sources: Object.fromEntries(sources.map((p) => [p, sha(readFileSync(join(repo, p)))])), +}; +mkdirSync(out, { recursive: true }); +writeFileSync(join(out, 'wai-quantum-views.html'), html); +writeFileSync(join(out, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n'); +console.log(`wrote ${join(out, 'wai-quantum-views.html')}: ${bytes(html)} bytes (script ${bytes(script)}, styles ${bytes(css)}, engine ${wasm.length} as WebAssembly and ${bytes(factory)} as JavaScript)`); diff --git a/wai/quantum-studio/locales/en/assistant.json b/wai/quantum-studio/locales/en/assistant.json new file mode 100644 index 000000000..8fb47af80 --- /dev/null +++ b/wai/quantum-studio/locales/en/assistant.json @@ -0,0 +1,14 @@ +{ + "_meta": { + "locale": "en", + "area": "assistant", + "translation": "source" + }, + "assistant.stance_questions": "Ask me questions first", + "assistant.stance_suggest": "Suggest changes", + "assistant.stance_write": "Write programs", + "assistant.model_answered": "the model answered {status}", + "assistant.server_answered": "the model server answered {status}", + "assistant.no_answer": "the model gave no answer", + "assistant.kept_calling": "the model kept calling tools without answering" +} diff --git a/wai/quantum-studio/locales/en/circuit_svg.json b/wai/quantum-studio/locales/en/circuit_svg.json new file mode 100644 index 000000000..6b427fada --- /dev/null +++ b/wai/quantum-studio/locales/en/circuit_svg.json @@ -0,0 +1,10 @@ +{ + "_meta": { + "locale": "en", + "area": "circuit_svg", + "translation": "source" + }, + "circuit_svg.label": "The circuit: {qubits} qubits, {gates} operations in {columns} columns", + "circuit_svg.cell": "{gate} on q{q}", + "circuit_svg.cell_sep": ", " +} diff --git a/wai/quantum-studio/locales/en/compile_view.json b/wai/quantum-studio/locales/en/compile_view.json new file mode 100644 index 000000000..304652555 --- /dev/null +++ b/wai/quantum-studio/locales/en/compile_view.json @@ -0,0 +1,43 @@ +{ + "_meta": { + "locale": "en", + "area": "compile_view", + "translation": "source" + }, + "compile_view.col_gates": "gates", + "compile_view.col_cnots": "CNOTs", + "compile_view.col_rotations": "rotations", + "compile_view.basis_label": "Basis", + "compile_view.basis_title": "The gate set to compile to. Clifford+T: every rotation written as a Clifford+T word within ε. Standard: the standard gates at any angle, nothing approximated", + "compile_view.standard": "standard", + "compile_view.level_label": "Level", + "compile_view.level_title": "0: lower and approximate; 1: also checked rewrites and phase folding; 2: also one- and two-qubit resynthesis", + "compile_view.level_option": "level {level}", + "compile_view.eps_title": "The most the output may differ from the program: the operator norm, up to a global phase, shared by its rotations", + "compile_view.compile": "Compile", + "compile_view.compile_title": "The program through the engine’s passes, each checked, with a signed certificate", + "compile_view.heading": "Compile", + "compile_view.caption": "After {pass} (pass {number} of {of}): {gates} gates, {cnots} CNOTs, {t} T, {rotations} rotations{partial}.", + "compile_view.caption_partial": "; the first {drawn} of {total} columns drawn", + "compile_view.draw_title": "Draw the circuit after this pass", + "compile_view.spent_one": "{n} rotation, {each} each", + "compile_view.spent_other": "{n} rotations, {each} each", + "compile_view.nothing_approximated": "nothing approximated", + "compile_view.whole": " The whole output measured against the program: {distance}.", + "compile_view.use": "Use", + "compile_view.use_title": "Offer the compiled program for review", + "compile_view.offer_note": "Compiled to {basis} at level {level}: within {bound} of the program (budget {budget}), {t} T gates; every pass checked.", + "compile_view.the_standard_gates": "the standard gates", + "compile_view.verdict_approximate": "Approximate", + "compile_view.verdict_exact": "Exact", + "compile_view.verdict_proved": "Proved", + "compile_view.summary": "{verdict}: within {bound} of the program, up to a global phase (budget {budget}; {spent}).{whole} {certificate}", + "compile_view.cert_verifies": "The certificate verifies.", + "compile_view.cert_fails": "The certificate DOES NOT VERIFY.", + "compile_view.col_pass": "pass", + "compile_view.col_check": "check", + "compile_view.note": "Choose a pass to draw the circuit after it. A check is proof (exact ring arithmetic, a phase polynomial in whole eighths), exact (equal to the rounding of doubles) or approximate with its distance; the distances add up to the bound.", + "compile_view.bad_eps": "ε is a positive number, such as 1e-6.", + "compile_view.compiling": "Compiling: each pass checked as it goes…", + "compile_view.stale": "For an earlier version of the program: compile again." +} diff --git a/wai/quantum-studio/locales/en/converge.json b/wai/quantum-studio/locales/en/converge.json new file mode 100644 index 000000000..a84b5bcaa --- /dev/null +++ b/wai/quantum-studio/locales/en/converge.json @@ -0,0 +1,19 @@ +{ + "_meta": { + "locale": "en", + "area": "converge", + "translation": "source" + }, + "converge.converging": "Converging: {why}.", + "converge.latest": "Your latest version is {distance} from the model’s proposals in how it is built.", + "converge.own_one": "On your own: {n} version, {distance} from them{rest}.", + "converge.own_many": "On your own: {n} versions, {distance} from them{rest}.", + "converge.own_rest": "; the rest {distance}", + "converge.try_own": "Try a while on your own: the assistant then only asks.", + "converge.chip": "on your own", + "converge.title": "Your own way", + "converge.end": "End: let the assistant propose again", + "converge.start": "Explore on my own for a while", + "converge.end_title": "The assistant, Alternatives and the Workbench may ask the model again", + "converge.start_title": "The assistant only asks; Alternatives and the Workbench do not ask the model; your versions are noted as made on your own" +} diff --git a/wai/quantum-studio/locales/en/course.json b/wai/quantum-studio/locales/en/course.json new file mode 100644 index 000000000..13f1946a1 --- /dev/null +++ b/wai/quantum-studio/locales/en/course.json @@ -0,0 +1,47 @@ +{ + "_meta": { + "locale": "en", + "area": "course", + "translation": "source" + }, + "course.title": "Course", + "course.open": "Course", + "course.open_title": "The circuits course: lessons with exercises the engine checks", + "course.lessons": "The lessons", + "course.where": "Lesson {number} of {of} · {unit}", + "course.exercise": "Exercise", + "course.starter": "Load the starter", + "course.starter_title": "Put the exercise’s starter program in the editor (your draft stays in Versions)", + "course.starter_loaded": "The starter program of “{title}” is in the editor.", + "course.check": "Check", + "course.check_title": "Check the program in the editor against the exercise, with the engine", + "course.hint": "Hint", + "course.no_more_hints": "No more hints", + "course.hint_of": "Hint {level} of {of}", + "course.why": "What went wrong?", + "course.ask": "Ask the assistant for a hint", + "course.ask_title": "The model on this machine, told not to write the program; any program in its answer is held back", + "course.ask_needs_model": "The assistant needs a model: connect one under Assistant, then ask again.", + "course.asking": "Asking the assistant…", + "course.assistant_says": "The assistant’s hint", + "course.held_solution": "The answer held a program that passes this exercise. It is not shown, so that the exercise stays yours.", + "course.held_code": "The answer held code, which is not shown here.", + "course.ask_failed": "The assistant could not answer: {error}", + "course.not_checked": "not checked", + "course.passed": "passed", + "course.not_yet": "not yet", + "course.passed_status": "Passed: “{title}”.", + "course.not_yet_status": "Not yet: What went wrong? says what the engine found.", + "course.engine_says": "The engine:", + "course.nothing_wrong": "Nothing went wrong: the check passes.", + "course.where_to_look": "Where to look:", + "course.previous": "← Previous", + "course.next": "Next →", + "course.progress": "{passed} of {of} lessons passed", + "course.kept": "kept only in this browser", + "course.reset": "Reset progress", + "course.reset_confirm": "Click again to reset", + "course.reset_done": "Your progress in this browser is cleared.", + "course.companion": "Beside it:", + "course.machine_assisted": "This lesson is a machine-assisted translation, to be reviewed by a fluent reader." +} diff --git a/wai/quantum-studio/locales/en/critique_view.json b/wai/quantum-studio/locales/en/critique_view.json new file mode 100644 index 000000000..cde21d668 --- /dev/null +++ b/wai/quantum-studio/locales/en/critique_view.json @@ -0,0 +1,24 @@ +{ + "_meta": { + "locale": "en", + "area": "critique_view", + "translation": "source" + }, + "critique_view.qubits": "qubits", + "critique_view.gates": "gates", + "critique_view.two_qubit": "two-qubit", + "critique_view.depth": "depth", + "critique_view.two_qubit_depth": "two-qubit depth", + "critique_view.non_clifford": "non-Clifford", + "critique_view.since_last_run": "since the last run", + "critique_view.fidelity_title": "The {note}. Rates: {one_qubit} per one-qubit gate, {two_qubit} per two-qubit gate, {readout} per measured qubit.", + "critique_view.fidelity_modelled": "fidelity, modelled", + "critique_view.line": "line {line}", + "critique_view.lines": "lines {lines}", + "critique_view.label": "The engine’s critique", + "critique_view.waiting": "The critique waits for a program the engine can read.", + "critique_view.clifford_title": "Every gate is a Clifford gate: the stabilizer tableau runs it exactly, at any width.", + "critique_view.show": "Show", + "critique_view.fix": "Fix", + "critique_view.fix_title": "The engine rewrites these lines and checks the result is the same circuit" +} diff --git a/wai/quantum-studio/locales/en/debugger.json b/wai/quantum-studio/locales/en/debugger.json new file mode 100644 index 000000000..593976237 --- /dev/null +++ b/wai/quantum-studio/locales/en/debugger.json @@ -0,0 +1,46 @@ +{ + "_meta": { + "locale": "en", + "area": "debugger", + "translation": "source" + }, + "debugger.start": "Step through", + "debugger.start_title": "Step through the program on the engine, forward and back", + "debugger.to_start": "To the start", + "debugger.back": "One step back", + "debugger.forward": "One step forward", + "debugger.to_end": "To the end", + "debugger.slider": "The step the state is shown at", + "debugger.not_started": "not started", + "debugger.pick": "The qubits whose reduced state to show", + "debugger.against_noiseless": "Against the noiseless state", + "debugger.qubits": "Qubits", + "debugger.amplitudes": "Amplitudes", + "debugger.reduced": "Reduced state", + "debugger.branches": "Branches", + "debugger.title": "Step through", + "debugger.empty": "Step through the program on the engine: the state at each step, each qubit on its Bloch sphere, and every branch of its measurements with its weight.", + "debugger.refused": "refused", + "debugger.position": "position {position} of {length}", + "debugger.line": " (line {line})", + "debugger.next": "Next: ", + "debugger.end": "The end of the program.", + "debugger.just_now": " · just now: {kind} q{qubit}{line} read {outcome} with p {p} ({by})", + "debugger.by_recorded": "the branch chosen", + "debugger.by_likelier": "the likelier", + "debugger.by_certain": "certain", + "debugger.path": "Path: {outcomes} · weight {weight} · bits {bits}{read}", + "debugger.outcomes": "outcomes {outcomes}", + "debugger.no_measurement": "no measurement yet", + "debugger.no_bits": "none", + "debugger.entangled": "Entangled across q{cut} | the rest: {bits} bits (Schmidt rank {rank}).", + "debugger.negativity": "Across q{cut} | the rest: logarithmic negativity {value} bits.", + "debugger.final": "final: {final}{more}", + "debugger.more": " and {n} more", + "debugger.reads": "reads {outcome} · p {p} · weight {weight}", + "debugger.branch_count_one": "{n} branch; weights {weights}.", + "debugger.branch_count": "{n} branches; weights {weights}.", + "debugger.tree_label": "The branches at the program’s measurements in the middle", + "debugger.one_path": "No measurement in the middle: one path; {final}.", + "debugger.again": "Again" +} diff --git a/wai/quantum-studio/locales/en/device.json b/wai/quantum-studio/locales/en/device.json new file mode 100644 index 000000000..bf3bdd2b2 --- /dev/null +++ b/wai/quantum-studio/locales/en/device.json @@ -0,0 +1,60 @@ +{ + "_meta": { + "locale": "en", + "area": "device", + "translation": "source" + }, + "device.by_readout": "readout error", + "device.by_one": "one-qubit error", + "device.by_t1": "T1 (shorter is worse)", + "device.map_label": "{name}: {qubits} qubits and {pairs} coupled pairs, each pair coloured by its two-qubit error and each qubit by its {by}, in five bins from lowest to highest{placed}", + "device.map_placed": "; the program placed on physical qubits {layout}", + "device.pair_tip": "pair {a}–{b}: {gate} error {error}", + "device.two_qubit_gate": "two-qubit gate", + "device.qubit_tip": "qubit {qubit}{label}: readout error {readout}, one-qubit error {single}, T1 {t_one} µs, T2 {t_two} µs", + "device.qubit_label": " ({label})", + "device.ends": "q{logical} ends", + "device.legend_pairs": "pairs: two-qubit error", + "device.legend_qubits": "qubits: {by}", + "device.legend_note": "Each scale is this device’s own: five bins by quantile, lowest first. A dashed mark states no value. Hover a qubit or a pair for its numbers.", + "device.as_table": "As a table", + "device.th_qubit": "qubit", + "device.th_readout": "readout", + "device.th_error_1q": "1q error", + "device.th_pair": "pair", + "device.th_error": "error", + "device.th_gate": "gate", + "device.target_label": "Device target", + "device.none": "None: the engine’s own methods", + "device.colour_by": "Colour qubits by", + "device.import": "Import…", + "device.import_title": "A device’s calibration or target snapshot, in any format the engine reads", + "device.title": "Device", + "device.run_as_on": "Run as on", + "device.about": "{qubits} qubits, {pairs} pairs · compiles to {basis}{costed} · calibration {calibration}{when} · {source} · hash {hash}…", + "device.costed_own": ", each gate costed as the device’s own", + "device.unnamed": "unnamed", + "device.when": ", {timestamp}", + "device.built_in": "Built in (synthetic)", + "device.option": "{name} · {qubits} qubits", + "device.yours": "Yours (wai-quantum targets)", + "device.imported_here": "Imported here", + "device.imported_app": "Imported {name} ({format}): {qubits} qubits, kept in this app.", + "device.imported_browser": "Imported {name} ({format}): {qubits} qubits, kept in this browser.", + "device.import_failed": "The snapshot did not read: {error}", + "device.placed": "Placed on {layout}{ending}; {swaps} swaps; expected success {success} against {fewest} for the fewest swaps{noise}.", + "device.ending_on": ", ending on {layout}", + "device.placed_noise": "; {channels} noise channels over {ns} ns", + "device.row_target": "target", + "device.name_calibration": "{name} · calibration {calibration}", + "device.row_hash": "hash", + "device.row_placed": "placed", + "device.placed_row": "on {layout} → {final}; {swaps} swaps; {gates} gates in {basis}", + "device.row_success": "expected success", + "device.success_row": "{success} (fewest swaps: {fewest})", + "device.row_noise": "noise", + "device.noise_row": "{channels} channels over {ns} ns, {idle} ns idle", + "device.row_ideal": "from ideal", + "device.ideal_row": "total variation distance {tvd}, Hellinger fidelity {fidelity}", + "device.model_note": "A model of the device, not a run on it: {model}" +} diff --git a/wai/quantum-studio/locales/en/dom.json b/wai/quantum-studio/locales/en/dom.json new file mode 100644 index 000000000..26b9c2fa1 --- /dev/null +++ b/wai/quantum-studio/locales/en/dom.json @@ -0,0 +1,14 @@ +{ + "_meta": { + "locale": "en", + "area": "dom", + "translation": "source" + }, + "dom.not_checked": "not checked", + "dom.how_tableau": "equal tableaus", + "dom.how_exact": "on every basis state", + "dom.how_rewrites": "by checked rewrites", + "dom.how_random": "on random inputs", + "dom.same_circuit": "same circuit · {how}", + "dom.different_circuit": "a different circuit" +} diff --git a/wai/quantum-studio/locales/en/estimate_view.json b/wai/quantum-studio/locales/en/estimate_view.json new file mode 100644 index 000000000..1ca0cac5a --- /dev/null +++ b/wai/quantum-studio/locales/en/estimate_view.json @@ -0,0 +1,63 @@ +{ + "_meta": { + "locale": "en", + "area": "estimate_view", + "translation": "source" + }, + "estimate_view.qubit_ns_e4": "ns-e4 · 50 ns gates, 1e-4", + "estimate_view.qubit_ns_e3": "ns-e3 · 50 ns gates, 1e-3", + "estimate_view.qubit_us_e4": "us-e4 · 100 µs gates, 1e-4", + "estimate_view.qubit_us_e3": "us-e3 · 100 µs gates, 1e-3", + "estimate_view.qubit_meas_e4": "meas-e4 · measurement-based, 1e-4", + "estimate_view.qubit_meas_e6": "meas-e6 · measurement-based, 1e-6", + "estimate_view.days": "{n} days", + "estimate_view.years": "{n} years", + "estimate_view.frontier_label_one": "The frontier: {n} estimate, runtime against physical qubits on log scales", + "estimate_view.frontier_label_other": "The frontier: {n} estimates, runtime against physical qubits on log scales", + "estimate_view.runtime": "runtime", + "estimate_view.physical_qubits": "physical qubits", + "estimate_view.point_label": "slowdown {slowdown}: {qubits} physical qubits, {runtime}, distance {distance}, {factories} factories", + "estimate_view.point_title": "slowdown {slowdown}: {qubits} physical qubits, {runtime}", + "estimate_view.the_estimate": "the estimate", + "estimate_view.qubit_label": "Physical qubit", + "estimate_view.qubit_title": "The physical qubit the machine is built of", + "estimate_view.budget_aria": "Error budget", + "estimate_view.budget_title": "The probability the run may fail, shared by logical errors, distillation and the rotations’ synthesis", + "estimate_view.model_label": "Code model", + "estimate_view.model_title": "The gate-based surface code’s model: fitted to this engine’s exact-matching or union-find decoder, or the planar model’s assumed one", + "estimate_view.fit_exact_matching": "fit: exact matching", + "estimate_view.fit_union_find": "fit: union-find", + "estimate_view.assumed_model": "assumed model", + "estimate_view.estimate": "Estimate", + "estimate_view.estimate_title": "What a fault-tolerant machine would need to run this program, each estimate signed", + "estimate_view.heading": "Estimate", + "estimate_view.budget": "budget", + "estimate_view.ancillas_one": " ({n} ancilla)", + "estimate_view.ancillas_other": " ({n} ancillas)", + "estimate_view.depths": "; T depth {t_depth}, depth {depth}", + "estimate_view.counts": "{logical} logical qubits{ancillas}, {measurements} measurements, {t} T, {toffolis} Toffolis, {rotations} rotations ({rotation_t} T in their words){depths}{bound}.", + "estimate_view.bound_upper": "; the program branches, so these bound every run", + "estimate_view.synth": "Each rotation within {each} (the synthesis share {synthesis} of the budget {budget}), {per} T apiece.", + "estimate_view.synth_none": "Budget {budget}; nothing to synthesise.", + "estimate_view.calibration": "Code distance from P(d) = a·(p/p*)^((d+1)/2) with a = {prefactor}, p* = {threshold}, fitted to this engine’s {label} decoder{extrapolated}.", + "estimate_view.extrapolated": ", extrapolated past the measured p = 0.002–0.005", + "estimate_view.factories_one": "{n} factory", + "estimate_view.factories_other": "{n} factories", + "estimate_view.readout": "{which}: {qubits} physical qubits ({factories}), {runtime}, code distance {distance}.", + "estimate_view.the_estimate_cap": "The estimate", + "estimate_view.slowed": "Slowed ×{slowdown}", + "estimate_view.col_slowdown": "slowdown", + "estimate_view.col_d": "d", + "estimate_view.col_factories": "factories", + "estimate_view.this_key": "this key", + "estimate_view.verified_one": "{n} estimate sealed with {signer}: the seals verify, and the request re-run here gives every figure.", + "estimate_view.verified_other": "{n} estimates sealed with {signer}: the seals verify, and the request re-run here gives every figure.", + "estimate_view.not_verified": "The seals DO NOT VERIFY here (signed {signed}, receipts {receipts}, reproduced {reproduced}).", + "estimate_view.t_factories_one": "{n} T factory", + "estimate_view.t_factories_other": "{n} T factories", + "estimate_view.summary": "On {qubit}: {qubits} physical qubits ({factories}, {share}% of them) for {runtime}, at code distance {distance} ({code}).", + "estimate_view.model_note": "An estimate of a model: the planar model’s assumptions (uniform independent noise, free transport of T states, smooth T consumption) hold for every figure here. Slowing the program needs fewer factories; the frontier keeps each estimate that none of the others beats on both.", + "estimate_view.bad_budget": "The budget is a probability between 0 and 1, such as 1e-3.", + "estimate_view.estimating": "Estimating: counting the program, synthesising each rotation, searching the factories…", + "estimate_view.stale": "For an earlier version of the program: estimate again." +} diff --git a/wai/quantum-studio/locales/en/examples.json b/wai/quantum-studio/locales/en/examples.json new file mode 100644 index 000000000..69a198ca5 --- /dev/null +++ b/wai/quantum-studio/locales/en/examples.json @@ -0,0 +1,21 @@ +{ + "_meta": { + "locale": "en", + "area": "examples", + "translation": "source" + }, + "examples.bell_title": "Bell pair", + "examples.bell_shows": "Two qubits entangled: only 00 and 11 come up, and ⟨Z0 Z1⟩ is exactly 1.", + "examples.ghz_title": "GHZ state, 40 qubits", + "examples.ghz_shows": "A Clifford circuit past any state vector, run exactly on the stabilizer tableau: all zeros or all ones.", + "examples.rotation_title": "A rotation", + "examples.rotation_shows": "Any angle runs on the double-precision state vector: ⟨Z0⟩ = cos 0.3 and ⟨Y0⟩ = −sin 0.3.", + "examples.qft_title": "Quantum Fourier transform", + "examples.qft_shows": "The Fourier transform of a basis state on three qubits: every outcome equally likely, the information in the amplitudes' phases.", + "examples.teleport_title": "Teleportation", + "examples.teleport_shows": "A state moved from qubit 0 to qubit 2 by measurement and correction: ⟨Z2⟩ = cos 0.7, ⟨X2⟩ = sin 0.7.", + "examples.grover_title": "Grover search, 3 qubits", + "examples.grover_shows": "Two rounds of amplitude amplification find the marked item 101 with probability 0.945.", + "examples.noisy_bell_title": "A noisy Bell pair", + "examples.noisy_bell_shows": "The same pair on a device whose gates go wrong at random: 01 and 10 appear, and ⟨Z0 Z1⟩ falls below 1." +} diff --git a/wai/quantum-studio/locales/en/flags.json b/wai/quantum-studio/locales/en/flags.json new file mode 100644 index 000000000..62fbee664 --- /dev/null +++ b/wai/quantum-studio/locales/en/flags.json @@ -0,0 +1,21 @@ +{ + "_meta": { + "locale": "en", + "area": "flags", + "translation": "source" + }, + "flags.critique": "the engine’s critique on the live diagram", + "flags.assistant_asks": "the assistant asks before it proposes, and speaks only when asked", + "flags.alternatives": "Alternatives: improve and explore, on the engine’s axes", + "flags.predict_first": "predict before a run", + "flags.workbench": "the Workbench", + "flags.rules": "rules for proposals", + "flags.shortcuts": "the classical-shortcut lens", + "flags.novelty": "what is new: labels and Found", + "flags.taste": "your taste", + "flags.rewrites": "the rewrite lens", + "flags.convergence": "your own way", + "flags.note_locked": "The study you take part in sets the ones it names, so that each can be compared against a control; leave the study to change those.", + "flags.note": "Turn any feature off; the Studio keeps your choice in this browser.", + "flags.title": "Features" +} diff --git a/wai/quantum-studio/locales/en/frame.json b/wai/quantum-studio/locales/en/frame.json new file mode 100644 index 000000000..e1c9666bc --- /dev/null +++ b/wai/quantum-studio/locales/en/frame.json @@ -0,0 +1,188 @@ +{ + "_meta": { + "locale": "en", + "area": "frame", + "translation": "source" + }, + "method.exact": "Exact state vector", + "method.statevector": "State vector, double precision", + "method.statevector_single": "State vector on the GPU, single precision", + "method.stabilizer": "Stabilizer tableau", + "method.pauli": "Sparse Pauli dynamics", + "method.mps": "Matrix product state", + "method.tensor_network": "Tensor network", + "method.cutrank": "Cut-rank sum", + "method.cutrank_single": "Cut-rank sum on the GPU, single precision", + "method.density_matrix": "Density matrix", + "method.trajectories": "Trajectories", + "accuracy.bit-exact": "The same bits on every machine.", + "accuracy.exact": "No approximation beyond double-precision rounding.", + "accuracy.single": "No approximation beyond single-precision rounding, about 10⁻⁷ per gate.", + "accuracy.bounded": "An approximation, each value with a proven bound on its error.", + "accuracy.estimate": "A statistical estimate, each value with its standard error.", + "accuracy.heuristic": "An approximation, with the standard estimate of its error but no bound.", + "noise.depolarizing_1q": "one-qubit depolarizing", + "noise.depolarizing_2q": "two-qubit depolarizing", + "noise.amplitude_damping": "amplitude damping", + "noise.dephasing": "dephasing", + "noise.readout": "readout flips", + "frame.under_ms": "under 1 ms", + "frame.bits": "{n} bits", + "frame.run_title": "Run (Ctrl or ⌘ + Enter)", + "frame.run": "▶ Run", + "frame.cancel": "Cancel", + "frame.predict_title": "Say what you expect before each run, and see how close you come", + "frame.predict": "Predict first", + "frame.this_computer": "This computer", + "frame.this_browser": "This browser", + "frame.the_service": "The service", + "frame.examples_label": "Load an example", + "frame.examples": "Examples…", + "frame.studio": "Quantum Studio", + "frame.run_on": "Run on", + "frame.held": "Held lines {a} to {z}: no proposal may change them. Delete the markers to let them go.", + "frame.review_label": "A proposal to review", + "frame.copy_link": "Copy link", + "frame.copy_link_title": "A link with the program in its fragment, which no server sees", + "frame.open": "Open", + "frame.save": "Save", + "frame.shots": "Shots", + "frame.seed": "Seed", + "frame.auto_title": "The strongest method the program admits", + "frame.method": "Method", + "frame.observables": "Observables", + "frame.amplitudes": "Amplitudes", + "frame.device_noise": "A device is chosen: the noise is the device’s own, from its calibration.", + "frame.noise": "Noise", + "frame.program": "Program", + "frame.circuit_text_label": "The circuit, drawn as text", + "frame.saved": "Saved {path}.", + "frame.saved_drawing": "Saved {name}, the engine's drawing.", + "frame.svg_title": "Save the drawing as an SVG image", + "frame.latex_title": "Save the drawing as LaTeX (plain TikZ)", + "frame.choose_first": "Choose on the proposal under review first.", + "frame.text": "Text", + "frame.text_title": "The circuit as text, to copy", + "frame.diagram": "Diagram", + "frame.circuit": "Circuit", + "frame.ready": "ready", + "frame.result_empty": "Run the program to see its result here. Counts are keyed by the classical bits, highest bit first.", + "frame.result": "Result", + "frame.history": "History", + "frame.clear": "Clear", + "ai.server": "Model server", + "ai.model": "Model", + "ai.model_placeholder": "the server’s own", + "ai.not_connected": "not connected", + "ai.connect": "Connect", + "ai.when_asked": "Only when I ask", + "ai.when_stuck": "Offer a hint when I seem stuck", + "ai.how": "How it helps", + "ai.when": "When it speaks", + "ai.settings_note": "A model on this machine by default: any server with the chat-completions interface and tool calls. Nothing leaves this machine unless the address points elsewhere.", + "ai.intro": "Ask about the program. The assistant speaks only when asked, and uses the engine to check what it says: every number comes from a run, and every program it proposes is read and run here before it is offered.", + "ai.placeholder": "Ask about this program…", + "ai.ask_label": "Ask the assistant", + "ai.ask": "Ask", + "ai.stuck": "Stuck? Ask for a hint", + "ai.q.question": "Ask me a question", + "ai.q.hint": "Hint", + "ai.q.explain": "Explain this circuit", + "ai.q.method": "Why this method?", + "ai.q.fix": "Fix the problem", + "ai.title": "Assistant", + "ai.gear": "Model…", + "frame.engine_not_loaded": "engine not loaded", + "frame.lint_at": " at {values}", + "frame.lint": "{qubits} qubits · {gates} gates", + "frame.lint_dynamic": " · measures in the middle", + "frame.problem": "problem", + "frame.editor_label": "The OpenQASM program", + "frame.plan": "{method} · {mib} MiB at most", + "frame.your_prediction": "your prediction", + "frame.running": "running", + "frame.running_on": "Running on {where}…", + "frame.on_service": "On the service: {status}…", + "frame.done": "Done on {where} in {took}.", + "frame.cancelled_badge": "cancelled", + "frame.refused": "refused", + "frame.cancelled": "Cancelled.", + "frame.verdict": "Verdict", + "frame.accuracy": "Accuracy", + "frame.working_out": "working it out here…", + "frame.counts": "Counts · {total} shots · highest bit first", + "frame.more_outcomes": "{n} more outcomes.", + "frame.expectations": "Expectation values", + "frame.th_state": "state", + "frame.th_amplitude": "amplitude", + "frame.th_probability": "probability", + "frame.th_phase": "phase", + "frame.checking": "checking…", + "frame.download_job": "Download job", + "frame.receipt": "Receipt", + "frame.ran_on": "ran on", + "frame.signed_by": "signed by", + "frame.check": "check", + "frame.ran_in": "ran in", + "frame.timed_here": "{took}, timed by this page", + "frame.energy": "energy", + "frame.written_by": "written by", + "frame.account": "account", + "frame.receipt_note": "A receipt shows that this key signed this request and this result; anyone can run the request again to compare. It does not say who holds the key.", + "frame.no_verdict": "no verdict", + "frame.key_service": "the service’s key", + "frame.key_computer": "this computer’s key", + "frame.key_browser": "this browser’s key", + "frame.key_named": "the key it names", + "frame.verifies": "✓ verifies, checked on {where}", + "frame.here_computer": "this computer", + "frame.here_device": "this device", + "frame.does_not_verify": "✗ does not verify", + "frame.not_checked": "not checked: {error}", + "frame.recomputed": "✓ recomputed from {version} in Versions", + "frame.not_in_computer": "not in this computer’s versions", + "frame.not_in_browser": "not in this browser’s versions", + "frame.took": "Took {accepted} of {offered} change(s) from {who}{stops}, as {version}.", + "frame.took_none": "Took none of the proposal from {who}{stops}; {version} in Versions keeps it.", + "frame.blocked": "; {n} blocked by your rules", + "frame.no_rewrite": "The engine has no rewrite for that one now; the program may have changed.", + "frame.fix_blocked": "Your rules block that fix: it would change what they hold. {version} in Versions keeps it.", + "frame.fixed_shows": "Fixed: {message}. The engine checked the result: {why}.", + "frame.fixed": "Fixed, as {version}. Undo brings the lines back.", + "frame.saved_job": "Saved {name}. wai-quantum verify checks it.", + "frame.opened": "Opened {name}.", + "frame.copied": "Copied a link with the program in its fragment, which no server sees.", + "frame.from_history": "From the history: run on {where}, {when}.", + "frame.history_empty": "Jobs you run appear here, kept on this device.", + "frame.method_reach": "{method} (to {n} qubits)", + "ai.thinking": "thinking…", + "ai.used": "used {name}", + "ai.retry": "The engine could not read the proposed program ({error}); asked for a correction.", + "ai.does_not_parse": "does not parse", + "ai.checked": "checked: {qubits} qubits, {gates} gates", + "ai.ran_here": " ran here on {method}", + "ai.did_not_run": " did not run: {error}", + "ai.organic": " You are exploring on your own: end that under Versions to take it.", + "ai.apply": "Apply", + "ai.unsourced": "Not from a run: {list}. The model gave these itself; the engine did not compute them.", + "ai.tokens": "{into} tokens in, {out} out", + "ai.error": "The assistant could not answer: {error}. Check the model server under “Model…”.", + "ai.connecting": "connecting…", + "ai.connected": "connected", + "frame.from_link": "The program from the link.", + "frame.signs_as": "Signs as {did}… ({whose})", + "frame.key_made_browser": "a key made for this browser", + "frame.service_silent": "The service did not answer", + "frame.versions_error": "Versions: {error}", + "frame.lesson_loaded": "The starter program of “{title}”.", + "frame.language": "Language", + "frame.machine_assisted": "This interface's words are a machine-assisted translation, to be reviewed by a fluent reader.", + "frame.where_browser": "this browser", + "frame.where_service": "the service", + "frame.where_computer": "this computer", + "frame.gates_by": "{n} by {who}", + "frame.written": "{parts} · of {of} gates, {version}", + "frame.no_gates": "no gates", + "frame.where_hardware": "your hardware", + "frame.where_provider": "your provider {name}" +} diff --git a/wai/quantum-studio/locales/en/gallery.json b/wai/quantum-studio/locales/en/gallery.json new file mode 100644 index 000000000..1042af266 --- /dev/null +++ b/wai/quantum-studio/locales/en/gallery.json @@ -0,0 +1,51 @@ +{ + "_meta": { + "locale": "en", + "area": "gallery", + "translation": "source" + }, + "gallery.atlas_label": "The draft and {n} alternatives by two-qubit gates and depth", + "gallery.axis_two_qubit": "two-qubit gates →", + "gallery.axis_depth": "↑ depth", + "gallery.draft_point": "the draft: {two_qubit} two-qubit gates, depth {depth}", + "gallery.point_label": "alternative {n}: {two_qubit} two-qubit gates, depth {depth}", + "gallery.point_title": "{n}. {label}: {two_qubit} two-qubit gates, depth {depth}", + "gallery.aim_placeholder": "Toward… fewer qubits, another entangler, more symmetry", + "gallery.aim_label": "What Explore should aim for", + "gallery.improve": "Improve", + "gallery.improve_title": "The same circuit, cheaper: each checked equivalent to the draft", + "gallery.explore": "Explore", + "gallery.explore_title": "Different circuits toward the aim, from the model, each measured by the engine", + "gallery.intro": "Improve: the same circuit, cheaper, each checked equivalent. Explore: different circuits toward an aim, from the model, each measured by the engine. Nothing changes until you apply one.", + "gallery.title": "Alternatives", + "gallery.how_one": "How: {n} rewrite, each checked", + "gallery.how_many": "How: {n} rewrites, each checked", + "gallery.how_reordered": "How: the same gates, in another order", + "gallery.and_this_one": "and this one reduces to the same by:", + "gallery.commute": "The two are then the same up to the order of gates that commute.", + "gallery.does_not_parse": "does not parse: {error}", + "gallery.by_taste_one": "Ordered by your taste ({profile}, {n} choice).", + "gallery.by_taste_many": "Ordered by your taste ({profile}, {n} choices).", + "gallery.from_engine": "the engine", + "gallery.from_model": "the model", + "gallery.lines_changed": "{n} lines changed", + "gallery.same_lines": "the same lines", + "gallery.program": "Program", + "gallery.apply": "Apply", + "gallery.nothing_to_improve": "Nothing to improve that the engine can check: no gates cancel or merge, and the model offered none it could confirm.", + "gallery.nothing_read": "The model offered nothing the engine could read.", + "gallery.not_offered": "Not offered: {list}.", + "gallery.engine_looking": "The engine is looking…", + "gallery.cannot_read": "The engine cannot read the draft: {error}", + "gallery.paused_engine_only": "You are exploring on your own: these are the engine’s alone.", + "gallery.connect_for_rewrites": "Connect a model under “Model…” in the Assistant for its rewrites as well.", + "gallery.a_rewrite": "a rewrite", + "gallery.model_failed_engine_only": "The model did not answer ({error}); these are the engine’s alone.", + "gallery.breaks_rules": "{label}: it breaks your rules: {why}", + "gallery.not_same": "{label}: not the same circuit ({why})", + "gallery.paused_explore": "You are exploring on your own: Explore asks the model, so it waits until you end that under Versions. Improve works without it.", + "gallery.explore_needs_model": "Explore asks a model for ideas, and none is connected. Connect one under “Model…” in the Assistant; Improve works without one.", + "gallery.model_imagining": "The model is imagining; the engine will measure each idea…", + "gallery.an_idea": "an idea", + "gallery.model_failed": "The model did not answer: {error}" +} diff --git a/wai/quantum-studio/locales/en/generators.json b/wai/quantum-studio/locales/en/generators.json new file mode 100644 index 000000000..fcf0eb126 --- /dev/null +++ b/wai/quantum-studio/locales/en/generators.json @@ -0,0 +1,11 @@ +{ + "_meta": { + "locale": "en", + "area": "generators", + "translation": "source" + }, + "generators.inputs": "The generator’s inputs", + "generators.read_at": "A generator, read at", + "generators.one_circuit": "one circuit of its family; the Workbench checks it at every size.", + "generators.value_of": "the value of {name}" +} diff --git a/wai/quantum-studio/locales/en/hardware.json b/wai/quantum-studio/locales/en/hardware.json new file mode 100644 index 000000000..4e75a0429 --- /dev/null +++ b/wai/quantum-studio/locales/en/hardware.json @@ -0,0 +1,24 @@ +{ + "_meta": { + "locale": "en", + "area": "hardware", + "translation": "source" + }, + "hardware.note": "Runs on your own hardware go through the desktop app or the command line (wai-quantum run --provider NAME): a browser tab must not hold the credentials a provider needs.", + "hardware.cost_unknown": "cost unknown: {why}", + "hardware.free": "free", + "hardware.billed_by": " (billed by the {unit})", + "hardware.about_seconds": ", about {seconds} s", + "hardware.confirm_run": "Run on {name} ({device}, {provenance})?", + "hardware.confirm_cost": "It is not free: {cost}.", + "hardware.confirm_compiled": "The program is compiled to {gates} native gates in {basis}, {swaps} swaps, and was checked against your circuit here; {shots} shots.", + "hardware.confirm_spend": "Running it spends your account's money or quota.", + "hardware.choose_provider": "choose one of your providers first", + "hardware.run_it": "Run it", + "hardware.cancel": "Cancel", + "hardware.not_free_title": "This run is not free", + "hardware.segment": "Your hardware", + "hardware.segment_title": "Run on one of your own providers; a run that is not free asks first", + "hardware.provider_label": "Your provider", + "hardware.no_providers": "No providers yet: add one with wai-quantum providers add, then reopen this list." +} diff --git a/wai/quantum-studio/locales/en/novelty.json b/wai/quantum-studio/locales/en/novelty.json new file mode 100644 index 000000000..24ec785dc --- /dev/null +++ b/wai/quantum-studio/locales/en/novelty.json @@ -0,0 +1,29 @@ +{ + "_meta": { + "locale": "en", + "area": "novelty", + "translation": "source" + }, + "novelty.same_state": "the same state from |0…0⟩", + "novelty.same_circuit": "the same circuit", + "novelty.route": "a different route to {name}", + "novelty.route_why": "The engine finds {what} as {name} ({how}), built unlike it: {distance} apart, where 0 is built alike and 1 shares nothing. Worth a second look.", + "novelty.known": "a known construction: {name}", + "novelty.known_why": "The engine finds {what} as {name} ({how}), built alike.", + "novelty.new": "new: {novelty} from the nearest ({name})", + "novelty.new_why": "Its gates and their order are {novelty} from {name}, the nearest of the {compared} programs compared (0 built alike, 1 nothing shared). A reason to look, not a claim that nobody has built it.", + "novelty.unasked": "Better than the reference on what the cost does not weigh: {list}.", + "novelty.unasked_item": "{measure} {candidate} against {reference}", + "novelty.kind_route": "a different route", + "novelty.kind_aberration": "an aberration", + "novelty.kind_novel": "far from the known", + "novelty.found_empty": "What the engine finds worth a second look comes here: a known result reached by a different route, a Workbench candidate that breaks only one of its rules, a verified entry far from everything it was compared with. Save what you mean to keep; dismiss the rest.", + "novelty.found_title": "Found", + "novelty.found_count": "{flagged} flagged · {saved} saved", + "novelty.note_placeholder": "A note, kept with it", + "novelty.note_label": "A note on {title}", + "novelty.saved": "Saved", + "novelty.save": "Save", + "novelty.dismiss": "Dismiss", + "novelty.open": "Open in editor" +} diff --git a/wai/quantum-studio/locales/en/predict.json b/wai/quantum-studio/locales/en/predict.json new file mode 100644 index 000000000..8e306f7d0 --- /dev/null +++ b/wai/quantum-studio/locales/en/predict.json @@ -0,0 +1,28 @@ +{ + "_meta": { + "locale": "en", + "area": "predict", + "translation": "source" + }, + "predict.keyed": "Counts are keyed by the classical bits, highest bit first.", + "predict.keyed_width": "Counts are keyed by the classical bits, highest bit first: {width} of them.", + "predict.not_bits": "An outcome is a string of 0s and 1s, highest bit first.", + "predict.wrong_width_one": "This program has {width} classical bit; the outcome has {length}.", + "predict.wrong_width_other": "This program has {width} classical bits; the outcome has {length}.", + "predict.bad_share": "The share is a percentage of the shots, from 0 to 100.", + "predict.before": "Before it runs", + "predict.question": "Which outcome do you expect most often, and in what share of the shots?", + "predict.outcome_label": "Most likely outcome", + "predict.share_label": "Its share, %", + "predict.run": "Run", + "predict.skip": "Run without predicting", + "predict.yours": "Your prediction", + "predict.no_counts": "This run gave no counts to set it against: give the job shots.", + "predict.right": "The outcome you named came out on top; your share was {points} points off.", + "predict.wrong": "Another outcome came out on top: {top} in {share} of the shots.", + "predict.you_said": "you said", + "predict.bits_in": "{bits} in {share}", + "predict.the_run": "the run", + "predict.first": "Your first prediction kept on this device; each one after it adds to a tally here.", + "predict.tally": "Over {n} predictions kept on this device: you named the top outcome in {right}, and your share was off by {points} points on average." +} diff --git a/wai/quantum-studio/locales/en/rewrite_view.json b/wai/quantum-studio/locales/en/rewrite_view.json new file mode 100644 index 000000000..2afc1cf7d --- /dev/null +++ b/wai/quantum-studio/locales/en/rewrite_view.json @@ -0,0 +1,24 @@ +{ + "_meta": { + "locale": "en", + "area": "rewrite_view", + "translation": "source" + }, + "rewrite_view.rule_cancel": "cancel", + "rewrite_view.rule_merge": "merge", + "rewrite_view.rule_through": "carry", + "rewrite_view.rule_conjugate": "conjugate", + "rewrite_view.rule_drop": "drop", + "rewrite_view.rule_move": "move", + "rewrite_view.rewrites": "Rewrites", + "rewrite_view.rewrites_title": "Every rewrite that keeps the program’s meaning, each checked by the engine", + "rewrite_view.heading": "Rewrites", + "rewrite_view.take": "Take", + "rewrite_view.offer_note": "A rewrite, checked by the engine: {why}.", + "rewrite_view.lines_one": "line {lines}", + "rewrite_view.lines_other": "lines {lines}", + "rewrite_view.checking": "The engine is checking each rewrite…", + "rewrite_view.unread": "The engine cannot read the program: {error}", + "rewrite_view.none": "No rewrite applies: no gates cancel, merge or conjugate, and no two that commute sit side by side.", + "rewrite_view.note": "Each keeps the circuit’s meaning: the engine checks the gates a rewrite takes against the gates it puts, on the qubits they touch. Take one at a time; the review shows the change." +} diff --git a/wai/quantum-studio/locales/en/rules.json b/wai/quantum-studio/locales/en/rules.json new file mode 100644 index 000000000..b9aaf4ccf --- /dev/null +++ b/wai/quantum-studio/locales/en/rules.json @@ -0,0 +1,21 @@ +{ + "_meta": { + "locale": "en", + "area": "rules", + "translation": "source" + }, + "rules.title": "Rules for proposals", + "rules.hold": "Hold the selected lines", + "rules.hold_title": "Mark them with {lock} and {unlock}: no proposal may change them", + "rules.region": "Keep the next proposal to the selected lines", + "rules.gates": "Gates a proposal may add", + "rules.gates_any": "any", + "rules.avoid_gates": "Gates to avoid", + "rules.none": "none", + "rules.avoid_text": "Text to avoid", + "rules.avoid_text_placeholder": "none, or text, one per comma", + "rules.qubits": "Hold the qubit declarations", + "rules.note": "The engine checks every proposal against these, change by change: the assistant's, an alternative, a fix, an agent's. A change that breaks one cannot be taken. Lines between {lock} and {unlock} are held. What you type is yours to change.", + "rules.only_lines": "Only lines {first} to {last}, for the next proposal ", + "rules.clear_region": "Keep to no lines" +} diff --git a/wai/quantum-studio/locales/en/shortcuts_view.json b/wai/quantum-studio/locales/en/shortcuts_view.json new file mode 100644 index 000000000..578e01ac3 --- /dev/null +++ b/wai/quantum-studio/locales/en/shortcuts_view.json @@ -0,0 +1,40 @@ +{ + "_meta": { + "locale": "en", + "area": "shortcuts_view", + "translation": "source" + }, + "shortcuts_view.route_stabilizer": "Stabilizer tableau", + "shortcuts_view.route_cutrank": "Cut-rank sum", + "shortcuts_view.route_mps": "Exact matrix product state", + "shortcuts_view.route_light_cone": "Light cone", + "shortcuts_view.route_tensor_network": "Tensor network", + "shortcuts_view.route_statevector": "State vector", + "shortcuts_view.under_ms": "under 1 ms", + "shortcuts_view.days": "{n} days", + "shortcuts_view.years": "{n} years", + "shortcuts_view.size_widest_cut": "widest cut {n} bits", + "shortcuts_view.size_product_state": "a product state", + "shortcuts_view.size_bond": "bond ≤ 2^{n}", + "shortcuts_view.size_cone": "cone of {n} qubits", + "shortcuts_view.size_largest_tensor": "largest tensor 2^{n}", + "shortcuts_view.size_amplitudes": "2^{n} amplitudes", + "shortcuts_view.size_tableau": "a tableau", + "shortcuts_view.classical_cost": "Classical cost", + "shortcuts_view.classical_cost_title": "Every exact classical route the engine knows for this program, with its cost", + "shortcuts_view.thorough": "Thorough", + "shortcuts_view.thorough_title": "Also search a contraction tree and a longer cut-rank order: seconds to minutes on a large program", + "shortcuts_view.heading": "Classically", + "shortcuts_view.unread": "The engine cannot read the program: {error}", + "shortcuts_view.does_not_apply": "does not apply", + "shortcuts_view.not_searched": "not searched", + "shortcuts_view.col_route": "route", + "shortcuts_view.col_size": "size", + "shortcuts_view.col_memory": "memory", + "shortcuts_view.col_operations": "operations", + "shortcuts_view.col_rate": "at 10¹⁰/s", + "shortcuts_view.note": "Each route is exact; operations are to a first sample, amplitude or expectation value. The times are at a reference rate, not a measurement. {note}.", + "shortcuts_view.searching": "Searching each route, a contraction tree among them: this can take minutes on a large program…", + "shortcuts_view.pricing": "Pricing each route…", + "shortcuts_view.stale": "For an earlier version of the program: check again." +} diff --git a/wai/quantum-studio/locales/en/state_views.json b/wai/quantum-studio/locales/en/state_views.json new file mode 100644 index 000000000..76cbf719b --- /dev/null +++ b/wai/quantum-studio/locales/en/state_views.json @@ -0,0 +1,25 @@ +{ + "_meta": { + "locale": "en", + "area": "state_views", + "translation": "source" + }, + "state_views.amp_label": "probability {p}, phase {phase}π", + "state_views.prob_label": "probability {p}", + "state_views.amps_label": "Amplitudes at this step", + "state_views.col_state": "state", + "state_views.col_amplitude": "amplitude", + "state_views.col_probability": "probability", + "state_views.col_phase": "phase", + "state_views.amps_note": "{listed} of {nonzero} non-zero basis states listed, of {of}; purity {purity}{mixed}; digest {digest}…", + "state_views.mixed": " (a mixed state: probabilities, no amplitudes)", + "state_views.bloch_label": "Qubit {qubit}: Bloch vector ({x}, {y}, {z}), length {length}", + "state_views.qubit_label": "Qubit {qubit}: probability of 1 {prob}, purity {purity}", + "state_views.caption": " p(1) {prob} · purity {purity}{twin}", + "state_views.without_noise_z": " · without noise z {z}", + "state_views.choose_qubits": "Choose up to six qubits for their reduced state.", + "state_views.rho_label": "The reduced density matrix of qubits {qubits}", + "state_views.rho_note": "qubits {qubits} (the first the most significant): purity {purity}, entropy {entropy} bits. Each entry's size is shown; hover for its value.", + "state_views.without_noise": "without noise {p}", + "state_views.overlay_note": "The bars are the noisy state's probabilities; each mark is the noiseless state's on the same path. Fidelity to it {fidelity}." +} diff --git a/wai/quantum-studio/locales/en/study.json b/wai/quantum-studio/locales/en/study.json new file mode 100644 index 000000000..ddec12464 --- /dev/null +++ b/wai/quantum-studio/locales/en/study.json @@ -0,0 +1,60 @@ +{ + "_meta": { + "locale": "en", + "area": "study", + "translation": "source" + }, + "study.measure_quality": "your edits the engine proves equal that made it cheaper", + "study.measure_quality_trend": "that share, later against earlier", + "study.measure_selection": "how often you took the engine’s best of those shown", + "study.measure_calibration_off": "how far your predicted share was from the run’s", + "study.measure_calibration_slope": "the run’s share against your prediction (1 is calibrated)", + "study.measure_unassisted": "your probe score without the assistant (1 matches the reference)", + "study.measure_ownership": "your answers on ownership, 1 to 7", + "study.measure_authorship": "your share of the program’s gates", + "study.measure_novelty": "how far your designs are from the known constructions", + "study.measure_diversity": "how many distinct designs you made (Vendi)", + "study.measure_convergence": "a model’s share of your program’s gates", + "study.title": "Study", + "study.load": "Load a protocol", + "study.keep": "Keep a record of how I work, in this browser", + "study.enrolled": "You take part in “{study}”, condition {condition}, set by your key. Your record stays in this browser until you export it.", + "study.intro": "Take part in a study of how people design with the engine: a researcher gives you a protocol, sealed before any data, and you see what it studies before you choose. Or keep a record for yourself.", + "study.kept": "Kept here: {choices} among candidates, {probes}, {answers} on ownership, and your predictions and history.", + "study.choice_one": "{n} choice", + "study.choices": "{n} choices", + "study.probe_one": "{n} probe", + "study.probes": "{n} probes", + "study.answer_one": "{n} answer", + "study.answers": "{n} answers", + "study.own": "How much is this yours?", + "study.probe": "A probe task", + "study.my_record": "My record", + "study.export": "Export my record", + "study.leave": "Leave the study", + "study.forget": "Forget what is kept", + "study.hypotheses": "What it studies, written before any data:", + "study.compares": "It compares {n} conditions, each a setting of the Studio’s features; your key assigns you one. Its primary measure: {primary}. The protocol is sealed (digest {digest}…), so it cannot change after you join.", + "study.join": "Take part", + "study.not_now": "Not now", + "study.not_protocol": "That is not a protocol the engine opens: {error}", + "study.left": "You left the study. What was kept stays in this browser until you forget it.", + "study.save_answers": "Save my answers", + "study.answer_all": "Answer every one", + "study.saved_answers": "Saved, with the version of the program you answered about.", + "study.own_scale": "About the program in the editor, from 1 (not at all) to 7 (completely):", + "study.which_probe": "Which probe", + "study.done_before": " (done before)", + "study.answer_label": "Your answer, in OpenQASM", + "study.score": "Score it", + "study.verified": "Verified. Score {score} (the reference scores 1; cost {cost} against {reference}).", + "study.not_verified": "Not verified: {failure}. Score 0.", + "study.probe_note": "Done without the assistant: it is paused until you close this. The engine scores your answer.", + "study.close": "Close", + "study.of_edits": "{n} of {of} edits", + "study.of_choices": "{n} of {of}; by chance {chance}", + "study.predictions": "{n} predictions", + "study.designs": "{n} designs", + "study.measures_note": "Each is the engine’s, computed again from what you made and did; none is a model’s judgement.", + "study.exported": "Saved {where}, sealed with your key. Give it to the researcher; nothing was sent." +} diff --git a/wai/quantum-studio/locales/en/taste.json b/wai/quantum-studio/locales/en/taste.json new file mode 100644 index 000000000..c19e08bd8 --- /dev/null +++ b/wai/quantum-studio/locales/en/taste.json @@ -0,0 +1,58 @@ +{ + "_meta": { + "locale": "en", + "area": "taste", + "translation": "source" + }, + "taste.part_economy_what": "what it spends: gates, depth, T gates", + "taste.part_clarity_what": "how readily it is read", + "taste.part_insight_what": "what it shows about the problem", + "taste.part_precision_what": "how exactly it does its job, under noise too", + "taste.part_utility_what": "how readily it is used elsewhere", + "taste.part_economy": "economy", + "taste.part_clarity": "clarity", + "taste.part_insight": "insight", + "taste.part_precision": "precision", + "taste.part_utility": "utility", + "taste.no_choices": "No choices yet. Compare two kept generators, or apply or refuse an alternative that is the same circuit as the draft, and your taste is learned from what you choose.", + "taste.choices_one": "{n} choice", + "taste.choices_many": "{n} choices", + "taste.choices_parts": " ({parts})", + "taste.predicts": "it predicts {right} of the {of} you made after your first, each from the ones before", + "taste.leans": "it leans toward {list}", + "taste.unmeasured": "{n} differ in nothing the engine measures", + "taste.summary": "{parts}.", + "taste.puts_first": "{part} puts {first} first", + "taste.the_cost": "the cost", + "taste.disagreeing": "Disagreeing: {list}.", + "taste.narrow": "The first {first} are {ranked} apart in how they are built; by {by}, {baseline}{narrows}.", + "taste.narrows": ": this ranking narrows what comes first", + "taste.profile_label": "The taste profile’s name", + "taste.compare": "Compare two", + "taste.compare_title": "The two kept generators your profile is least sure of: choose the one you prefer", + "taste.rank": "Rank by my taste", + "taste.dial_label": "The novelty dial, from the typical to the novel", + "taste.title": "Your taste", + "taste.profile": "Profile", + "taste.typical": "typical", + "taste.novel": "novel", + "taste.note": "You choose only between programs the engine proves do the same thing, so a choice is about taste, never correctness. The profile is yours, kept in this browser under its name. A ranking shows how typical each one is, which the profile never learns and only the dial moves.", + "taste.none": "Start an archive and keep two generators or more to compare and rank them.", + "taste.finding_pair": "The engine is finding the pair your profile is least sure of…", + "taste.nothing_to_compare": "Nothing to compare: {why}.", + "taste.part_label": "What the choice is about", + "taste.the_whole": "the whole", + "taste.cost_suffix": " · cost {cost}", + "taste.this_one": "This one", + "taste.which": "Which do you prefer? The engine proves they do the same thing ({how}).", + "taste.about": "About", + "taste.skip": "Skip", + "taste.kept": "Kept: {chosen} over {other}{part}. ", + "taste.for_its": ", for its {part}", + "taste.another": "Another", + "taste.typicality_title": "Typicality: how near the known constructions it is built, from 1 (built as one is) to 0 (nothing shared)", + "taste.typicality_none": "typicality —", + "taste.typicality": "typicality {value}", + "taste.score": "score {score}", + "taste.no_choices_dial": "No choices yet: ordered by the dial alone." +} diff --git a/wai/quantum-studio/locales/en/verdict_view.json b/wai/quantum-studio/locales/en/verdict_view.json new file mode 100644 index 000000000..579b485f1 --- /dev/null +++ b/wai/quantum-studio/locales/en/verdict_view.json @@ -0,0 +1,17 @@ +{ + "_meta": { + "locale": "en", + "area": "verdict_view", + "translation": "source" + }, + "verdict_view.refused": "refused", + "verdict_view.parsed": "parsed, not run", + "verdict_view.executed": "executed, not checked", + "verdict_view.evidence": "checked as evidence", + "verdict_view.proof": "checked as proof", + "verdict_view.fails": ": fails", + "verdict_view.no_verdict": "no verdict", + "verdict_view.sealed": "sealed by the receipt", + "verdict_view.not_sealed": "worked out here, not sealed by a receipt", + "verdict_view.why": " {statement} ({where})" +} diff --git a/wai/quantum-studio/locales/en/versions.json b/wai/quantum-studio/locales/en/versions.json new file mode 100644 index 000000000..25f136d8d --- /dev/null +++ b/wai/quantum-studio/locales/en/versions.json @@ -0,0 +1,69 @@ +{ + "_meta": { + "locale": "en", + "area": "versions", + "translation": "source" + }, + "versions.hand_person": "you", + "versions.hand_assistant": "the assistant", + "versions.hand_agent": "an agent", + "versions.hand_engine": "the engine", + "versions.hand_origin": "where it came from", + "versions.hand_assistant_then_person": "the assistant, then you", + "versions.hand_agent_then_person": "an agent, then you", + "versions.how_start": "started", + "versions.how_edit": "edited", + "versions.how_proposal": "proposed", + "versions.how_merge": "merged", + "versions.made_by": "{how} by {who}", + "versions.started_from": "started from {who}", + "versions.opened_from": "opened {who}", + "versions.change_one": "{n} change", + "versions.changes": "{n} changes", + "versions.gate_one": "{n} gate", + "versions.gates": "{n} gates", + "versions.line_one": "{n} line", + "versions.lines": "{n} lines", + "versions.hands": "Who wrote it", + "versions.hands_title": "Mark each line and gate with whose hand it is in", + "versions.download": "Download", + "versions.download_title": "The program and its history, as wai-quantum history verify reads them", + "versions.title": "Versions", + "versions.intro": "Each change, who made it and how. What you type is yours; a proposal you apply is its proposer’s, and only the changes you take reach the program.", + "versions.merge_nothing": "{id} holds nothing the program does not.", + "versions.proposal_same": "The proposal is the program as it is.", + "versions.take_all": "Take all {n}", + "versions.take_some": "Take {n} of {of}", + "versions.bring_in": "Bring in {id}", + "versions.proposal_from": "A proposal from {who}", + "versions.review_sum": " · {changes}{blocked}. Take the ones you want; the program waits until you choose.", + "versions.blocked_by_your_rules": ", {n} blocked by your rules", + "versions.at_line": "at line {line}", + "versions.blocked_why": "Blocked: {why}.", + "versions.not_now": "Not now", + "versions.reject_all": "Reject all", + "versions.unread": "The program does not read here, so its gates are not counted; its lines are marked.", + "versions.bar_part": "{n} of {of} gates by {who}", + "versions.hand_gates": "{who}: {gates}", + "versions.sum": "{version}: {gates}, {lines}", + "versions.hand_share": "{who}: {gates}, {lines}", + "versions.open": "Open {id}", + "versions.open_title": "Put this version in the editor; what you change from it is a line of work of its own", + "versions.bring": "Bring {id} in", + "versions.bring_title": "Review its changes against the program and take the ones you want; recorded as a merge", + "versions.what": " {made}{from}, {when}", + "versions.from": ", from {parents}", + "versions.and": " and ", + "versions.review_taken": "{n} of {changes} taken{blocked}{rest}.", + "versions.blocked_by_the_rules": ", {n} blocked by the rules", + "versions.rest_kept": "; the rest were not taken, and the whole proposal is kept below", + "versions.whole_proposal": "The whole proposal", + "versions.against": "+{added} −{removed} against {parent}{sealed}", + "versions.sealed": " · sealed", + "versions.opened": "Opened {id}. What you change from here is a line of work of its own.", + "versions.in_editor": "in the editor", + "versions.a_head": "a head", + "versions.item_review": "{n} of {of} taken{blocked} · ", + "versions.item_blocked": ", {n} blocked", + "versions.saved": "Saved program.qasm and program.history.json; wai-quantum history verify program.qasm checks every version." +} diff --git a/wai/quantum-studio/locales/en/visual_edit.json b/wai/quantum-studio/locales/en/visual_edit.json new file mode 100644 index 000000000..f416ec34f --- /dev/null +++ b/wai/quantum-studio/locales/en/visual_edit.json @@ -0,0 +1,18 @@ +{ + "_meta": { + "locale": "en", + "area": "visual_edit", + "translation": "source" + }, + "visual_edit.chip_title": "Drag {gate} onto a wire, or choose it and then a place on the diagram", + "visual_edit.armed": "Now choose a place on a wire for {gate}: click the diagram.", + "visual_edit.delete": "Delete gate", + "visual_edit.delete_title": "Delete the selected gate (Delete)", + "visual_edit.palette_label": "Gates to put on the circuit", + "visual_edit.edit_by_drawing": "Edit by drawing", + "visual_edit.edited": "{summary}: an edit to the program's text, recorded as yours.", + "visual_edit.too_few_wires": "{gate} takes {arity} wires; the program has {wires}.", + "visual_edit.no_wire": "No wire there.", + "visual_edit.gate_label": "{gate}: arrows move it, Delete removes it", + "visual_edit.a_gate": "a gate" +} diff --git a/wai/quantum-studio/locales/en/workbench.json b/wai/quantum-studio/locales/en/workbench.json new file mode 100644 index 000000000..ed9e41de5 --- /dev/null +++ b/wai/quantum-studio/locales/en/workbench.json @@ -0,0 +1,101 @@ +{ + "_meta": { + "locale": "en", + "area": "workbench", + "translation": "source" + }, + "workbench.measure_depth": "depth", + "workbench.measure_two_qubit": "two-qubit gates", + "workbench.measure_t_count": "T gates", + "workbench.measure_gates": "gates", + "workbench.measure_two_qubit_depth": "two-qubit depth", + "workbench.measure_one_qubit": "one-qubit gates", + "workbench.measure_three_qubit": "three-qubit gates", + "workbench.measure_non_clifford": "non-Clifford gates", + "workbench.measure_qubits": "qubits", + "workbench.sizes_name_each": "name each input: {list}", + "workbench.sizes_no_input": "the program takes no input {name}; it takes {names}", + "workbench.sizes_whole": "{name}: whole numbers, 0 or more, as {name} = 3, 5, 8", + "workbench.sizes_missing": "give values for {names}", + "workbench.cost_needs_weight": "give a weight above 0 to at least one measure of the cost", + "workbench.map_label": "The archive: {n} cells by {x} and {y}", + "workbench.point_label": "{id}, by {proposer}: cost {cost}, {x} and {y}", + "workbench.point_title": "{id} ({proposer}): cost {cost}", + "workbench.outcome_duplicate": "{who}: seen before, not evaluated again", + "workbench.outcome_not_verified": "{who}: not verified: {failure}", + "workbench.outcome_did_not_read": "it did not read", + "workbench.outcome_improvement": "{who}: kept as {id}, cheaper than every generator before it (cost {cost})", + "workbench.outcome_kept": "{who}: kept as {id}, the best of its cell (cost {cost})", + "workbench.outcome_verified": "{who}: verified (cost {cost}), but its cell already holds one as cheap", + "workbench.start": "Start", + "workbench.start_title": "A new archive, seeded with the program in the editor as the reference", + "workbench.task_label": "What each member must do", + "workbench.task_prepares": "prepare the same state from |0…0⟩", + "workbench.task_equivalent": "be the same circuit (unitary)", + "workbench.sizes_label": "The sizes to check at", + "workbench.gates_any": "any", + "workbench.gates_label": "The gates a proposal may use", + "workbench.conn_label": "Which qubits a gate may join", + "workbench.conn_all": "any pair", + "workbench.conn_line": "neighbours on a line", + "workbench.weight_label": "the cost's weight on {measure}", + "workbench.axis_first": "The map’s first axis", + "workbench.axis_second": "The map’s second axis", + "workbench.spec": "The specification", + "workbench.each_member_must": "Each member must", + "workbench.checked_at": "checked at", + "workbench.using": "using", + "workbench.on": "on", + "workbench.cost_summed": "Cost, summed over the sizes:", + "workbench.map": "Map", + "workbench.against": "against", + "workbench.search": "Search ×100", + "workbench.search_title": "The built-in, blind search: a hundred seeded edits of the archive’s best, each checked by the engine", + "workbench.ask": "Ask the model ×3", + "workbench.ask_title": "Three generators from the model, from the archive’s best; the engine judges each", + "workbench.submit": "Submit the editor’s program", + "workbench.submit_title": "The program in the editor, judged and kept if it is the best of its cell", + "workbench.download": "Download", + "workbench.download_title": "The specification and its archive, every kept generator with its sealed verdict, for wai-quantum discover verify", + "workbench.intro": "Write a generator in the editor (a program with input uint n;), say what each member of its family must do and what it costs, and start. A program with no inputs works too: one circuit, one size.", + "workbench.title": "Workbench", + "workbench.stats_energy": ", {joules} J", + "workbench.improvement_one": "{n} improvement", + "workbench.improvement_many": "{n} improvements", + "workbench.stats": "{evaluations} evaluated, {verified} verified, {kept} kept, {improvements}{energy}. ", + "workbench.stats_best": "Best {id}: cost {cost} against the reference’s {reference}, by {proposer}, checked by {strength}.", + "workbench.open": "Open in editor", + "workbench.entry_title": "{id} · cost {cost} · {x_measure} {x_value}, {y_measure} {y_value}", + "workbench.checked_by": "checked by {strength}{sealed}", + "workbench.sealed": ", sealed", + "workbench.lineage": "Where it came from", + "workbench.its_generator": "Its generator", + "workbench.checking_reference": "The engine is checking the reference at every size…", + "workbench.started": "Started: the reference is {id}, cost {cost}.", + "workbench.loosened": "The specification now {change}. This archive was built under the old rule; start a new one to search under the new.", + "workbench.loosened_allows": "allows {gate}", + "workbench.loosened_any_pair": "lets a gate join any pair", + "workbench.novelty_said": "{id}: {text}.", + "workbench.aberration_uses": "it uses {gate}", + "workbench.aberration_joins": "it joins qubits that are not neighbours", + "workbench.aberration_breaks": "it breaks a rule", + "workbench.aberration_why": "It breaks only one rule ({rule}); without it, it would be verified ({strength}) at cost {cost}, {x_measure} {x_value}, {y_measure} {y_value}.", + "workbench.try_without": "Try without that rule", + "workbench.aberration_said": "{who}: an aberration. {why} The rule may be worth revising. ", + "workbench.search_progress": "The blind search: {done} of {total} edits judged…", + "workbench.search_done": "The blind search: {total} edits judged, {kept} kept.", + "workbench.search_find_said": "{id} (the blind search): {text}.", + "workbench.search_find_title": "{id} (the blind search): {text}", + "workbench.search_stopped": "The search stopped: {error}", + "workbench.paused": "You are exploring on your own: the model waits until you end that under Versions.", + "workbench.needs_model": "Ask the model needs one: connect it under “Model…” in the Assistant.", + "workbench.model_proposing": "The model is proposing; the engine will judge each…", + "workbench.model_wrote_none": "The model wrote no generator in a ```qasm block.", + "workbench.an_idea": "an idea", + "workbench.model_failed": "The model did not answer: {error}", + "workbench.your_program": "Your program", + "workbench.not_judged": "It could not be judged: {error}", + "workbench.saved": "Saved {path} and its archive; wai-quantum discover verify {stem}.json checks every seal.", + "workbench.not_saved": "It could not be saved: {error}", + "workbench.resumed": "Taken up again: {n} evaluated so far." +} diff --git a/wai/quantum-studio/locales/index.js b/wai/quantum-studio/locales/index.js new file mode 100644 index 000000000..0ae430dba --- /dev/null +++ b/wai/quantum-studio/locales/index.js @@ -0,0 +1,67 @@ +// The Studio's words: one file for each part of the interface in each locale, +// gathered here. English is the source. test/locales.test.mjs holds this list +// to the files on disk, and every locale to every key. +import enAssistant from './en/assistant.json' with { type: 'json' }; +import enCircuitSvg from './en/circuit_svg.json' with { type: 'json' }; +import enCompileView from './en/compile_view.json' with { type: 'json' }; +import enConverge from './en/converge.json' with { type: 'json' }; +import enCourse from './en/course.json' with { type: 'json' }; +import enCritiqueView from './en/critique_view.json' with { type: 'json' }; +import enDebugger from './en/debugger.json' with { type: 'json' }; +import enDevice from './en/device.json' with { type: 'json' }; +import enDom from './en/dom.json' with { type: 'json' }; +import enEstimateView from './en/estimate_view.json' with { type: 'json' }; +import enExamples from './en/examples.json' with { type: 'json' }; +import enFlags from './en/flags.json' with { type: 'json' }; +import enFrame from './en/frame.json' with { type: 'json' }; +import enGallery from './en/gallery.json' with { type: 'json' }; +import enGenerators from './en/generators.json' with { type: 'json' }; +import enHardware from './en/hardware.json' with { type: 'json' }; +import enNovelty from './en/novelty.json' with { type: 'json' }; +import enPredict from './en/predict.json' with { type: 'json' }; +import enRewriteView from './en/rewrite_view.json' with { type: 'json' }; +import enRules from './en/rules.json' with { type: 'json' }; +import enShortcutsView from './en/shortcuts_view.json' with { type: 'json' }; +import enStateViews from './en/state_views.json' with { type: 'json' }; +import enStudy from './en/study.json' with { type: 'json' }; +import enTaste from './en/taste.json' with { type: 'json' }; +import enVerdictView from './en/verdict_view.json' with { type: 'json' }; +import enVersions from './en/versions.json' with { type: 'json' }; +import enVisualEdit from './en/visual_edit.json' with { type: 'json' }; +import enWorkbench from './en/workbench.json' with { type: 'json' }; +import zhAssistant from './zh-CN/assistant.json' with { type: 'json' }; +import zhCircuitSvg from './zh-CN/circuit_svg.json' with { type: 'json' }; +import zhCompileView from './zh-CN/compile_view.json' with { type: 'json' }; +import zhConverge from './zh-CN/converge.json' with { type: 'json' }; +import zhCourse from './zh-CN/course.json' with { type: 'json' }; +import zhCritiqueView from './zh-CN/critique_view.json' with { type: 'json' }; +import zhDebugger from './zh-CN/debugger.json' with { type: 'json' }; +import zhDevice from './zh-CN/device.json' with { type: 'json' }; +import zhDom from './zh-CN/dom.json' with { type: 'json' }; +import zhEstimateView from './zh-CN/estimate_view.json' with { type: 'json' }; +import zhExamples from './zh-CN/examples.json' with { type: 'json' }; +import zhFlags from './zh-CN/flags.json' with { type: 'json' }; +import zhFrame from './zh-CN/frame.json' with { type: 'json' }; +import zhGallery from './zh-CN/gallery.json' with { type: 'json' }; +import zhGenerators from './zh-CN/generators.json' with { type: 'json' }; +import zhHardware from './zh-CN/hardware.json' with { type: 'json' }; +import zhNovelty from './zh-CN/novelty.json' with { type: 'json' }; +import zhPredict from './zh-CN/predict.json' with { type: 'json' }; +import zhRewriteView from './zh-CN/rewrite_view.json' with { type: 'json' }; +import zhRules from './zh-CN/rules.json' with { type: 'json' }; +import zhShortcutsView from './zh-CN/shortcuts_view.json' with { type: 'json' }; +import zhStateViews from './zh-CN/state_views.json' with { type: 'json' }; +import zhStudy from './zh-CN/study.json' with { type: 'json' }; +import zhTaste from './zh-CN/taste.json' with { type: 'json' }; +import zhVerdictView from './zh-CN/verdict_view.json' with { type: 'json' }; +import zhVersions from './zh-CN/versions.json' with { type: 'json' }; +import zhVisualEdit from './zh-CN/visual_edit.json' with { type: 'json' }; +import zhWorkbench from './zh-CN/workbench.json' with { type: 'json' }; + +// The parts of the interface, in the order their words are gathered. +export const AREAS = ['assistant', 'circuit_svg', 'compile_view', 'converge', 'course', 'critique_view', 'debugger', 'device', 'dom', 'estimate_view', 'examples', 'flags', 'frame', 'gallery', 'generators', 'hardware', 'novelty', 'predict', 'rewrite_view', 'rules', 'shortcuts_view', 'state_views', 'study', 'taste', 'verdict_view', 'versions', 'visual_edit', 'workbench']; + +export default { + en: { ...enAssistant, ...enCircuitSvg, ...enCompileView, ...enConverge, ...enCourse, ...enCritiqueView, ...enDebugger, ...enDevice, ...enDom, ...enEstimateView, ...enExamples, ...enFlags, ...enFrame, ...enGallery, ...enGenerators, ...enHardware, ...enNovelty, ...enPredict, ...enRewriteView, ...enRules, ...enShortcutsView, ...enStateViews, ...enStudy, ...enTaste, ...enVerdictView, ...enVersions, ...enVisualEdit, ...enWorkbench }, + 'zh-CN': { ...zhAssistant, ...zhCircuitSvg, ...zhCompileView, ...zhConverge, ...zhCourse, ...zhCritiqueView, ...zhDebugger, ...zhDevice, ...zhDom, ...zhEstimateView, ...zhExamples, ...zhFlags, ...zhFrame, ...zhGallery, ...zhGenerators, ...zhHardware, ...zhNovelty, ...zhPredict, ...zhRewriteView, ...zhRules, ...zhShortcutsView, ...zhStateViews, ...zhStudy, ...zhTaste, ...zhVerdictView, ...zhVersions, ...zhVisualEdit, ...zhWorkbench }, +}; diff --git a/wai/quantum-studio/locales/zh-CN/assistant.json b/wai/quantum-studio/locales/zh-CN/assistant.json new file mode 100644 index 000000000..72a189fde --- /dev/null +++ b/wai/quantum-studio/locales/zh-CN/assistant.json @@ -0,0 +1,14 @@ +{ + "_meta": { + "locale": "zh-CN", + "area": "assistant", + "translation": "machine-assisted: to be reviewed by a fluent reader before it is advertised" + }, + "assistant.stance_questions": "先向我提问", + "assistant.stance_suggest": "建议修改", + "assistant.stance_write": "编写程序", + "assistant.model_answered": "模型返回了 {status}", + "assistant.server_answered": "模型服务器返回了 {status}", + "assistant.no_answer": "模型没有给出回答", + "assistant.kept_calling": "模型一直在调用工具,没有作答" +} diff --git a/wai/quantum-studio/locales/zh-CN/circuit_svg.json b/wai/quantum-studio/locales/zh-CN/circuit_svg.json new file mode 100644 index 000000000..9a3320877 --- /dev/null +++ b/wai/quantum-studio/locales/zh-CN/circuit_svg.json @@ -0,0 +1,10 @@ +{ + "_meta": { + "locale": "zh-CN", + "area": "circuit_svg", + "translation": "machine-assisted: to be reviewed by a fluent reader before it is advertised" + }, + "circuit_svg.label": "线路:{qubits} 个量子比特,{gates} 个操作,共 {columns} 列", + "circuit_svg.cell": "q{q} 上的 {gate}", + "circuit_svg.cell_sep": "," +} diff --git a/wai/quantum-studio/locales/zh-CN/compile_view.json b/wai/quantum-studio/locales/zh-CN/compile_view.json new file mode 100644 index 000000000..4d938ebeb --- /dev/null +++ b/wai/quantum-studio/locales/zh-CN/compile_view.json @@ -0,0 +1,43 @@ +{ + "_meta": { + "locale": "zh-CN", + "area": "compile_view", + "translation": "machine-assisted: to be reviewed by a fluent reader before it is advertised" + }, + "compile_view.col_gates": "门", + "compile_view.col_cnots": "CNOT", + "compile_view.col_rotations": "旋转门", + "compile_view.basis_label": "门集", + "compile_view.basis_title": "编译所用的门集。Clifford+T:每个旋转门都写成误差在 ε 以内的 Clifford+T 门序列。标准门:任意角度的标准门,不做任何近似", + "compile_view.standard": "标准门", + "compile_view.level_label": "级别", + "compile_view.level_title": "0:降低到基本门并作近似;1:另加经检查的重写和相位折叠;2:再加单、双量子比特重合成", + "compile_view.level_option": "级别 {level}", + "compile_view.eps_title": "输出与程序之间允许的最大差异:算子范数,不计全局相位,由各旋转门分摊", + "compile_view.compile": "编译", + "compile_view.compile_title": "让程序经过引擎的各遍处理,每一遍都经过检查,并附有签名的证书", + "compile_view.heading": "编译", + "compile_view.caption": "{pass} 之后(第 {number}/{of} 遍):{gates} 个门,{cnots} 个 CNOT,{t} 个 T 门,{rotations} 个旋转门{partial}。", + "compile_view.caption_partial": ";只画出了 {total} 列中的前 {drawn} 列", + "compile_view.draw_title": "画出这一遍之后的线路", + "compile_view.spent_one": "{n} 个旋转门,每个 {each}", + "compile_view.spent_other": "{n} 个旋转门,每个 {each}", + "compile_view.nothing_approximated": "没有任何近似", + "compile_view.whole": "整个输出与程序对照测得的距离:{distance}。", + "compile_view.use": "使用", + "compile_view.use_title": "把编译后的程序作为建议提交审阅", + "compile_view.offer_note": "已在级别 {level} 编译为“{basis}”:与程序的距离不超过 {bound}(预算 {budget}),{t} 个 T 门;每一遍都经过检查。", + "compile_view.the_standard_gates": "标准门", + "compile_view.verdict_approximate": "近似", + "compile_view.verdict_exact": "精确", + "compile_view.verdict_proved": "已证明", + "compile_view.summary": "{verdict}:与程序的距离不超过 {bound},不计全局相位(预算 {budget};{spent})。{whole}{certificate}", + "compile_view.cert_verifies": "证书验证通过。", + "compile_view.cert_fails": "证书未通过验证。", + "compile_view.col_pass": "遍", + "compile_view.col_check": "检查", + "compile_view.note": "选择一遍,即可画出它之后的线路。检查结果为 proof(精确的环算术,以整八分之一为单位的相位多项式)、exact(等于双精度浮点数的舍入)或 approximate(附其距离);各距离之和即为上界。", + "compile_view.bad_eps": "ε 是一个正数,例如 1e-6。", + "compile_view.compiling": "正在编译:每一遍都随即检查…", + "compile_view.stale": "这是针对程序较早版本的结果:请重新编译。" +} diff --git a/wai/quantum-studio/locales/zh-CN/converge.json b/wai/quantum-studio/locales/zh-CN/converge.json new file mode 100644 index 000000000..86ade8857 --- /dev/null +++ b/wai/quantum-studio/locales/zh-CN/converge.json @@ -0,0 +1,19 @@ +{ + "_meta": { + "locale": "zh-CN", + "area": "converge", + "translation": "machine-assisted: to be reviewed by a fluent reader before it is advertised" + }, + "converge.converging": "正在趋同:{why}。", + "converge.latest": "你最新的版本在构造方式上与模型的建议相距 {distance}。", + "converge.own_one": "独自完成:{n} 个版本,与它们相距 {distance}{rest}。", + "converge.own_many": "独自完成:{n} 个版本,与它们相距 {distance}{rest}。", + "converge.own_rest": ";其余版本相距 {distance}", + "converge.try_own": "试着独自探索一段时间:届时助手只提问。", + "converge.chip": "独自探索中", + "converge.title": "你自己的方式", + "converge.end": "结束:让助手重新提出建议", + "converge.start": "独自探索一段时间", + "converge.end_title": "助手、备选方案和工作台可以再次询问模型", + "converge.start_title": "助手只提问;备选方案和工作台不询问模型;你的版本会被记为独自完成" +} diff --git a/wai/quantum-studio/locales/zh-CN/course.json b/wai/quantum-studio/locales/zh-CN/course.json new file mode 100644 index 000000000..432cf63af --- /dev/null +++ b/wai/quantum-studio/locales/zh-CN/course.json @@ -0,0 +1,47 @@ +{ + "_meta": { + "locale": "zh-CN", + "area": "course", + "translation": "machine-assisted: to be reviewed by a fluent reader before it is advertised" + }, + "course.title": "课程", + "course.open": "课程", + "course.open_title": "量子线路课程:各课附有由引擎检查的练习", + "course.lessons": "各课", + "course.where": "第 {number}/{of} 课 · {unit}", + "course.exercise": "练习", + "course.starter": "载入起始程序", + "course.starter_title": "把练习的起始程序放进编辑器(你的草稿保留在“版本”中)", + "course.starter_loaded": "“{title}”的起始程序已放进编辑器。", + "course.check": "检查", + "course.check_title": "用引擎对照练习检查编辑器中的程序", + "course.hint": "提示", + "course.no_more_hints": "没有更多提示", + "course.hint_of": "提示 {level}/{of}", + "course.why": "哪里出了问题?", + "course.ask": "请助手给一条提示", + "course.ask_title": "这台机器上的模型,并被告知不要写出程序;它回答中的任何程序都会被扣下", + "course.ask_needs_model": "助手需要一个模型:请先在“助手”下连接一个模型,然后再问。", + "course.asking": "正在询问助手…", + "course.assistant_says": "助手的提示", + "course.held_solution": "回答中包含一个能通过这道练习的程序。为了让练习仍由你自己完成,它没有被显示。", + "course.held_code": "回答中包含代码,这里没有显示。", + "course.ask_failed": "助手无法回答:{error}", + "course.not_checked": "未检查", + "course.passed": "已通过", + "course.not_yet": "尚未通过", + "course.passed_status": "已通过:“{title}”。", + "course.not_yet_status": "尚未通过:“哪里出了问题?”会说明引擎发现了什么。", + "course.engine_says": "引擎原文:", + "course.nothing_wrong": "没有出问题:检查已经通过。", + "course.where_to_look": "从哪里看起:", + "course.previous": "← 上一课", + "course.next": "下一课 →", + "course.progress": "已通过 {passed}/{of} 课", + "course.kept": "只保存在这个浏览器中", + "course.reset": "清除进度", + "course.reset_confirm": "再点一次即清除", + "course.reset_done": "你在这个浏览器中的进度已清除。", + "course.companion": "配套课程:", + "course.machine_assisted": "本课为机器辅助翻译,尚待熟练的读者审校。" +} diff --git a/wai/quantum-studio/locales/zh-CN/critique_view.json b/wai/quantum-studio/locales/zh-CN/critique_view.json new file mode 100644 index 000000000..a5cf8b245 --- /dev/null +++ b/wai/quantum-studio/locales/zh-CN/critique_view.json @@ -0,0 +1,24 @@ +{ + "_meta": { + "locale": "zh-CN", + "area": "critique_view", + "translation": "machine-assisted: to be reviewed by a fluent reader before it is advertised" + }, + "critique_view.qubits": "量子比特", + "critique_view.gates": "门", + "critique_view.two_qubit": "双量子比特门", + "critique_view.depth": "深度", + "critique_view.two_qubit_depth": "双量子比特门深度", + "critique_view.non_clifford": "非 Clifford 门", + "critique_view.since_last_run": "相对上次运行的变化", + "critique_view.fidelity_title": "{note}。错误率:每个单量子比特门 {one_qubit},每个双量子比特门 {two_qubit},每个被测量的量子比特 {readout}。", + "critique_view.fidelity_modelled": "保真度(模型估计)", + "critique_view.line": "第 {line} 行", + "critique_view.lines": "第 {lines} 行", + "critique_view.label": "引擎的评析", + "critique_view.waiting": "等到引擎能读取这个程序时,才会给出评析。", + "critique_view.clifford_title": "每个门都是 Clifford 门:稳定子表可以在任意宽度上精确地运行它。", + "critique_view.show": "查看", + "critique_view.fix": "修正", + "critique_view.fix_title": "引擎重写这些行,并检查结果仍是同一个线路" +} diff --git a/wai/quantum-studio/locales/zh-CN/debugger.json b/wai/quantum-studio/locales/zh-CN/debugger.json new file mode 100644 index 000000000..854b92bff --- /dev/null +++ b/wai/quantum-studio/locales/zh-CN/debugger.json @@ -0,0 +1,46 @@ +{ + "_meta": { + "locale": "zh-CN", + "area": "debugger", + "translation": "machine-assisted: to be reviewed by a fluent reader before it is advertised" + }, + "debugger.start": "单步执行", + "debugger.start_title": "在引擎上单步执行程序,可前进也可后退", + "debugger.to_start": "回到开头", + "debugger.back": "后退一步", + "debugger.forward": "前进一步", + "debugger.to_end": "跳到结尾", + "debugger.slider": "显示状态的那一步", + "debugger.not_started": "未开始", + "debugger.pick": "要显示约化态的量子比特", + "debugger.against_noiseless": "与无噪声态对照", + "debugger.qubits": "量子比特", + "debugger.amplitudes": "振幅", + "debugger.reduced": "约化态", + "debugger.branches": "分支", + "debugger.title": "单步执行", + "debugger.empty": "在引擎上单步执行程序:每一步的状态、每个量子比特在其 Bloch 球上的位置,以及其测量的每个分支及其权重。", + "debugger.refused": "被拒绝", + "debugger.position": "位置 {position}/{length}", + "debugger.line": "(第 {line} 行)", + "debugger.next": "下一步:", + "debugger.end": "程序结束。", + "debugger.just_now": " · 刚才:{kind} q{qubit}{line} 读到 {outcome},p {p}({by})", + "debugger.by_recorded": "所选的分支", + "debugger.by_likelier": "概率较大者", + "debugger.by_certain": "确定", + "debugger.path": "路径:{outcomes} · 权重 {weight} · 比特 {bits}{read}", + "debugger.outcomes": "结果 {outcomes}", + "debugger.no_measurement": "尚无测量", + "debugger.no_bits": "无", + "debugger.entangled": "跨越 q{cut} | 其余部分的纠缠:{bits} 比特(Schmidt 秩 {rank})。", + "debugger.negativity": "跨越 q{cut} | 其余部分:对数负性 {value} 比特。", + "debugger.final": "最终:{final}{more}", + "debugger.more": ",另有 {n} 个", + "debugger.reads": "读到 {outcome} · p {p} · 权重 {weight}", + "debugger.branch_count_one": "{n} 个分支;权重 {weights}。", + "debugger.branch_count": "{n} 个分支;权重 {weights}。", + "debugger.tree_label": "程序中途测量处的分支", + "debugger.one_path": "没有中途测量:只有一条路径;{final}。", + "debugger.again": "重新开始" +} diff --git a/wai/quantum-studio/locales/zh-CN/device.json b/wai/quantum-studio/locales/zh-CN/device.json new file mode 100644 index 000000000..eacdf13f9 --- /dev/null +++ b/wai/quantum-studio/locales/zh-CN/device.json @@ -0,0 +1,60 @@ +{ + "_meta": { + "locale": "zh-CN", + "area": "device", + "translation": "machine-assisted: to be reviewed by a fluent reader before it is advertised" + }, + "device.by_readout": "读出误差", + "device.by_one": "单量子比特门误差", + "device.by_t1": "T1(越短越差)", + "device.map_label": "{name}:{qubits} 个量子比特、{pairs} 个耦合对,每个耦合对按其双量子比特门误差着色,每个量子比特按其{by}着色,从低到高分为五档{placed}", + "device.map_placed": ";程序放置在物理量子比特 {layout} 上", + "device.pair_tip": "耦合对 {a}–{b}:{gate} 误差 {error}", + "device.two_qubit_gate": "双量子比特门", + "device.qubit_tip": "量子比特 {qubit}{label}:读出误差 {readout},单量子比特门误差 {single},T1 {t_one} µs,T2 {t_two} µs", + "device.qubit_label": "({label})", + "device.ends": "q{logical} 最终位置", + "device.legend_pairs": "耦合对:双量子比特门误差", + "device.legend_qubits": "量子比特:{by}", + "device.legend_note": "每个色阶都取自本设备自身的数值:按分位数分为五档,最低的在前。虚线标记表示未给出数值。将指针悬停在量子比特或耦合对上可查看其数值。", + "device.as_table": "以表格显示", + "device.th_qubit": "量子比特", + "device.th_readout": "读出", + "device.th_error_1q": "单比特门误差", + "device.th_pair": "耦合对", + "device.th_error": "误差", + "device.th_gate": "门", + "device.target_label": "设备目标", + "device.none": "无:使用引擎自己的方法", + "device.colour_by": "量子比特着色依据", + "device.import": "导入…", + "device.import_title": "设备的校准快照或目标快照,可为引擎能读取的任何格式", + "device.title": "设备", + "device.run_as_on": "模拟运行于", + "device.about": "{qubits} 个量子比特,{pairs} 个耦合对 · 编译到 {basis}{costed} · 校准 {calibration}{when} · {source} · 哈希 {hash}…", + "device.costed_own": ",每个门按设备自身的门计算代价", + "device.unnamed": "未命名", + "device.when": ",{timestamp}", + "device.built_in": "内置(合成)", + "device.option": "{name} · {qubits} 个量子比特", + "device.yours": "你的(wai-quantum targets)", + "device.imported_here": "在此导入的", + "device.imported_app": "已导入 {name}({format}):{qubits} 个量子比特,保存在此应用中。", + "device.imported_browser": "已导入 {name}({format}):{qubits} 个量子比特,保存在此浏览器中。", + "device.import_failed": "无法读取该快照:{error}", + "device.placed": "放置在 {layout}{ending};{swaps} 次交换;预期成功率 {success},交换最少的方案为 {fewest}{noise}。", + "device.ending_on": ",最终位于 {layout}", + "device.placed_noise": ";{channels} 个噪声通道,历时 {ns} ns", + "device.row_target": "目标", + "device.name_calibration": "{name} · 校准 {calibration}", + "device.row_hash": "哈希", + "device.row_placed": "放置", + "device.placed_row": "位于 {layout} → {final};{swaps} 次交换;{basis} 中的 {gates} 个门", + "device.row_success": "预期成功率", + "device.success_row": "{success}(交换最少时:{fewest})", + "device.row_noise": "噪声", + "device.noise_row": "{channels} 个通道,历时 {ns} ns,其中空闲 {idle} ns", + "device.row_ideal": "与理想结果的差距", + "device.ideal_row": "总变差距离 {tvd},Hellinger 保真度 {fidelity}", + "device.model_note": "这是设备的模型,而非在设备上的运行:{model}" +} diff --git a/wai/quantum-studio/locales/zh-CN/dom.json b/wai/quantum-studio/locales/zh-CN/dom.json new file mode 100644 index 000000000..cd7789c39 --- /dev/null +++ b/wai/quantum-studio/locales/zh-CN/dom.json @@ -0,0 +1,14 @@ +{ + "_meta": { + "locale": "zh-CN", + "area": "dom", + "translation": "machine-assisted: to be reviewed by a fluent reader before it is advertised" + }, + "dom.not_checked": "未检查", + "dom.how_tableau": "tableau 相同", + "dom.how_exact": "在每个基态上", + "dom.how_rewrites": "通过经检查的重写", + "dom.how_random": "在随机输入上", + "dom.same_circuit": "同一线路 · {how}", + "dom.different_circuit": "不同的线路" +} diff --git a/wai/quantum-studio/locales/zh-CN/estimate_view.json b/wai/quantum-studio/locales/zh-CN/estimate_view.json new file mode 100644 index 000000000..32c7c4e7b --- /dev/null +++ b/wai/quantum-studio/locales/zh-CN/estimate_view.json @@ -0,0 +1,63 @@ +{ + "_meta": { + "locale": "zh-CN", + "area": "estimate_view", + "translation": "machine-assisted: to be reviewed by a fluent reader before it is advertised" + }, + "estimate_view.qubit_ns_e4": "ns-e4 · 50 ns 门,1e-4", + "estimate_view.qubit_ns_e3": "ns-e3 · 50 ns 门,1e-3", + "estimate_view.qubit_us_e4": "us-e4 · 100 µs 门,1e-4", + "estimate_view.qubit_us_e3": "us-e3 · 100 µs 门,1e-3", + "estimate_view.qubit_meas_e4": "meas-e4 · 基于测量,1e-4", + "estimate_view.qubit_meas_e6": "meas-e6 · 基于测量,1e-6", + "estimate_view.days": "{n} 天", + "estimate_view.years": "{n} 年", + "estimate_view.frontier_label_one": "前沿:{n} 个估算,运行时间与物理量子比特数的关系,双对数刻度", + "estimate_view.frontier_label_other": "前沿:{n} 个估算,运行时间与物理量子比特数的关系,双对数刻度", + "estimate_view.runtime": "运行时间", + "estimate_view.physical_qubits": "物理量子比特", + "estimate_view.point_label": "减速倍数 {slowdown}:{qubits} 个物理量子比特,{runtime},码距 {distance},{factories} 个工厂", + "estimate_view.point_title": "减速倍数 {slowdown}:{qubits} 个物理量子比特,{runtime}", + "estimate_view.the_estimate": "本估算", + "estimate_view.qubit_label": "物理量子比特", + "estimate_view.qubit_title": "构成这台机器的物理量子比特", + "estimate_view.budget_aria": "错误预算", + "estimate_view.budget_title": "允许这次运行失败的概率,由逻辑错误、蒸馏和旋转门的合成共同分摊", + "estimate_view.model_label": "码模型", + "estimate_view.model_title": "基于门的表面码所用的模型:拟合本引擎的精确匹配(exact matching)或并查集(union-find)解码器,或采用平面模型假定的模型", + "estimate_view.fit_exact_matching": "拟合:精确匹配", + "estimate_view.fit_union_find": "拟合:并查集", + "estimate_view.assumed_model": "假定模型", + "estimate_view.estimate": "估算", + "estimate_view.estimate_title": "容错量子计算机运行这个程序需要什么,每个估算都附有签名", + "estimate_view.heading": "估算", + "estimate_view.budget": "预算", + "estimate_view.ancillas_one": "(含 {n} 个辅助量子比特)", + "estimate_view.ancillas_other": "(含 {n} 个辅助量子比特)", + "estimate_view.depths": ";T 深度 {t_depth},深度 {depth}", + "estimate_view.counts": "{logical} 个逻辑量子比特{ancillas},{measurements} 次测量,{t} 个 T 门,{toffolis} 个 Toffoli 门,{rotations} 个旋转门(合成后的门序列中共 {rotation_t} 个 T 门){depths}{bound}。", + "estimate_view.bound_upper": ";程序含有分支,因此这些数字是任意一次运行的上界", + "estimate_view.synth": "每个旋转门的误差不超过 {each}(合成在预算 {budget} 中分得 {synthesis}),每个需要 {per} 个 T 门。", + "estimate_view.synth_none": "预算 {budget};没有需要合成的旋转门。", + "estimate_view.calibration": "码距由 P(d) = a·(p/p*)^((d+1)/2) 得出,其中 a = {prefactor},p* = {threshold},拟合自本引擎的 {label} 解码器{extrapolated}。", + "estimate_view.extrapolated": ",并外推到了实测范围 p = 0.002–0.005 之外", + "estimate_view.factories_one": "{n} 个工厂", + "estimate_view.factories_other": "{n} 个工厂", + "estimate_view.readout": "{which}:{qubits} 个物理量子比特({factories}),{runtime},码距 {distance}。", + "estimate_view.the_estimate_cap": "本估算", + "estimate_view.slowed": "减速 ×{slowdown}", + "estimate_view.col_slowdown": "减速倍数", + "estimate_view.col_d": "码距", + "estimate_view.col_factories": "工厂", + "estimate_view.this_key": "此密钥", + "estimate_view.verified_one": "{n} 个估算已用 {signer} 签封:签封验证通过,在这里重新运行请求也得出了每一个数字。", + "estimate_view.verified_other": "{n} 个估算已用 {signer} 签封:签封验证通过,在这里重新运行请求也得出了每一个数字。", + "estimate_view.not_verified": "签封在这里未通过验证(已签名:{signed},回执:{receipts},已复现:{reproduced})。", + "estimate_view.t_factories_one": "{n} 个 T 工厂", + "estimate_view.t_factories_other": "{n} 个 T 工厂", + "estimate_view.summary": "在 {qubit} 上:{qubits} 个物理量子比特({factories},占物理量子比特的 {share}%),运行 {runtime},码距 {distance}({code})。", + "estimate_view.model_note": "这是对一个模型的估算:平面模型的假设(噪声均匀且相互独立、T 态的传输不计代价、T 态的消耗平稳)适用于这里的每一个数字。放慢程序所需的工厂更少;前沿保留每一个不被其他估算在两方面同时胜过的估算。", + "estimate_view.bad_budget": "预算是 0 到 1 之间的概率,例如 1e-3。", + "estimate_view.estimating": "正在估算:统计程序、合成每个旋转门、搜索工厂配置…", + "estimate_view.stale": "这是针对程序较早版本的结果:请重新估算。" +} diff --git a/wai/quantum-studio/locales/zh-CN/examples.json b/wai/quantum-studio/locales/zh-CN/examples.json new file mode 100644 index 000000000..2e1d59a81 --- /dev/null +++ b/wai/quantum-studio/locales/zh-CN/examples.json @@ -0,0 +1,21 @@ +{ + "_meta": { + "locale": "zh-CN", + "area": "examples", + "translation": "machine-assisted: to be reviewed by a fluent reader before it is advertised" + }, + "examples.bell_title": "Bell 对", + "examples.bell_shows": "两个相互纠缠的量子比特:只会出现 00 和 11,且 ⟨Z0 Z1⟩ 恰好为 1。", + "examples.ghz_title": "GHZ 态,40 个量子比特", + "examples.ghz_shows": "一个超出任何态矢量能力的 Clifford 线路,在稳定子表上精确运行:结果要么全为 0,要么全为 1。", + "examples.rotation_title": "一个旋转门", + "examples.rotation_shows": "任意角度都可以在双精度态矢量上运行:⟨Z0⟩ = cos 0.3,⟨Y0⟩ = −sin 0.3。", + "examples.qft_title": "量子傅里叶变换", + "examples.qft_shows": "三个量子比特上一个基态的傅里叶变换:每个结果出现的可能性都相同,信息全在振幅的相位里。", + "examples.teleport_title": "量子隐形传态", + "examples.teleport_shows": "通过测量和校正,把一个态从量子比特 0 移到量子比特 2:⟨Z2⟩ = cos 0.7,⟨X2⟩ = sin 0.7。", + "examples.grover_title": "Grover 搜索,3 个量子比特", + "examples.grover_shows": "两轮振幅放大以 0.945 的概率找到被标记的项 101。", + "examples.noisy_bell_title": "有噪声的 Bell 对", + "examples.noisy_bell_shows": "同样的一对量子比特,放在门会随机出错的设备上:出现了 01 和 10,⟨Z0 Z1⟩ 也降到 1 以下。" +} diff --git a/wai/quantum-studio/locales/zh-CN/flags.json b/wai/quantum-studio/locales/zh-CN/flags.json new file mode 100644 index 000000000..8249fd9ea --- /dev/null +++ b/wai/quantum-studio/locales/zh-CN/flags.json @@ -0,0 +1,21 @@ +{ + "_meta": { + "locale": "zh-CN", + "area": "flags", + "translation": "machine-assisted: to be reviewed by a fluent reader before it is advertised" + }, + "flags.critique": "实时线路图上的引擎评析", + "flags.assistant_asks": "助手在提出建议之前先提问,且只在被问到时发言", + "flags.alternatives": "备选方案:沿引擎的度量轴改进与探索", + "flags.predict_first": "运行之前先预测", + "flags.workbench": "工作台", + "flags.rules": "建议的规则", + "flags.shortcuts": "经典捷径视角", + "flags.novelty": "新颖之处:标签与“发现”", + "flags.taste": "你的品味", + "flags.rewrites": "重写视角", + "flags.convergence": "你自己的方式", + "flags.note_locked": "你参与的研究设定了它所指定的功能,以便每一项都能与对照组比较;离开研究后才能更改这些功能。", + "flags.note": "可以关闭任何功能;Studio 会把你的选择保存在这个浏览器中。", + "flags.title": "功能" +} diff --git a/wai/quantum-studio/locales/zh-CN/frame.json b/wai/quantum-studio/locales/zh-CN/frame.json new file mode 100644 index 000000000..3517dfb0e --- /dev/null +++ b/wai/quantum-studio/locales/zh-CN/frame.json @@ -0,0 +1,188 @@ +{ + "_meta": { + "locale": "zh-CN", + "area": "frame", + "translation": "machine-assisted: to be reviewed by a fluent reader before it is advertised" + }, + "method.exact": "精确态矢量", + "method.statevector": "态矢量,双精度", + "method.statevector_single": "GPU 上的态矢量,单精度", + "method.stabilizer": "稳定子表", + "method.pauli": "稀疏 Pauli 动力学", + "method.mps": "矩阵乘积态", + "method.tensor_network": "张量网络", + "method.cutrank": "割秩求和", + "method.cutrank_single": "GPU 上的割秩求和,单精度", + "method.density_matrix": "密度矩阵", + "method.trajectories": "轨迹采样", + "accuracy.bit-exact": "在每台机器上都得到相同的比特。", + "accuracy.exact": "除双精度舍入外没有任何近似。", + "accuracy.single": "除单精度舍入外没有任何近似,每个门约 10⁻⁷。", + "accuracy.bounded": "一种近似,每个值都附有经证明的误差界。", + "accuracy.estimate": "一种统计估计,每个值都附有标准误差。", + "accuracy.heuristic": "一种近似,附有误差的标准估计,但没有误差界。", + "noise.depolarizing_1q": "单比特去极化", + "noise.depolarizing_2q": "双比特去极化", + "noise.amplitude_damping": "振幅阻尼", + "noise.dephasing": "退相位", + "noise.readout": "读出翻转", + "frame.under_ms": "不到 1 ms", + "frame.bits": "{n} 个比特", + "frame.run_title": "运行(Ctrl 或 ⌘ + Enter)", + "frame.run": "▶ 运行", + "frame.cancel": "取消", + "frame.predict_title": "每次运行之前先说出你的预期,再看看相差多少", + "frame.predict": "先预测", + "frame.this_computer": "这台计算机", + "frame.this_browser": "这个浏览器", + "frame.the_service": "作业服务", + "frame.examples_label": "载入示例", + "frame.examples": "示例…", + "frame.studio": "Quantum Studio", + "frame.run_on": "运行于", + "frame.held": "已锁定第 {a} 到 {z} 行:任何建议都不能改动它们。删除标记即可解除锁定。", + "frame.review_label": "待审阅的建议", + "frame.copy_link": "复制链接", + "frame.copy_link_title": "一个把程序放在片段(fragment)中的链接,任何服务器都看不到它", + "frame.open": "打开", + "frame.save": "保存", + "frame.shots": "采样次数", + "frame.seed": "种子", + "frame.auto_title": "该程序所允许的最强方法", + "frame.method": "方法", + "frame.observables": "可观测量", + "frame.amplitudes": "振幅", + "frame.device_noise": "已选择设备:噪声是该设备自身的噪声,来自它的校准数据。", + "frame.noise": "噪声", + "frame.program": "程序", + "frame.circuit_text_label": "以文本绘制的线路", + "frame.saved": "已保存 {path}。", + "frame.saved_drawing": "已保存 {name},即引擎绘制的图。", + "frame.svg_title": "把线路图保存为 SVG 图像", + "frame.latex_title": "把线路图保存为 LaTeX(纯 TikZ)", + "frame.choose_first": "请先对正在审阅的建议作出选择。", + "frame.text": "文本", + "frame.text_title": "以文本形式显示线路,便于复制", + "frame.diagram": "线路图", + "frame.circuit": "线路", + "frame.ready": "就绪", + "frame.result_empty": "运行程序后,结果会显示在这里。计数以经典比特为键,最高位在前。", + "frame.result": "结果", + "frame.history": "历史", + "frame.clear": "清除", + "ai.server": "模型服务器", + "ai.model": "模型", + "ai.model_placeholder": "服务器默认的模型", + "ai.not_connected": "未连接", + "ai.connect": "连接", + "ai.when_asked": "只在我提问时", + "ai.when_stuck": "在我似乎卡住时主动给出提示", + "ai.how": "帮助方式", + "ai.when": "何时发言", + "ai.settings_note": "默认使用这台机器上的模型:任何提供 chat-completions 接口和工具调用的服务器均可。除非地址指向别处,否则任何内容都不会离开这台机器。", + "ai.intro": "可以就这个程序提问。助手只在被问到时发言,并用引擎核对它所说的内容:每个数字都来自一次运行,它提出的每个程序在给出之前都已在这里读取并运行过。", + "ai.placeholder": "就这个程序提问…", + "ai.ask_label": "向助手提问", + "ai.ask": "提问", + "ai.stuck": "卡住了?请求一条提示", + "ai.q.question": "问我一个问题", + "ai.q.hint": "提示", + "ai.q.explain": "解释这条线路", + "ai.q.method": "为什么用这种方法?", + "ai.q.fix": "修正问题", + "ai.title": "助手", + "ai.gear": "模型…", + "frame.engine_not_loaded": "引擎未加载", + "frame.lint_at": ",取值 {values}", + "frame.lint": "{qubits} 个量子比特 · {gates} 个门", + "frame.lint_dynamic": " · 含中途测量", + "frame.problem": "有问题", + "frame.editor_label": "OpenQASM 程序", + "frame.plan": "{method} · 至多 {mib} MiB", + "frame.your_prediction": "你的预测", + "frame.running": "运行中", + "frame.running_on": "正在{where}上运行…", + "frame.on_service": "在作业服务上:{status}…", + "frame.done": "已在{where}上完成,用时 {took}。", + "frame.cancelled_badge": "已取消", + "frame.refused": "被拒绝", + "frame.cancelled": "已取消。", + "frame.verdict": "判定", + "frame.accuracy": "精度", + "frame.working_out": "正在这里推算…", + "frame.counts": "计数 · {total} 次采样 · 最高位在前", + "frame.more_outcomes": "还有 {n} 个结果。", + "frame.expectations": "期望值", + "frame.th_state": "状态", + "frame.th_amplitude": "振幅", + "frame.th_probability": "概率", + "frame.th_phase": "相位", + "frame.checking": "检查中…", + "frame.download_job": "下载作业", + "frame.receipt": "回执", + "frame.ran_on": "运行于", + "frame.signed_by": "签名者", + "frame.check": "检查", + "frame.ran_in": "用时", + "frame.timed_here": "{took},由本页计时", + "frame.energy": "能耗", + "frame.written_by": "作者", + "frame.account": "署名核对", + "frame.receipt_note": "回执表明这把密钥为这个请求和这个结果签了名;任何人都可以再次运行该请求进行比较。它并不说明密钥由谁持有。", + "frame.no_verdict": "没有判定", + "frame.key_service": "作业服务的密钥", + "frame.key_computer": "这台计算机的密钥", + "frame.key_browser": "这个浏览器的密钥", + "frame.key_named": "它所指明的密钥", + "frame.verifies": "✓ 验证通过,在{where}上检查", + "frame.here_computer": "这台计算机", + "frame.here_device": "这台设备", + "frame.does_not_verify": "✗ 验证未通过", + "frame.not_checked": "未检查:{error}", + "frame.recomputed": "✓ 已根据“版本”中的 {version} 重新计算", + "frame.not_in_computer": "不在这台计算机的版本中", + "frame.not_in_browser": "不在这个浏览器的版本中", + "frame.took": "采纳了来自{who}的 {offered} 处改动中的 {accepted} 处{stops},记为 {version}。", + "frame.took_none": "没有采纳来自{who}的建议{stops};“版本”中的 {version} 保留了它。", + "frame.blocked": ";{n} 处被你的规则阻止", + "frame.no_rewrite": "引擎现在没有针对它的重写;程序可能已经改变。", + "frame.fix_blocked": "你的规则阻止了这个修正:它会改动规则所锁定的内容。“版本”中的 {version} 保留了它。", + "frame.fixed_shows": "已修正:{message}。引擎检查了结果:{why}。", + "frame.fixed": "已修正,记为 {version}。撤销即可恢复这些行。", + "frame.saved_job": "已保存 {name}。可用 wai-quantum verify 检查它。", + "frame.opened": "已打开 {name}。", + "frame.copied": "已复制一个把程序放在片段中的链接,任何服务器都看不到它。", + "frame.from_history": "来自历史:在{where}上运行,{when}。", + "frame.history_empty": "你运行的作业会出现在这里,保存在这台设备上。", + "frame.method_reach": "{method}(至多 {n} 个量子比特)", + "ai.thinking": "思考中…", + "ai.used": "使用了 {name}", + "ai.retry": "引擎无法读取所提出的程序({error});已要求修正。", + "ai.does_not_parse": "无法解析", + "ai.checked": "已检查:{qubits} 个量子比特,{gates} 个门", + "ai.ran_here": " 已在这里用{method}运行", + "ai.did_not_run": " 未运行:{error}", + "ai.organic": " 你正在独自探索:在“版本”下结束独自探索后才能采纳它。", + "ai.apply": "采纳", + "ai.unsourced": "不是来自运行:{list}。这些是模型自己给出的,引擎没有计算过它们。", + "ai.tokens": "输入 {into} 个 token,输出 {out} 个", + "ai.error": "助手无法回答:{error}。请在“模型…”下检查模型服务器。", + "ai.connecting": "连接中…", + "ai.connected": "已连接", + "frame.from_link": "来自链接的程序。", + "frame.signs_as": "签名身份 {did}…({whose})", + "frame.key_made_browser": "为这个浏览器生成的密钥", + "frame.service_silent": "作业服务没有响应", + "frame.versions_error": "版本:{error}", + "frame.lesson_loaded": "“{title}”的起始程序。", + "frame.language": "语言", + "frame.machine_assisted": "此界面为机器辅助翻译,尚待熟练的读者审校。", + "frame.where_browser": "这个浏览器", + "frame.where_service": "作业服务", + "frame.where_computer": "这台计算机", + "frame.gates_by": "{n} 个出自{who}", + "frame.written": "{parts} · 共 {of} 个门,{version}", + "frame.no_gates": "没有门", + "frame.where_hardware": "你的硬件", + "frame.where_provider": "你的提供方 {name}" +} diff --git a/wai/quantum-studio/locales/zh-CN/gallery.json b/wai/quantum-studio/locales/zh-CN/gallery.json new file mode 100644 index 000000000..f4b1ae537 --- /dev/null +++ b/wai/quantum-studio/locales/zh-CN/gallery.json @@ -0,0 +1,51 @@ +{ + "_meta": { + "locale": "zh-CN", + "area": "gallery", + "translation": "machine-assisted: to be reviewed by a fluent reader before it is advertised" + }, + "gallery.atlas_label": "草稿与 {n} 个备选方案,按双量子比特门数和深度排布", + "gallery.axis_two_qubit": "双量子比特门 →", + "gallery.axis_depth": "↑ 深度", + "gallery.draft_point": "草稿:{two_qubit} 个双量子比特门,深度 {depth}", + "gallery.point_label": "备选方案 {n}:{two_qubit} 个双量子比特门,深度 {depth}", + "gallery.point_title": "{n}. {label}:{two_qubit} 个双量子比特门,深度 {depth}", + "gallery.aim_placeholder": "目标… 更少的量子比特、另一种纠缠方式、更多对称性", + "gallery.aim_label": "“探索”应以什么为目标", + "gallery.improve": "改进", + "gallery.improve_title": "同一条线路,代价更低:每一个都经检查与草稿等价", + "gallery.explore": "探索", + "gallery.explore_title": "朝着目标的不同线路,来自模型,每一个都由引擎度量", + "gallery.intro": "改进:同一条线路,代价更低,每一个都经检查等价。探索:朝着某个目标的不同线路,来自模型,每一个都由引擎度量。在你应用其中一个之前,什么都不会改变。", + "gallery.title": "备选方案", + "gallery.how_one": "如何得到:{n} 次重写,每次都经检查", + "gallery.how_many": "如何得到:{n} 次重写,每次都经检查", + "gallery.how_reordered": "如何得到:相同的门,换了一种顺序", + "gallery.and_this_one": "而这一个经以下步骤化简为同一结果:", + "gallery.commute": "于是,除可交换的门的顺序之外,两者完全相同。", + "gallery.does_not_parse": "无法解析:{error}", + "gallery.by_taste_one": "按你的品味排序({profile},{n} 次选择)。", + "gallery.by_taste_many": "按你的品味排序({profile},{n} 次选择)。", + "gallery.from_engine": "引擎", + "gallery.from_model": "模型", + "gallery.lines_changed": "改动了 {n} 行", + "gallery.same_lines": "各行相同", + "gallery.program": "程序", + "gallery.apply": "应用", + "gallery.nothing_to_improve": "没有引擎能够检查的可改进之处:没有可以相消或合并的门,模型也没有提出它能确认的改进。", + "gallery.nothing_read": "模型提出的内容,引擎都无法读取。", + "gallery.not_offered": "未提供:{list}。", + "gallery.engine_looking": "引擎正在查找…", + "gallery.cannot_read": "引擎无法读取草稿:{error}", + "gallery.paused_engine_only": "你正在独自探索:这些都只来自引擎。", + "gallery.connect_for_rewrites": "在“助手”的“模型…”下连接一个模型,也可以获得它的重写。", + "gallery.a_rewrite": "一个重写", + "gallery.model_failed_engine_only": "模型没有回答({error});这些都只来自引擎。", + "gallery.breaks_rules": "{label}:它违反了你的规则:{why}", + "gallery.not_same": "{label}:不是同一条线路({why})", + "gallery.paused_explore": "你正在独自探索:“探索”需要询问模型,所以它会等到你在“版本”下结束独自探索。“改进”无需模型也能使用。", + "gallery.explore_needs_model": "“探索”需要向模型征求想法,但没有连接任何模型。请在“助手”的“模型…”下连接一个;“改进”无需模型也能使用。", + "gallery.model_imagining": "模型正在构想;引擎将度量每一个想法…", + "gallery.an_idea": "一个想法", + "gallery.model_failed": "模型没有回答:{error}" +} diff --git a/wai/quantum-studio/locales/zh-CN/generators.json b/wai/quantum-studio/locales/zh-CN/generators.json new file mode 100644 index 000000000..e304920b1 --- /dev/null +++ b/wai/quantum-studio/locales/zh-CN/generators.json @@ -0,0 +1,11 @@ +{ + "_meta": { + "locale": "zh-CN", + "area": "generators", + "translation": "machine-assisted: to be reviewed by a fluent reader before it is advertised" + }, + "generators.inputs": "生成器的输入", + "generators.read_at": "生成器,取值为", + "generators.one_circuit": "即它所描述的线路族中的一个线路;工作台会在每种规模下检查它。", + "generators.value_of": "{name} 的值" +} diff --git a/wai/quantum-studio/locales/zh-CN/hardware.json b/wai/quantum-studio/locales/zh-CN/hardware.json new file mode 100644 index 000000000..c861d2b18 --- /dev/null +++ b/wai/quantum-studio/locales/zh-CN/hardware.json @@ -0,0 +1,24 @@ +{ + "_meta": { + "locale": "zh-CN", + "area": "hardware", + "translation": "machine-assisted: to be reviewed by a fluent reader before it is advertised" + }, + "hardware.note": "在你自己的硬件上运行,须经由桌面应用或命令行(wai-quantum run --provider NAME):浏览器标签页不得持有提供方所需的凭据。", + "hardware.cost_unknown": "费用未知:{why}", + "hardware.free": "免费", + "hardware.billed_by": "(按 {unit} 计费)", + "hardware.about_seconds": ",约 {seconds} 秒", + "hardware.confirm_run": "在 {name}({device},{provenance})上运行?", + "hardware.confirm_cost": "此次运行不免费:{cost}。", + "hardware.confirm_compiled": "程序已编译为 {basis} 中的 {gates} 个原生门、{swaps} 次交换,并已在此处对照你的线路检查;测量 {shots} 次。", + "hardware.confirm_spend": "运行它会花费你账户的资金或配额。", + "hardware.choose_provider": "请先选择你的一个提供方", + "hardware.run_it": "运行", + "hardware.cancel": "取消", + "hardware.not_free_title": "此次运行不免费", + "hardware.segment": "你的硬件", + "hardware.segment_title": "在你自己的某个提供方上运行;不免费的运行会先征求你的同意", + "hardware.provider_label": "你的提供方", + "hardware.no_providers": "还没有提供方:用 wai-quantum providers add 添加一个,然后重新打开此列表。" +} diff --git a/wai/quantum-studio/locales/zh-CN/novelty.json b/wai/quantum-studio/locales/zh-CN/novelty.json new file mode 100644 index 000000000..0f6f9f8d2 --- /dev/null +++ b/wai/quantum-studio/locales/zh-CN/novelty.json @@ -0,0 +1,29 @@ +{ + "_meta": { + "locale": "zh-CN", + "area": "novelty", + "translation": "machine-assisted: to be reviewed by a fluent reader before it is advertised" + }, + "novelty.same_state": "从 |0…0⟩ 制备出相同的态", + "novelty.same_circuit": "是同一条线路", + "novelty.route": "通往 {name} 的另一条路径", + "novelty.route_why": "引擎发现它与 {name}({how}){what},但构造方式不同:两者相距 {distance},其中 0 表示构造相同,1 表示毫无共同之处。值得再看一眼。", + "novelty.known": "已知构造:{name}", + "novelty.known_why": "引擎发现它与 {name}({how}){what},且构造方式相同。", + "novelty.new": "新颖:与最近者({name})相距 {novelty}", + "novelty.new_why": "它的门及其顺序与所比较的 {compared} 个程序中最近的 {name} 相距 {novelty}(0 表示构造相同,1 表示毫无共同之处)。这是值得一看的理由,而不是声称没有人构造过它。", + "novelty.unasked": "在代价未计入的方面优于参照:{list}。", + "novelty.unasked_item": "{measure} {candidate},参照为 {reference}", + "novelty.kind_route": "另一条路径", + "novelty.kind_aberration": "异常", + "novelty.kind_novel": "远离已知", + "novelty.found_empty": "引擎认为值得再看一眼的内容会出现在这里:经由另一条路径得到的已知结果、只违反其规则之一的工作台候选、与所比较的一切都相距甚远的已验证条目。保存你想留下的;其余的忽略即可。", + "novelty.found_title": "发现", + "novelty.found_count": "已标记 {flagged} 个 · 已保存 {saved} 个", + "novelty.note_placeholder": "备注,与它一起保存", + "novelty.note_label": "关于 {title} 的备注", + "novelty.saved": "已保存", + "novelty.save": "保存", + "novelty.dismiss": "忽略", + "novelty.open": "在编辑器中打开" +} diff --git a/wai/quantum-studio/locales/zh-CN/predict.json b/wai/quantum-studio/locales/zh-CN/predict.json new file mode 100644 index 000000000..eea6a1ce9 --- /dev/null +++ b/wai/quantum-studio/locales/zh-CN/predict.json @@ -0,0 +1,28 @@ +{ + "_meta": { + "locale": "zh-CN", + "area": "predict", + "translation": "machine-assisted: to be reviewed by a fluent reader before it is advertised" + }, + "predict.keyed": "计数以经典比特为键,最高位在前。", + "predict.keyed_width": "计数以经典比特为键,最高位在前:共 {width} 位。", + "predict.not_bits": "结果是由 0 和 1 组成的字符串,最高位在前。", + "predict.wrong_width_one": "这个程序有 {width} 个经典比特;而你给出的结果有 {length} 位。", + "predict.wrong_width_other": "这个程序有 {width} 个经典比特;而你给出的结果有 {length} 位。", + "predict.bad_share": "占比是采样次数的百分比,取值为 0 到 100。", + "predict.before": "运行之前", + "predict.question": "你预计哪个结果出现得最多?它会占采样次数的多少?", + "predict.outcome_label": "最可能的结果", + "predict.share_label": "它的占比,%", + "predict.run": "运行", + "predict.skip": "不预测,直接运行", + "predict.yours": "你的预测", + "predict.no_counts": "这次运行没有给出可供对照的计数:请为作业设置采样次数。", + "predict.right": "你说的结果出现得最多;你给出的占比相差 {points} 个百分点。", + "predict.wrong": "出现得最多的是另一个结果:{top},占采样次数的 {share}。", + "predict.you_said": "你说的", + "predict.bits_in": "{bits} 占 {share}", + "predict.the_run": "运行结果", + "predict.first": "这是这台设备上保存的你的第一次预测;之后的每一次都会累计到这里的统计中。", + "predict.tally": "在这台设备上保存的 {n} 次预测中:你说中出现最多的结果 {right} 次,你给出的占比平均相差 {points} 个百分点。" +} diff --git a/wai/quantum-studio/locales/zh-CN/rewrite_view.json b/wai/quantum-studio/locales/zh-CN/rewrite_view.json new file mode 100644 index 000000000..c5add7bd2 --- /dev/null +++ b/wai/quantum-studio/locales/zh-CN/rewrite_view.json @@ -0,0 +1,24 @@ +{ + "_meta": { + "locale": "zh-CN", + "area": "rewrite_view", + "translation": "machine-assisted: to be reviewed by a fluent reader before it is advertised" + }, + "rewrite_view.rule_cancel": "抵消", + "rewrite_view.rule_merge": "合并", + "rewrite_view.rule_through": "移过", + "rewrite_view.rule_conjugate": "共轭", + "rewrite_view.rule_drop": "删去", + "rewrite_view.rule_move": "交换", + "rewrite_view.rewrites": "重写", + "rewrite_view.rewrites_title": "每一个保持程序含义不变的重写,每个都经引擎检查", + "rewrite_view.heading": "重写", + "rewrite_view.take": "采纳", + "rewrite_view.offer_note": "经引擎检查的一次重写:{why}。", + "rewrite_view.lines_one": "第 {lines} 行", + "rewrite_view.lines_other": "第 {lines} 行", + "rewrite_view.checking": "引擎正在检查每一个重写…", + "rewrite_view.unread": "引擎无法读取这个程序:{error}", + "rewrite_view.none": "没有适用的重写:没有可以抵消、合并或共轭的门,也没有彼此相邻且可交换的两个门。", + "rewrite_view.note": "每一个重写都保持线路的含义:引擎在相关的量子比特上,把重写取走的门与它放入的门相对照进行检查。一次采纳一个;审阅中会显示改动。" +} diff --git a/wai/quantum-studio/locales/zh-CN/rules.json b/wai/quantum-studio/locales/zh-CN/rules.json new file mode 100644 index 000000000..df1d53ba0 --- /dev/null +++ b/wai/quantum-studio/locales/zh-CN/rules.json @@ -0,0 +1,21 @@ +{ + "_meta": { + "locale": "zh-CN", + "area": "rules", + "translation": "machine-assisted: to be reviewed by a fluent reader before it is advertised" + }, + "rules.title": "针对建议的规则", + "rules.hold": "锁定选中的行", + "rules.hold_title": "用 {lock} 和 {unlock} 标记它们:任何建议都不能改动它们", + "rules.region": "把下一个建议限定在选中的行内", + "rules.gates": "建议可以添加的门", + "rules.gates_any": "任意", + "rules.avoid_gates": "要避免的门", + "rules.none": "无", + "rules.avoid_text": "要避免的文本", + "rules.avoid_text_placeholder": "无,或多段文本,用逗号分隔", + "rules.qubits": "锁定量子比特声明", + "rules.note": "引擎会逐处对照这些规则检查每一个建议:助手的建议、备选方案、修正、智能体的建议。违反任意一条规则的改动都不能被采纳。{lock} 与 {unlock} 之间的行被锁定。你自己输入的内容,你可以随意改动。", + "rules.only_lines": "下一个建议仅限第 {first} 至 {last} 行 ", + "rules.clear_region": "取消行范围限制" +} diff --git a/wai/quantum-studio/locales/zh-CN/shortcuts_view.json b/wai/quantum-studio/locales/zh-CN/shortcuts_view.json new file mode 100644 index 000000000..96df05084 --- /dev/null +++ b/wai/quantum-studio/locales/zh-CN/shortcuts_view.json @@ -0,0 +1,40 @@ +{ + "_meta": { + "locale": "zh-CN", + "area": "shortcuts_view", + "translation": "machine-assisted: to be reviewed by a fluent reader before it is advertised" + }, + "shortcuts_view.route_stabilizer": "稳定子表", + "shortcuts_view.route_cutrank": "割秩和", + "shortcuts_view.route_mps": "精确矩阵乘积态", + "shortcuts_view.route_light_cone": "光锥", + "shortcuts_view.route_tensor_network": "张量网络", + "shortcuts_view.route_statevector": "态矢量", + "shortcuts_view.under_ms": "不到 1 ms", + "shortcuts_view.days": "{n} 天", + "shortcuts_view.years": "{n} 年", + "shortcuts_view.size_widest_cut": "最宽割 {n} 比特", + "shortcuts_view.size_product_state": "一个乘积态", + "shortcuts_view.size_bond": "键维 ≤ 2^{n}", + "shortcuts_view.size_cone": "{n} 个量子比特的光锥", + "shortcuts_view.size_largest_tensor": "最大张量 2^{n}", + "shortcuts_view.size_amplitudes": "2^{n} 个振幅", + "shortcuts_view.size_tableau": "一个稳定子表", + "shortcuts_view.classical_cost": "经典代价", + "shortcuts_view.classical_cost_title": "引擎所知的、适用于这个程序的每一条精确经典路线,以及各自的代价", + "shortcuts_view.thorough": "深入搜索", + "shortcuts_view.thorough_title": "另外搜索收缩树和更长的割秩顺序:大型程序需要几秒到几分钟", + "shortcuts_view.heading": "经典计算", + "shortcuts_view.unread": "引擎无法读取这个程序:{error}", + "shortcuts_view.does_not_apply": "不适用", + "shortcuts_view.not_searched": "未搜索", + "shortcuts_view.col_route": "路线", + "shortcuts_view.col_size": "规模", + "shortcuts_view.col_memory": "内存", + "shortcuts_view.col_operations": "运算次数", + "shortcuts_view.col_rate": "按 10¹⁰/s", + "shortcuts_view.note": "每条路线都是精确的;运算次数按得到第一个样本、振幅或期望值计算。时间按参考速率换算,并非实测。{note}.", + "shortcuts_view.searching": "正在搜索每条路线,其中包括收缩树:大型程序可能需要几分钟…", + "shortcuts_view.pricing": "正在估算每条路线的代价…", + "shortcuts_view.stale": "这是针对程序较早版本的结果:请重新检查。" +} diff --git a/wai/quantum-studio/locales/zh-CN/state_views.json b/wai/quantum-studio/locales/zh-CN/state_views.json new file mode 100644 index 000000000..d0630215b --- /dev/null +++ b/wai/quantum-studio/locales/zh-CN/state_views.json @@ -0,0 +1,25 @@ +{ + "_meta": { + "locale": "zh-CN", + "area": "state_views", + "translation": "machine-assisted: to be reviewed by a fluent reader before it is advertised" + }, + "state_views.amp_label": "概率 {p},相位 {phase}π", + "state_views.prob_label": "概率 {p}", + "state_views.amps_label": "这一步的振幅", + "state_views.col_state": "态", + "state_views.col_amplitude": "振幅", + "state_views.col_probability": "概率", + "state_views.col_phase": "相位", + "state_views.amps_note": "列出了 {nonzero} 个非零基态中的 {listed} 个(基态共 {of} 个);纯度 {purity}{mixed};摘要 {digest}…", + "state_views.mixed": "(混合态:只有概率,没有振幅)", + "state_views.bloch_label": "量子比特 {qubit}:Bloch 矢量 ({x}, {y}, {z}),长度 {length}", + "state_views.qubit_label": "量子比特 {qubit}:读到 1 的概率 {prob},纯度 {purity}", + "state_views.caption": " p(1) {prob} · 纯度 {purity}{twin}", + "state_views.without_noise_z": " · 无噪声时 z {z}", + "state_views.choose_qubits": "最多选择六个量子比特,查看它们的约化态。", + "state_views.rho_label": "量子比特 {qubits} 的约化密度矩阵", + "state_views.rho_note": "量子比特 {qubits}(第一个为最高位):纯度 {purity},熵 {entropy} 比特。图中显示每个矩阵元的大小;悬停可查看其值。", + "state_views.without_noise": "无噪声时 {p}", + "state_views.overlay_note": "条形表示有噪声态的概率;每个标记表示同一路径上无噪声态的概率。与它的保真度为 {fidelity}。" +} diff --git a/wai/quantum-studio/locales/zh-CN/study.json b/wai/quantum-studio/locales/zh-CN/study.json new file mode 100644 index 000000000..790cb3fb9 --- /dev/null +++ b/wai/quantum-studio/locales/zh-CN/study.json @@ -0,0 +1,60 @@ +{ + "_meta": { + "locale": "zh-CN", + "area": "study", + "translation": "machine-assisted: to be reviewed by a fluent reader before it is advertised" + }, + "study.measure_quality": "经引擎证明等价且让程序更省的编辑所占的比例", + "study.measure_quality_trend": "该比例:后期相对前期", + "study.measure_selection": "你选中所示候选中引擎最优者的频率", + "study.measure_calibration_off": "你预测的比例与运行结果的偏差", + "study.measure_calibration_slope": "运行结果的比例相对你的预测(1 表示校准良好)", + "study.measure_unassisted": "不借助助手时你的探测任务得分(1 表示与参考答案相当)", + "study.measure_ownership": "你关于归属感的回答,1 到 7", + "study.measure_authorship": "程序的门中出自你手的比例", + "study.measure_novelty": "你的设计与已知构造的距离", + "study.measure_diversity": "你做出的不同设计的数量(Vendi)", + "study.measure_convergence": "你的程序的门中出自模型的比例", + "study.title": "研究", + "study.load": "载入研究方案", + "study.keep": "在这个浏览器中记录我的工作方式", + "study.enrolled": "你正在参与“{study}”,条件组 {condition},由你的密钥决定。在你导出之前,你的记录一直保留在这个浏览器中。", + "study.intro": "参与一项关于人们如何借助引擎进行设计的研究:研究者会给你一份在收集任何数据之前就已封存的研究方案,你可以先看它研究什么,再决定是否参与。也可以只为自己保留一份记录。", + "study.kept": "这里保存着:在候选之间的 {choices}、{probes}、关于归属感的 {answers},以及你的预测和历史。", + "study.choice_one": "{n} 次选择", + "study.choices": "{n} 次选择", + "study.probe_one": "{n} 个探测任务", + "study.probes": "{n} 个探测任务", + "study.answer_one": "{n} 个回答", + "study.answers": "{n} 个回答", + "study.own": "这有多少是你的?", + "study.probe": "做一个探测任务", + "study.my_record": "我的记录", + "study.export": "导出我的记录", + "study.leave": "退出研究", + "study.forget": "清除已保存的内容", + "study.hypotheses": "它研究什么(在收集任何数据之前写下):", + "study.compares": "它比较 {n} 个条件,每个条件是 Studio 功能的一种设置;你的密钥为你分配其中之一。主要指标:{primary}。该研究方案已封存(摘要 {digest}…),因此在你加入之后无法更改。", + "study.join": "参与", + "study.not_now": "暂不", + "study.not_protocol": "这不是引擎能打开的研究方案:{error}", + "study.left": "你已退出研究。已保存的内容会一直留在这个浏览器中,直到你清除它。", + "study.save_answers": "保存我的回答", + "study.answer_all": "请回答每一项", + "study.saved_answers": "已保存,并附上你所回答的程序版本。", + "study.own_scale": "关于编辑器中的程序,从 1(完全不是)到 7(完全是):", + "study.which_probe": "哪个探测任务", + "study.done_before": "(之前做过)", + "study.answer_label": "你的回答(OpenQASM)", + "study.score": "评分", + "study.verified": "已验证。得分 {score}(参考答案得 1 分;代价 {cost},参考答案为 {reference})。", + "study.not_verified": "未通过验证:{failure}。得分 0。", + "study.probe_note": "不借助助手完成:在你关闭这里之前,助手处于暂停状态。由引擎为你的回答评分。", + "study.close": "关闭", + "study.of_edits": "{of} 次编辑中的 {n} 次", + "study.of_choices": "{n}/{of};随机水平 {chance}", + "study.predictions": "{n} 次预测", + "study.designs": "{n} 个设计", + "study.measures_note": "每一项都由引擎根据你做出的程序和你的操作重新计算;没有一项是模型的判断。", + "study.exported": "已保存 {where},并已用你的密钥封存。请把它交给研究者;没有发送任何内容。" +} diff --git a/wai/quantum-studio/locales/zh-CN/taste.json b/wai/quantum-studio/locales/zh-CN/taste.json new file mode 100644 index 000000000..6d0478b6e --- /dev/null +++ b/wai/quantum-studio/locales/zh-CN/taste.json @@ -0,0 +1,58 @@ +{ + "_meta": { + "locale": "zh-CN", + "area": "taste", + "translation": "machine-assisted: to be reviewed by a fluent reader before it is advertised" + }, + "taste.part_economy_what": "它的花费:门、深度、T 门", + "taste.part_clarity_what": "它有多容易读懂", + "taste.part_insight_what": "它揭示了问题的什么", + "taste.part_precision_what": "它完成任务有多精确,在噪声下也是如此", + "taste.part_utility_what": "它有多容易在别处使用", + "taste.part_economy": "经济性", + "taste.part_clarity": "清晰性", + "taste.part_insight": "洞见", + "taste.part_precision": "精确性", + "taste.part_utility": "实用性", + "taste.no_choices": "还没有任何选择。比较两个保留的生成器,或者应用或拒绝一个与草稿是同一条线路的备选方案,你的品味就会从你的选择中学习得来。", + "taste.choices_one": "{n} 次选择", + "taste.choices_many": "{n} 次选择", + "taste.choices_parts": "({parts})", + "taste.predicts": "在你第一次之后所作的 {of} 次选择中,它都只根据此前的选择来预测,猜对了 {right} 次", + "taste.leans": "它偏向 {list}", + "taste.unmeasured": "其中 {n} 次的两者在引擎所度量的方面毫无差别", + "taste.summary": "{parts}。", + "taste.puts_first": "{part}把 {first} 排在首位", + "taste.the_cost": "代价", + "taste.disagreeing": "存在分歧:{list}。", + "taste.narrow": "前 {first} 个在构造方式上相距 {ranked};按 {by} 排序则为 {baseline}{narrows}。", + "taste.narrows": ":这一排序收窄了排在前面的范围", + "taste.profile_label": "品味档案的名称", + "taste.compare": "比较两个", + "taste.compare_title": "你的档案最拿不准的两个保留生成器:选出你更喜欢的那个", + "taste.rank": "按我的品味排序", + "taste.dial_label": "新颖度旋钮,从典型到新颖", + "taste.title": "你的品味", + "taste.profile": "档案", + "taste.typical": "典型", + "taste.novel": "新颖", + "taste.note": "你只在引擎证明作用相同的程序之间作选择,所以一次选择关乎品味,而无关正确性。档案归你所有,以其名称保存在这个浏览器中。排序会显示每个程序有多典型;档案从不学习这一点,只有旋钮能改变它。", + "taste.none": "先建立一个存档,并保留两个或更多生成器,才能比较和排序它们。", + "taste.finding_pair": "引擎正在寻找你的档案最拿不准的一对…", + "taste.nothing_to_compare": "没有可比较的:{why}。", + "taste.part_label": "这次选择关乎什么", + "taste.the_whole": "整体", + "taste.cost_suffix": " · 代价 {cost}", + "taste.this_one": "选这个", + "taste.which": "你更喜欢哪一个?引擎证明它们作用相同({how})。", + "taste.about": "关于", + "taste.skip": "跳过", + "taste.kept": "已记录:{chosen} 优于 {other}{part}。", + "taste.for_its": ",因为它的{part}", + "taste.another": "再来一对", + "taste.typicality_title": "典型度:它的构造与已知构造有多接近,从 1(构造与之相同)到 0(毫无共同之处)", + "taste.typicality_none": "典型度 —", + "taste.typicality": "典型度 {value}", + "taste.score": "得分 {score}", + "taste.no_choices_dial": "还没有任何选择:仅按旋钮排序。" +} diff --git a/wai/quantum-studio/locales/zh-CN/verdict_view.json b/wai/quantum-studio/locales/zh-CN/verdict_view.json new file mode 100644 index 000000000..1c8b6d220 --- /dev/null +++ b/wai/quantum-studio/locales/zh-CN/verdict_view.json @@ -0,0 +1,17 @@ +{ + "_meta": { + "locale": "zh-CN", + "area": "verdict_view", + "translation": "machine-assisted: to be reviewed by a fluent reader before it is advertised" + }, + "verdict_view.refused": "被拒绝", + "verdict_view.parsed": "已读取,未运行", + "verdict_view.executed": "已运行,未检查", + "verdict_view.evidence": "作为证据检查", + "verdict_view.proof": "作为证明检查", + "verdict_view.fails": ":不成立", + "verdict_view.no_verdict": "无判定", + "verdict_view.sealed": "由回执封存", + "verdict_view.not_sealed": "在此处得出,未由回执封存", + "verdict_view.why": " {statement}({where})" +} diff --git a/wai/quantum-studio/locales/zh-CN/versions.json b/wai/quantum-studio/locales/zh-CN/versions.json new file mode 100644 index 000000000..a167a384e --- /dev/null +++ b/wai/quantum-studio/locales/zh-CN/versions.json @@ -0,0 +1,69 @@ +{ + "_meta": { + "locale": "zh-CN", + "area": "versions", + "translation": "machine-assisted: to be reviewed by a fluent reader before it is advertised" + }, + "versions.hand_person": "你", + "versions.hand_assistant": "助手", + "versions.hand_agent": "一个智能体", + "versions.hand_engine": "引擎", + "versions.hand_origin": "它的来源", + "versions.hand_assistant_then_person": "助手,然后是你", + "versions.hand_agent_then_person": "一个智能体,然后是你", + "versions.how_start": "创建", + "versions.how_edit": "编辑", + "versions.how_proposal": "建议", + "versions.how_merge": "合并", + "versions.made_by": "由{who}{how}", + "versions.started_from": "从{who}开始", + "versions.opened_from": "从{who}打开", + "versions.change_one": "{n} 处改动", + "versions.changes": "{n} 处改动", + "versions.gate_one": "{n} 个门", + "versions.gates": "{n} 个门", + "versions.line_one": "{n} 行", + "versions.lines": "{n} 行", + "versions.hands": "谁写的", + "versions.hands_title": "为每一行和每个门标出它出自谁之手", + "versions.download": "下载", + "versions.download_title": "程序及其历史,格式与 wai-quantum history verify 读取的一致", + "versions.title": "版本", + "versions.intro": "每一处改动:由谁做出、如何做出。你输入的内容归你;你应用的建议归其提出者,并且只有你采纳的改动才会进入程序。", + "versions.merge_nothing": "{id} 中没有程序里没有的内容。", + "versions.proposal_same": "这个建议与程序现状完全相同。", + "versions.take_all": "全部采纳({n} 处)", + "versions.take_some": "采纳 {n}/{of} 处", + "versions.bring_in": "并入 {id}", + "versions.proposal_from": "来自{who}的建议", + "versions.review_sum": " · {changes}{blocked}。采纳你想要的改动;在你作出选择之前,程序保持不变。", + "versions.blocked_by_your_rules": ",其中 {n} 处被你的规则阻止", + "versions.at_line": "第 {line} 行", + "versions.blocked_why": "已阻止:{why}。", + "versions.not_now": "暂不", + "versions.reject_all": "全部拒绝", + "versions.unread": "这里无法读取该程序,因此不统计它的门;它的各行已标出。", + "versions.bar_part": "{who}写了 {of} 个门中的 {n} 个", + "versions.hand_gates": "{who}:{gates}", + "versions.sum": "{version}:{gates},{lines}", + "versions.hand_share": "{who}:{gates},{lines}", + "versions.open": "打开 {id}", + "versions.open_title": "把这个版本放进编辑器;你在它之上做的改动会成为一条独立的工作线", + "versions.bring": "并入 {id}", + "versions.bring_title": "对照程序审阅它的改动,采纳你想要的;记录为一次合并", + "versions.what": " {made}{from},{when}", + "versions.from": ",基于 {parents}", + "versions.and": " 和 ", + "versions.review_taken": "采纳了 {changes}中的 {n} 处{blocked}{rest}。", + "versions.blocked_by_the_rules": ",{n} 处被规则阻止", + "versions.rest_kept": ";其余未被采纳,完整的建议保留在下方", + "versions.whole_proposal": "完整的建议", + "versions.against": "相对 {parent}:+{added} −{removed}{sealed}", + "versions.sealed": " · 已封存", + "versions.opened": "已打开 {id}。你从这里做的改动会成为一条独立的工作线。", + "versions.in_editor": "在编辑器中", + "versions.a_head": "末端版本", + "versions.item_review": "采纳 {n}/{of}{blocked} · ", + "versions.item_blocked": ",{n} 处被阻止", + "versions.saved": "已保存 program.qasm 和 program.history.json;运行 wai-quantum history verify program.qasm 可检查每一个版本。" +} diff --git a/wai/quantum-studio/locales/zh-CN/visual_edit.json b/wai/quantum-studio/locales/zh-CN/visual_edit.json new file mode 100644 index 000000000..198a27c00 --- /dev/null +++ b/wai/quantum-studio/locales/zh-CN/visual_edit.json @@ -0,0 +1,18 @@ +{ + "_meta": { + "locale": "zh-CN", + "area": "visual_edit", + "translation": "machine-assisted: to be reviewed by a fluent reader before it is advertised" + }, + "visual_edit.chip_title": "把 {gate} 拖到一条线上,或先选中它,再在线路图上选一个位置", + "visual_edit.armed": "现在为 {gate} 在线上选一个位置:点击线路图。", + "visual_edit.delete": "删除门", + "visual_edit.delete_title": "删除所选的门(Delete 键)", + "visual_edit.palette_label": "可放到线路上的门", + "visual_edit.edit_by_drawing": "在图上编辑", + "visual_edit.edited": "{summary}:这是对程序文本的一次修改,记为你的修改。", + "visual_edit.too_few_wires": "{gate} 需要 {arity} 条线;程序只有 {wires} 条。", + "visual_edit.no_wire": "那里没有线。", + "visual_edit.gate_label": "{gate}:方向键移动它,Delete 键删除它", + "visual_edit.a_gate": "一个门" +} diff --git a/wai/quantum-studio/locales/zh-CN/workbench.json b/wai/quantum-studio/locales/zh-CN/workbench.json new file mode 100644 index 000000000..0eaa9dda9 --- /dev/null +++ b/wai/quantum-studio/locales/zh-CN/workbench.json @@ -0,0 +1,101 @@ +{ + "_meta": { + "locale": "zh-CN", + "area": "workbench", + "translation": "machine-assisted: to be reviewed by a fluent reader before it is advertised" + }, + "workbench.measure_depth": "深度", + "workbench.measure_two_qubit": "双量子比特门", + "workbench.measure_t_count": "T 门", + "workbench.measure_gates": "门", + "workbench.measure_two_qubit_depth": "双量子比特门深度", + "workbench.measure_one_qubit": "单量子比特门", + "workbench.measure_three_qubit": "三量子比特门", + "workbench.measure_non_clifford": "非 Clifford 门", + "workbench.measure_qubits": "量子比特", + "workbench.sizes_name_each": "请为每个输入写出名称:{list}", + "workbench.sizes_no_input": "程序没有输入 {name};它的输入是 {names}", + "workbench.sizes_whole": "{name}:须为 0 或更大的整数,例如 {name} = 3, 5, 8", + "workbench.sizes_missing": "请给出 {names} 的取值", + "workbench.cost_needs_weight": "请至少为代价中的一项度量设置大于 0 的权重", + "workbench.map_label": "存档:按{x}和{y}划分的 {n} 个单元", + "workbench.point_label": "{id},由 {proposer} 提出:代价 {cost},{x} 和 {y}", + "workbench.point_title": "{id}({proposer}):代价 {cost}", + "workbench.outcome_duplicate": "{who}:之前已见过,不再重复评估", + "workbench.outcome_not_verified": "{who}:未通过验证:{failure}", + "workbench.outcome_did_not_read": "无法读取", + "workbench.outcome_improvement": "{who}:已保留为 {id},比此前的每个生成器都更便宜(代价 {cost})", + "workbench.outcome_kept": "{who}:已保留为 {id},是其单元中最好的(代价 {cost})", + "workbench.outcome_verified": "{who}:已验证(代价 {cost}),但它的单元中已有一个同样便宜的", + "workbench.start": "开始", + "workbench.start_title": "新建一个存档,以编辑器中的程序作为参照", + "workbench.task_label": "每个成员必须做到什么", + "workbench.task_prepares": "从 |0…0⟩ 制备出相同的态", + "workbench.task_equivalent": "是同一条线路(酉矩阵相同)", + "workbench.sizes_label": "要检查的规模", + "workbench.gates_any": "任意", + "workbench.gates_label": "建议可以使用的门", + "workbench.conn_label": "门可以连接哪些量子比特", + "workbench.conn_all": "任意一对", + "workbench.conn_line": "一条直线上的相邻量子比特", + "workbench.weight_label": "代价中{measure}的权重", + "workbench.axis_first": "图的第一个轴", + "workbench.axis_second": "图的第二个轴", + "workbench.spec": "规格", + "workbench.each_member_must": "每个成员必须", + "workbench.checked_at": "检查的规模", + "workbench.using": "使用的门", + "workbench.on": "连接方式", + "workbench.cost_summed": "代价(对各规模求和):", + "workbench.map": "图", + "workbench.against": "对", + "workbench.search": "搜索 ×100", + "workbench.search_title": "内置的盲搜索:对存档中最好的生成器做一百次带种子的编辑,每次都由引擎检查", + "workbench.ask": "询问模型 ×3", + "workbench.ask_title": "以存档中最好的生成器为起点,由模型给出三个生成器;引擎逐一判定", + "workbench.submit": "提交编辑器中的程序", + "workbench.submit_title": "判定编辑器中的程序,若它是其单元中最好的则保留", + "workbench.download": "下载", + "workbench.download_title": "规格及其存档,每个保留的生成器都附有带封签的判定,供 wai-quantum discover verify 使用", + "workbench.intro": "在编辑器中写一个生成器(一个带有 input uint n; 的程序),说明其族中每个成员必须做到什么、代价如何,然后开始。没有输入的程序也可以:一条线路,一个规模。", + "workbench.title": "工作台", + "workbench.stats_energy": ",{joules} J", + "workbench.improvement_one": "{n} 次改进", + "workbench.improvement_many": "{n} 次改进", + "workbench.stats": "已评估 {evaluations} 个,已验证 {verified} 个,保留 {kept} 个,{improvements}{energy}。", + "workbench.stats_best": "最好的是 {id}:代价 {cost},参照的代价为 {reference};由 {proposer} 提出,以 {strength} 检查。", + "workbench.open": "在编辑器中打开", + "workbench.entry_title": "{id} · 代价 {cost} · {x_measure} {x_value},{y_measure} {y_value}", + "workbench.checked_by": "以 {strength} 检查{sealed}", + "workbench.sealed": ",已封签", + "workbench.lineage": "它的来历", + "workbench.its_generator": "它的生成器", + "workbench.checking_reference": "引擎正在每个规模上检查参照…", + "workbench.started": "已开始:参照是 {id},代价 {cost}。", + "workbench.loosened": "规格现在{change}。这个存档是在旧规则下建立的;要按新规则搜索,请新建一个存档。", + "workbench.loosened_allows": "允许使用 {gate}", + "workbench.loosened_any_pair": "允许门连接任意一对量子比特", + "workbench.novelty_said": "{id}:{text}。", + "workbench.aberration_uses": "它使用了 {gate}", + "workbench.aberration_joins": "它连接了不相邻的量子比特", + "workbench.aberration_breaks": "它违反了一条规则", + "workbench.aberration_why": "它只违反了一条规则({rule});若没有这条规则,它会以代价 {cost} 通过验证({strength}),{x_measure} {x_value},{y_measure} {y_value}。", + "workbench.try_without": "去掉这条规则再试", + "workbench.aberration_said": "{who}:一个异常。{why}这条规则也许值得修改。", + "workbench.search_progress": "盲搜索:{total} 次编辑中已判定 {done} 次…", + "workbench.search_done": "盲搜索:判定了 {total} 次编辑,保留 {kept} 个。", + "workbench.search_find_said": "{id}(盲搜索):{text}。", + "workbench.search_find_title": "{id}(盲搜索):{text}", + "workbench.search_stopped": "搜索已停止:{error}", + "workbench.paused": "你正在独自探索:模型会等到你在“版本”下结束独自探索。", + "workbench.needs_model": "“询问模型”需要一个模型:请在“助手”的“模型…”下连接一个。", + "workbench.model_proposing": "模型正在提出建议;引擎将逐一判定…", + "workbench.model_wrote_none": "模型没有在 ```qasm 代码块中写出任何生成器。", + "workbench.an_idea": "一个想法", + "workbench.model_failed": "模型没有回答:{error}", + "workbench.your_program": "你的程序", + "workbench.not_judged": "无法判定:{error}", + "workbench.saved": "已保存 {path} 及其存档;wai-quantum discover verify {stem}.json 会检查每个封签。", + "workbench.not_saved": "无法保存:{error}", + "workbench.resumed": "已接续:迄今已评估 {n} 个。" +} diff --git a/wai/quantum-studio/package-lock.json b/wai/quantum-studio/package-lock.json index 0b453354b..0181bcb06 100644 --- a/wai/quantum-studio/package-lock.json +++ b/wai/quantum-studio/package-lock.json @@ -19,6 +19,7 @@ "@lezer/highlight": "1.2.5" }, "devDependencies": { + "binaryen": "132.0.0", "esbuild": "0.27.4", "playwright": "1.60.0" } @@ -583,6 +584,24 @@ "integrity": "sha512-Wy0V7+SGUjnF9/TkiM1hKVDPj7jKXduPNboMVtHTA8dySMURWqfg/JZ9E2Sq8JgSJmkl7k7Qe9FLeMSrSraWmQ==", "license": "MIT" }, + "node_modules/binaryen": { + "version": "132.0.0", + "resolved": "https://registry.npmjs.org/binaryen/-/binaryen-132.0.0.tgz", + "integrity": "sha512-4Usg7yEvwO3FMso3Oov6qkOOHSod1IEv+boKkh77kTljM+eNBouqCTbqWswGzbG+vDNO/b/v2C4gfEh1WgB9Lg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "wasm-as": "bin/wasm-as", + "wasm-ctor-eval": "bin/wasm-ctor-eval", + "wasm-dis": "bin/wasm-dis", + "wasm-merge": "bin/wasm-merge", + "wasm-metadce": "bin/wasm-metadce", + "wasm-opt": "bin/wasm-opt", + "wasm-reduce": "bin/wasm-reduce", + "wasm-shell": "bin/wasm-shell", + "wasm2js": "bin/wasm2js" + } + }, "node_modules/crelt": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz", diff --git a/wai/quantum-studio/package.json b/wai/quantum-studio/package.json index 8cc6ffbe2..089977637 100644 --- a/wai/quantum-studio/package.json +++ b/wai/quantum-studio/package.json @@ -7,7 +7,9 @@ "type": "module", "scripts": { "build": "node build.mjs", - "test": "node test/studio.test.mjs" + "test": "node test/studio.test.mjs", + "build:views": "bash ../wai-quantum-views/build.sh", + "test:views": "node test/views.test.mjs" }, "dependencies": { "@codemirror/autocomplete": "6.20.4", @@ -20,6 +22,7 @@ "@lezer/highlight": "1.2.5" }, "devDependencies": { + "binaryen": "132.0.0", "esbuild": "0.27.4", "playwright": "1.60.0" } diff --git a/wai/quantum-studio/src/answer-view.js b/wai/quantum-studio/src/answer-view.js new file mode 100644 index 000000000..adc6c3e7c --- /dev/null +++ b/wai/quantum-studio/src/answer-view.js @@ -0,0 +1,216 @@ +// An answer in the answer view layer (wai/extensions/quantum-answers.md), drawn as +// it arrives: the root's layout as soon as its line does, each view once its +// statement is ready, its numbers from its engine call carried out here, a program's +// circuit gate by gate while its lines stream in, and every problem with its code. +// +// The text goes through the engine's own reader (engine.answer('snapshot'), then +// 'parse' once it has ended), so what is drawn is what the layer says; a view +// whose call the engine refuses shows the refusal, for that view alone. A seal +// is checked here, by the engine, and the answer can be kept with its receipt. +import { el, sameness } from './dom.js'; +import { circuitSvg } from './circuit-svg.js'; +import { verdictLine } from './verdict-view.js'; +import { chips, fidelityChip } from './critique-view.js'; +import { frontierSvg, compact as count, duration } from './estimate-view.js'; +import { blochSphere } from './state-views.js'; +import { METHOD_NAMES, countsBlock, expectationsBlock, amplitudesBlock } from './result-parts.js'; + +const note = (text, attrs = {}) => el('p', { class: 'qs-note', ...attrs, text }); + +// A view's card: its name and view, then what it shows. +function card(name, what) { + const body = el('div', { class: 'qs-av-body' }, note('waiting for its statement…')); + const node = el('div', { class: 'qs-av-card', 'data-name': name, 'data-state': 'waiting' }, + el('div', { class: 'qs-k' }, el('span', { text: name }), el('span', { class: 'qs-av-what', text: what ? ` · ${what}` : '' })), body); + return { node, body }; +} + +// The program a fence has opened and not yet closed, as far as it has come. +function openProgram(text) { + const lines = text.split('\n'); + let open = null; + for (let i = 0; i < lines.length - 1; i++) { + const t = lines[i].trim(); + if (open && t === '```') open = null; + else if (open) open.lines.push(lines[i]); + else { + const m = /^```(?:qasm|openqasm)\s+([A-Za-z_][A-Za-z0-9_]*)\s*$/.exec(t); + if (m) open = { name: m[1], lines: [] }; + } + } + return open && open.lines.length ? { name: open.name, qasm: open.lines.join('\n') + '\n' } : null; +} + +// What a statement's value shows, by its view. +function draw(view, v, engine) { + if (!v || v.ok === false) { + return [el('p', { class: 'qs-warn', text: v?.error || 'the engine gave no value' }), v?.code ? note(`${v.code} · ${v.outcome}: ${v.meaning || ''}`, { class: 'qs-note qs-av-code' }) : null]; + } + const x = v.value; + switch (view) { + case 'Text': return [el('p', { class: 'qs-why', text: x.text ?? '' })]; + case 'Circuit': + case 'program': { + if (!x.layout) return [note(x.why || 'not drawn')]; + const { svg } = circuitSvg(x.layout); + return [el('div', { class: 'qs-av-circuit' }, svg), x.description ? el('details', {}, el('summary', { text: 'The circuit in words' }), el('p', { class: 'qs-why', text: x.description })) : null]; + } + case 'Histogram': return [note(`${METHOD_NAMES[x.method] || x.method}: ${x.why || ''}`), countsBlock(x.result)]; + case 'Expectation': return [expectationsBlock(x.result)]; + case 'Amplitudes': return [amplitudesBlock(x.result)]; + case 'Bloch': { + const b = x.bloch || {}; + return [el('div', { class: 'qs-av-bloch' }, blochSphere(b, 120), el('p', { class: 'qs-why', text: `qubit ${b.qubit}: (${Number(b.x).toFixed(4)}, ${Number(b.y).toFixed(4)}, ${Number(b.z).toFixed(4)}), length ${Number(b.length).toFixed(4)}, purity ${Number(b.purity).toFixed(4)}` }))]; + } + case 'Verdict': return [verdictLine(x, { sealed: false })]; + case 'Equivalence': { + const s = sameness(x); + return [el('span', { class: `qs-badge ${s.cls}`, text: s.text }), s.why ? el('span', { class: 'qs-why', text: ` ${s.why}` }) : null]; + } + case 'Critique': return [el('div', { class: 'qs-chips' }, ...chips(x), fidelityChip(x)), el('ul', { class: 'qs-smells' }, ...(x.smells || []).map((s) => el('li', { class: 'qs-smell' }, el('span', { class: 'qs-smell-text', text: s.message }))))]; + case 'Estimate': { + const e = x.estimate || {}; + const pts = x.frontier || []; + return [el('p', { class: 'qs-why', text: `${count(e.physical_qubits)} physical qubits, ${e.factories} factories, ${duration(e.runtime_s)} at code distance ${e.distance}` }), pts.length ? el('div', { class: 'qs-frontier-figure' }, frontierSvg(pts, Math.max(0, pts.findIndex((p) => p.headline)), () => {})) : null]; + } + case 'Compile': { + const c = x.certificate || {}; + return [el('p', { class: 'qs-why', text: `${c.verdict}: ${c.output?.counts?.gates} gates, ${c.output?.counts?.t_count} T, within ${c.epsilon?.bound} of the program (budget ${c.epsilon?.budget})` })]; + } + case 'Plan': return [el('p', {}, el('b', { text: METHOD_NAMES[x.method] || x.method || '' }), el('span', { class: 'qs-why', text: ` ${x.why || ''}` }))]; + case 'Receipt': { + const check = el('span', { class: 'qs-badge', 'data-receipt': 'checking', text: 'checking here…' }); + engine.verify(x).then((r) => { + check.dataset.receipt = r.valid ? 'valid' : 'invalid'; + check.className = `qs-badge ${r.valid ? 'ok' : 'bad'}`; + check.textContent = r.valid ? `✓ seals ${(x.request?.sealed || []).join(', ')}, checked here` : '✗ does not verify'; + }).catch((e) => { check.dataset.receipt = 'unchecked'; check.textContent = `not checked: ${e.message}`; }); + return [check, note(`signed by ${(x.result?.receipt?.key || '').slice(0, 16)}…; the receipt shows that this key sealed these values for this answer, not who holds the key.`)]; + } + default: return [note(JSON.stringify(x).slice(0, 400))]; + } +} + +// A renderer: `feed(text)` as the answer arrives, `end()` once it has ended. `engine` +// answers answer(...), layout(...) and verify(...); `onEnded(summary)` hears the +// finished answer: its parse, each view's value, and the seal's. +export function answerRenderer({ engine, onEnded = () => {} }) { + const shell = el('div', { class: 'qs-av-shell', id: 'qs-av-shell' }); + const live = el('div', { class: 'qs-av-live', id: 'qs-av-live' }); + const diags = el('ul', { class: 'qs-av-diags', id: 'qs-av-diags' }); + const node = el('div', { class: 'qs-av', id: 'qs-av' }, shell, live, diags); + let text = ''; + let cards = new Map(); + let drawn = new Set(); + let values = new Map(); + let queue = Promise.resolve(); + let lines = 0; + + const cardFor = (name, what) => { + if (!cards.has(name)) { + const c = card(name, what); + cards.set(name, c); + shell.append(c.node); + } + return cards.get(name); + }; + + async function refresh(complete) { + const a = await engine.answer(complete ? 'parse' : 'snapshot', { text }); + if (a.error) { diags.replaceChildren(el('li', { class: 'qs-warn', text: a.error })); return a; } + node.dataset.root = a.root ?? ''; + const by = new Map(a.statements.map((s) => [s.name, s])); + // The root's layout first: a card for each view it lists, waiting until it is ready. + for (const name of a.order) { + const s = by.get(name); + const c = cardFor(name, s?.view ?? (s?.kind === 'program' ? 'program' : null)); + if (s?.view) c.node.querySelector('.qs-av-what').textContent = ` · ${s.view}`; + } + // Each view and program that is ready, drawn once, its call carried out here. + for (const name of a.order) { + const s = by.get(name); + if (!s || drawn.has(name)) continue; + const c = cardFor(name); + if (s.status === 'dropped') { c.node.dataset.state = 'dropped'; c.body.replaceChildren(note('left out: see the problems below')); drawn.add(name); continue; } + if (s.status !== 'ready') { c.node.dataset.state = 'pending'; c.body.replaceChildren(note(`waiting for ${s.deps.filter((d) => by.get(d)?.status !== 'ready').join(', ') || 'what it names'}…`)); continue; } + drawn.add(name); + c.node.dataset.state = 'running'; + c.body.replaceChildren(note('running on the engine here…')); + const view = s.kind === 'program' ? 'program' : s.view; + const v = s.kind === 'program' + ? await engine.layout(s.qasm).then((l) => (l.error ? { ok: false, error: l.error } : { ok: true, value: { layout: l } }), (e) => ({ ok: false, error: e.message })) + : await engine.answer('value', { text, name, complete }); + values.set(name, v); + c.body.replaceChildren(...draw(view, v, engine).filter(Boolean)); + c.node.dataset.state = v.ok === false ? 'refused' : 'drawn'; + c.node.dataset.view = view; + } + // A program still arriving: its circuit, gate by gate. + const open = complete ? null : openProgram(text); + if (open) { + const l = await engine.layout(open.qasm).catch(() => null); + if (l && !l.error) live.replaceChildren(el('div', { class: 'qs-k', text: `${open.name}, arriving` }), circuitSvg(l).svg); + } else live.replaceChildren(); + // The problems: each with its line and code; the errors are what a model is sent back. + diags.replaceChildren(...a.diagnostics.map((d) => el('li', { class: `qs-av-diag ${d.severity}`, 'data-code': d.code, 'data-severity': d.severity }, + el('span', { class: 'qs-badge' + (d.severity === 'error' ? ' bad' : ''), text: d.code }), ` line ${d.line}: ${d.message}`))); + return a; + } + + return { + node, + // The next piece of the answer; the view redraws as each line completes. + feed(piece) { + text += piece; + const now = text.split('\n').length - 1; + if (now === lines) return queue; + lines = now; + queue = queue.then(() => refresh(false)).catch(() => {}); + return queue; + }, + // The answer has ended: read whole, everything left drawn or left out. + end() { + queue = queue.then(async () => { + const a = await refresh(true); + for (const [name, c] of cards) if (!drawn.has(name)) { c.node.dataset.state = 'dropped'; c.body.replaceChildren(note('never arrived')); } + node.dataset.done = 'true'; + onEnded({ answer: a, text, values }); + return a; + }); + return queue; + }, + reset() { + text = ''; lines = 0; cards = new Map(); drawn = new Set(); values = new Map(); + shell.replaceChildren(); live.replaceChildren(); diags.replaceChildren(); + delete node.dataset.done; + }, + text: () => text, + }; +} + +// What a finished answer gives a correctness record: its codes (every +// diagnostic's, and each refused view's), the first verdict it shows, its first +// program, the intent of its first check, and its seal's digest. +export function answerRecord({ answer, values }) { + const codes = [...answer.diagnostics.map((d) => d.code)]; + for (const v of values.values()) for (const c of v?.codes || []) if (!codes.includes(c)) codes.push(c); + const stmts = answer.statements || []; + const verdictOf = stmts.find((s) => s.view === 'Verdict' && values.get(s.name)?.ok); + const program = stmts.find((s) => s.kind === 'program' && s.reads); + const seal = stmts.find((s) => s.view === 'Receipt' && values.get(s.name)?.ok); + const fields = { codes, program: program?.qasm, receipt: seal ? values.get(seal.name).value?.result?.receipt?.digest : undefined }; + if (verdictOf) { + const v = values.get(verdictOf.name).value; + fields.verdict = { level: v.level, holds: v.holds }; + // The intent its check held the program to, as a job's intent. + const call = verdictOf.expr?.args?.[0]; + const intent = call?.call === 'Check' ? call.args?.[1] : null; + if (intent?.object) { + const entries = intent.object; + const ref = (e) => stmts.find((s) => s.name === e?.ref)?.qasm; + if (entries.length === 1 && ['equivalent', 'prepares'].includes(entries[0][0]) && entries[0][1].ref) fields.intent = { [entries[0][0]]: ref(entries[0][1]) }; + else fields.intent = { distribution: Object.fromEntries(entries.map(([k, e]) => [k, e.value])) }; + } else if (intent?.ref) fields.intent = { equivalent: stmts.find((s) => s.name === intent.ref)?.qasm }; + } + return fields; +} diff --git a/wai/quantum-studio/src/assistant.js b/wai/quantum-studio/src/assistant.js index f8b1368b5..3579d0c49 100644 --- a/wai/quantum-studio/src/assistant.js +++ b/wai/quantum-studio/src/assistant.js @@ -13,6 +13,10 @@ // it asks a question before it proposes (the stance, which the reader sets). For // Alternatives it is a source of candidates, never the judge: each one it writes // is read, measured and compared by the engine before the Studio shows it. +// +// Only the words the reader sees (the stances' labels, the errors) follow the +// reader's language; every word the model reads stays in English. +import { t as tr } from './i18n.js'; const SYSTEM = `You are the assistant in Quantum Studio, an editor for OpenQASM programs. Tools run the Studio's own engine: @@ -37,9 +41,9 @@ Answer briefly.`; // How the assistant helps, which the reader chooses. Asking first is the default: // a question the reader answers teaches more than an answer they read. export const STANCES = { - questions: { label: 'Ask me questions first', text: 'The user is the author and is learning. Before you propose a program or a change, ask one short question that helps them see it themselves: what they expect a line to do, or what a result shows. Answer a direct question from the tools, then ask one question back that moves them forward. Write a whole program only when they ask you to.' }, - suggest: { label: 'Suggest changes', text: 'The user is the author. Suggest changes as small edits to their program, each naming its line and the reason the tools gave; write a whole program only when they ask for one.' }, - write: { label: 'Write programs', text: 'When a change is wanted, write the whole program as described above.' }, + questions: { label: tr('assistant.stance_questions'), text: 'The user is the author and is learning. Before you propose a program or a change, ask one short question that helps them see it themselves: what they expect a line to do, or what a result shows. Answer a direct question from the tools, then ask one question back that moves them forward. Write a whole program only when they ask you to.' }, + suggest: { label: tr('assistant.stance_suggest'), text: 'The user is the author. Suggest changes as small edits to their program, each naming its line and the reason the tools gave; write a whole program only when they ask for one.' }, + write: { label: tr('assistant.stance_write'), text: 'When a change is wanted, write the whole program as described above.' }, }; export const TOOLS = [ @@ -100,14 +104,43 @@ export class HttpModel { async chat(body) { const res = await fetch(`${this.endpoint}/chat/completions`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }); const v = await res.json().catch(() => ({})); - if (!res.ok) throw new Error(v.error?.message || v.error || `the model answered ${res.status}`); + if (!res.ok) throw new Error(v.error?.message || v.error || tr('assistant.model_answered', { status: res.status })); return v; } async models() { const res = await fetch(`${this.endpoint}/models`); - if (!res.ok) throw new Error(`the model server answered ${res.status}`); + if (!res.ok) throw new Error(tr('assistant.server_answered', { status: res.status })); return (await res.json()).data?.map((m) => m.id) ?? []; } + // The same call, streamed: `onText(piece)` hears the answer's text as it + // arrives (server-sent events); the whole text is returned at the end. + async stream(body, onText) { + const res = await fetch(`${this.endpoint}/chat/completions`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ ...body, stream: true }) }); + if (!res.ok) { + const v = await res.json().catch(() => ({})); + throw new Error(v.error?.message || v.error || `the model answered ${res.status}`); + } + const reader = res.body.getReader(); + const dec = new TextDecoder(); + let buf = '', all = ''; + for (;;) { + const { value, done } = await reader.read(); + if (done) break; + buf += dec.decode(value, { stream: true }); + for (let i = buf.indexOf('\n'); i >= 0; i = buf.indexOf('\n')) { + const line = buf.slice(0, i).trim(); + buf = buf.slice(i + 1); + if (!line.startsWith('data:')) continue; + const data = line.slice(5).trim(); + if (data === '[DONE]') return all; + let d; + try { d = JSON.parse(data); } catch (e) { continue; } + const t = d.choices?.[0]?.delta?.content; + if (t) { all += t; await onText(t); } + } + } + return all; + } } // The same, called by the desktop app (no browser origin rules in the way). @@ -123,6 +156,13 @@ export class DesktopModel { if (v.error && !v.data) throw new Error(v.error.message || v.error); return v.data?.map((m) => m.id) ?? []; } + // The app calls the model whole; the answer is heard at once. + async stream(body, onText) { + const v = await this.chat(body); + const t = v.choices?.[0]?.message?.content || ''; + await onText(t); + return t; + } } export class Assistant { @@ -206,7 +246,7 @@ export class Assistant { // The first turn must ask the engine something, so every answer starts from it. const res = await this.model.chat({ model: this.model.name || undefined, messages, tools: TOOLS, tool_choice: round === 0 ? 'required' : 'auto', temperature: 0.2 }); const msg = res.choices?.[0]?.message; - if (!msg) throw new Error('the model gave no answer'); + if (!msg) throw new Error(tr('assistant.no_answer')); messages.push({ role: 'assistant', content: msg.content ?? '', ...(msg.tool_calls?.length ? { tool_calls: msg.tool_calls } : {}) }); if (msg.tool_calls?.length) { for (const call of msg.tool_calls) { @@ -262,7 +302,7 @@ export class Assistant { on({ type: 'answer', text: answer, proposals: checked, usage: res.usage, unsourced: unsourced(answer, heard.join(' ')) }); return; } - throw new Error('the model kept calling tools without answering'); + throw new Error(tr('assistant.kept_calling')); } } diff --git a/wai/quantum-studio/src/circuit-svg.js b/wai/quantum-studio/src/circuit-svg.js index 4ddf5a3e3..77677d0d7 100644 --- a/wai/quantum-studio/src/circuit-svg.js +++ b/wai/quantum-studio/src/circuit-svg.js @@ -5,6 +5,8 @@ // tokens, through classes in studio.css. Each gate carries its operation's index // in the program (data-op) and each wire its qubit (data-q), which the critique // marks: see mark(). +import { t as tr } from './i18n.js'; + const NS = 'http://www.w3.org/2000/svg'; const ROW = 40; // between wires const LEFT = 40; // the qubits' names @@ -66,7 +68,7 @@ export function circuitSvg(layout) { const H = n * ROW + 10; const y = (q) => 25 + q * ROW; const gates = layout.columns.reduce((s, c) => s + c.length, 0); - const svg = node('svg', { xmlns: NS, width: W, height: H, viewBox: `0 0 ${W} ${H}`, class: 'qc', role: 'img', 'aria-label': `The circuit: ${n} qubits, ${gates} operations in ${layout.columns.length} columns` }); + const svg = node('svg', { xmlns: NS, width: W, height: H, viewBox: `0 0 ${W} ${H}`, class: 'qc', role: 'img', 'aria-label': tr('circuit_svg.label', { qubits: n, gates, columns: layout.columns.length }) }); for (let q = 0; q < n; q++) { const name = text(LEFT - 14, y(q), `q${q}`, 'qc-name'); name.setAttribute('data-q', q); @@ -80,7 +82,7 @@ export function circuitSvg(layout) { for (const it of col) { const qs = it.cells.map(([q]) => q); const g = node('g', { class: 'qc-item', 'data-op': it.op, 'data-col': ci, 'data-qubits': qs.join(' ') }); - g.append(node('title', {}, it.cells.map(([q, l]) => `${l} on q${q}`).join(', '))); + g.append(node('title', {}, it.cells.map(([q, l]) => tr('circuit_svg.cell', { gate: l, q })).join(tr('circuit_svg.cell_sep')))); if (it.joined && qs.length > 1) g.append(node('line', { x1: cx, y1: y(Math.min(...qs)), x2: cx, y2: y(Math.max(...qs)), class: 'qc-join' })); for (const [q, l] of it.cells) g.append(cell(cx, y(q), parse(l))); svg.append(g); diff --git a/wai/quantum-studio/src/compile-view.js b/wai/quantum-studio/src/compile-view.js index 1f2ba404e..7733a7a54 100644 --- a/wai/quantum-studio/src/compile-view.js +++ b/wai/quantum-studio/src/compile-view.js @@ -7,22 +7,23 @@ // proposal. import { el } from './dom.js'; import { circuitSvg } from './circuit-svg.js'; +import { t } from './i18n.js'; -const COUNTS = [['gates', 'gates'], ['cnots', 'CNOTs'], ['t_count', 'T'], ['rotations', 'rotations']]; +const COUNTS = [['gates', t('compile_view.col_gates')], ['cnots', t('compile_view.col_cnots')], ['t_count', 'T'], ['rotations', t('compile_view.col_rotations')]]; const exp = (x) => (x == null ? '' : x === 0 ? '0' : Number(x).toExponential(2).replace('e-', '·10⁻').replace(/⁻(\d+)/, (_, d) => '⁻' + [...d].map((c) => '⁰¹²³⁴⁵⁶⁷⁸⁹'[c]).join(''))); // `engine` answers compile(request), verify(job) and layout(qasm); `program()` is // the editor's text; `offer(qasm, note)` proposes a program. export function compilePanel({ engine, program, offer }) { - const basis = el('select', { class: 'qs-input qs-small-input', id: 'qs-compile-basis', 'aria-label': 'Basis', title: 'The gate set to compile to. Clifford+T: every rotation written as a Clifford+T word within ε. Standard: the standard gates at any angle, nothing approximated' }, - el('option', { value: 'clifford_t', text: 'Clifford+T' }), el('option', { value: 'standard', text: 'standard' })); - const level = el('select', { class: 'qs-input qs-small-input', id: 'qs-compile-level', 'aria-label': 'Level', title: '0: lower and approximate; 1: also checked rewrites and phase folding; 2: also one- and two-qubit resynthesis' }, - ...[0, 1, 2].map((l) => el('option', { value: String(l), text: `level ${l}`, selected: l === 1 }))); - const eps = el('input', { class: 'qs-input qs-small-input', id: 'qs-compile-eps', type: 'text', value: '1e-6', inputmode: 'decimal', spellcheck: 'false', 'aria-label': 'ε', title: 'The most the output may differ from the program: the operator norm, up to a global phase, shared by its rotations' }); - const go = el('button', { class: 'qs-btn qs-small', id: 'qs-compile', type: 'button', text: 'Compile', title: 'The program through the engine’s passes, each checked, with a signed certificate' }); + const basis = el('select', { class: 'qs-input qs-small-input', id: 'qs-compile-basis', 'aria-label': t('compile_view.basis_label'), title: t('compile_view.basis_title') }, + el('option', { value: 'clifford_t', text: 'Clifford+T' }), el('option', { value: 'standard', text: t('compile_view.standard') })); + const level = el('select', { class: 'qs-input qs-small-input', id: 'qs-compile-level', 'aria-label': t('compile_view.level_label'), title: t('compile_view.level_title') }, + ...[0, 1, 2].map((l) => el('option', { value: String(l), text: t('compile_view.level_option', { level: l }), selected: l === 1 }))); + const eps = el('input', { class: 'qs-input qs-small-input', id: 'qs-compile-eps', type: 'text', value: '1e-6', inputmode: 'decimal', spellcheck: 'false', 'aria-label': 'ε', title: t('compile_view.eps_title') }); + const go = el('button', { class: 'qs-btn qs-small', id: 'qs-compile', type: 'button', text: t('compile_view.compile'), title: t('compile_view.compile_title') }); const body = el('div', { class: 'qs-shortcuts-body', id: 'qs-compile-body' }); const node = el('div', { class: 'qs-shortcuts qs-compile', id: 'qs-compile-panel' }, - el('div', { class: 'qs-shortcuts-head' }, el('span', { class: 'qs-k', text: 'Compile' }), el('span', { class: 'qs-grow' }), basis, level, el('label', { class: 'qs-inline', for: 'qs-compile-eps' }, el('span', { text: 'ε' }), eps), go), + el('div', { class: 'qs-shortcuts-head' }, el('span', { class: 'qs-k', text: t('compile_view.heading') }), el('span', { class: 'qs-grow' }), basis, level, el('label', { class: 'qs-inline', for: 'qs-compile-eps' }, el('span', { text: 'ε' }), eps), go), body); let compiledFor = null; let busy = false; @@ -33,7 +34,7 @@ export function compilePanel({ engine, program, offer }) { const { svg, drawn, total } = circuitSvg(l); const after = cert.passes[i].after; figure.replaceChildren(svg); - caption.textContent = `After ${stage.pass} (pass ${i + 1} of ${cert.passes.length}): ${after.gates} gates, ${after.cnots} CNOTs, ${after.t_count} T, ${after.rotations} rotations${drawn < total ? `; the first ${drawn} of ${total} columns drawn` : ''}.`; + caption.textContent = t('compile_view.caption', { pass: stage.pass, number: i + 1, of: cert.passes.length, gates: after.gates, cnots: after.cnots, t: after.t_count, rotations: after.rotations, partial: drawn < total ? t('compile_view.caption_partial', { drawn, total }) : '' }); } catch (e) { figure.replaceChildren(el('p', { class: 'qs-note', text: e.message })); } } @@ -43,7 +44,7 @@ export function compilePanel({ engine, program, offer }) { const figure = el('div', { class: 'qs-diagram qs-compile-diagram', id: 'qs-compile-diagram', tabindex: '0' }); const caption = el('p', { class: 'qs-note', id: 'qs-compile-caption' }); const rows = cert.passes.map((p, i) => { - const show = el('button', { class: 'qs-btn qs-small', type: 'button', text: p.pass, title: 'Draw the circuit after this pass' }); + const show = el('button', { class: 'qs-btn qs-small', type: 'button', text: p.pass, title: t('compile_view.draw_title') }); const tr = el('tr', { 'data-pass': p.pass, class: p.check.verdict === 'rejected' || p.check.verdict === 'skipped' ? 'off' : '', title: p.check.how + (p.check.why ? `: ${p.check.why}` : '') }, el('th', { scope: 'row' }, show), ...COUNTS.map(([k]) => el('td', { class: 'num', text: p.before[k] === p.after[k] ? String(p.after[k]) : `${p.before[k]}→${p.after[k]}` })), @@ -56,28 +57,29 @@ export function compilePanel({ engine, program, offer }) { return tr; }); const e = cert.epsilon; - const spent = e.rotations ? `${e.rotations} rotation${e.rotations === 1 ? '' : 's'}, ${exp(e.each)} each` : 'nothing approximated'; - const whole = cert.whole.distance != null ? ` The whole output measured against the program: ${exp(cert.whole.distance)}.` : ''; - const use = el('button', { class: 'qs-btn qs-small', id: 'qs-compile-use', type: 'button', text: 'Use', title: 'Offer the compiled program for review' }); - use.addEventListener('click', () => offer(r.qasm, `Compiled to ${cert.basis === 'clifford_t' ? 'Clifford+T' : 'the standard gates'} at level ${cert.level}: within ${exp(e.bound)} of the program (budget ${exp(e.budget)}), ${cert.output.counts.t_count} T gates; every pass checked.`)); + const spent = e.rotations ? (e.rotations === 1 ? t('compile_view.spent_one', { n: e.rotations, each: exp(e.each) }) : t('compile_view.spent_other', { n: e.rotations, each: exp(e.each) })) : t('compile_view.nothing_approximated'); + const whole = cert.whole.distance != null ? t('compile_view.whole', { distance: exp(cert.whole.distance) }) : ''; + const use = el('button', { class: 'qs-btn qs-small', id: 'qs-compile-use', type: 'button', text: t('compile_view.use'), title: t('compile_view.use_title') }); + use.addEventListener('click', () => offer(r.qasm, t('compile_view.offer_note', { basis: cert.basis === 'clifford_t' ? 'Clifford+T' : t('compile_view.the_standard_gates'), level: cert.level, bound: exp(e.bound), budget: exp(e.budget), t: cert.output.counts.t_count }))); + const verdict = cert.verdict === 'approximate' ? t('compile_view.verdict_approximate') : cert.verdict === 'exact' ? t('compile_view.verdict_exact') : t('compile_view.verdict_proved'); body.replaceChildren( - el('p', { class: 'qs-why', id: 'qs-compile-summary', text: `${cert.verdict === 'approximate' ? 'Approximate' : cert.verdict === 'exact' ? 'Exact' : 'Proved'}: within ${exp(e.bound)} of the program, up to a global phase (budget ${exp(e.budget)}; ${spent}).${whole} The certificate ${valid ? 'verifies' : 'DOES NOT VERIFY'}.` }), + el('p', { class: 'qs-why', id: 'qs-compile-summary', text: t('compile_view.summary', { verdict, bound: exp(e.bound), budget: exp(e.budget), spent, whole, certificate: valid ? t('compile_view.cert_verifies') : t('compile_view.cert_fails') }) }), el('div', { class: 'qs-scroll' }, el('table', { class: 'qs-passes', id: 'qs-passes' }, - el('thead', {}, el('tr', {}, el('th', { text: 'pass' }), ...COUNTS.map(([, label]) => el('th', { class: 'num', text: label })), el('th', { text: 'check' }))), + el('thead', {}, el('tr', {}, el('th', { text: t('compile_view.col_pass') }), ...COUNTS.map(([, label]) => el('th', { class: 'num', text: label })), el('th', { text: t('compile_view.col_check') }))), el('tbody', {}, ...rows))), caption, figure, - el('div', { class: 'qs-row' }, el('p', { class: 'qs-note qs-grow', text: 'Choose a pass to draw the circuit after it. A check is proof (exact ring arithmetic, a phase polynomial in whole eighths), exact (equal to the rounding of doubles) or approximate with its distance; the distances add up to the bound.' }), use)); + el('div', { class: 'qs-row' }, el('p', { class: 'qs-note qs-grow', text: t('compile_view.note') }), use)); rows.at(-1)?.querySelector('button')?.click(); } async function compile() { if (busy) return; const epsilon = Number(eps.value); - if (!(epsilon > 0)) { body.replaceChildren(el('p', { class: 'qs-note', text: 'ε is a positive number, such as 1e-6.' })); return; } + if (!(epsilon > 0)) { body.replaceChildren(el('p', { class: 'qs-note', text: t('compile_view.bad_eps') })); return; } busy = true; go.disabled = true; const src = program(); node.classList.remove('stale'); - body.replaceChildren(el('p', { class: 'qs-note', text: 'Compiling: each pass checked as it goes…' })); + body.replaceChildren(el('p', { class: 'qs-note', text: t('compile_view.compiling') })); try { const job = await engine.compile({ qasm: src, epsilon, level: Number(level.value), basis: basis.value, stages: true }); const v = await engine.verify(job); @@ -93,7 +95,7 @@ export function compilePanel({ engine, program, offer }) { const old = src !== compiledFor; node.classList.toggle('stale', old); body.querySelector('.qs-stale-note')?.remove(); - if (old) body.prepend(el('p', { class: 'qs-note qs-stale-note', text: 'For an earlier version of the program: compile again.' })); + if (old) body.prepend(el('p', { class: 'qs-note qs-stale-note', text: t('compile_view.stale') })); } go.addEventListener('click', compile); return { node, compile, stale }; diff --git a/wai/quantum-studio/src/converge.js b/wai/quantum-studio/src/converge.js index 14cbf8f5e..f3124c437 100644 --- a/wai/quantum-studio/src/converge.js +++ b/wai/quantum-studio/src/converge.js @@ -9,6 +9,7 @@ // reader makes are noted, so what they made alone is set beside the rest. // Nothing leaves this browser. import { el } from './dom.js'; +import { t } from './i18n.js'; export const ORGANIC = 'organic exploration'; const SAVED = 'wai.quantum-studio.organic'; @@ -33,11 +34,15 @@ const cap = (s) => s[0].toUpperCase() + s.slice(1); // The monitor's answer in words. export function convergeText(v, organicOn = false) { - const out = [v.converging ? `Converging: ${v.why}.` : `${cap(v.why)}.`]; + const out = [v.converging ? t('converge.converging', { why: v.why }) : `${cap(v.why)}.`]; const last = v.series?.at(-1); - if (last) out.push(`Your latest version is ${last.distance.toFixed(2)} from the model’s proposals in how it is built.`); - if (v.organic?.versions) out.push(`On your own: ${v.organic.versions} version${v.organic.versions === 1 ? '' : 's'}, ${v.organic.distance.toFixed(2)} from them${v.assisted?.distance != null ? `; the rest ${v.assisted.distance.toFixed(2)}` : ''}.`); - if (v.converging && !organicOn) out.push('Try a while on your own: the assistant then only asks.'); + if (last) out.push(t('converge.latest', { distance: last.distance.toFixed(2) })); + if (v.organic?.versions) { + const rest = v.assisted?.distance != null ? t('converge.own_rest', { distance: v.assisted.distance.toFixed(2) }) : ''; + const own = { n: v.organic.versions, distance: v.organic.distance.toFixed(2), rest }; + out.push(v.organic.versions === 1 ? t('converge.own_one', own) : t('converge.own_many', own)); + } + if (v.converging && !organicOn) out.push(t('converge.try_own')); return out.join(' '); } @@ -46,15 +51,15 @@ export function convergeText(v, organicOn = false) { export function convergePanel({ engine, history, inputs, organic }) { const line = el('p', { class: 'qs-note', id: 'qs-converge' }); const toggle = el('button', { class: 'qs-btn qs-small', id: 'qs-organic', type: 'button' }); - const chip = el('span', { class: 'qs-badge find', id: 'qs-organic-chip', text: 'on your own', hidden: true }); + const chip = el('span', { class: 'qs-badge find', id: 'qs-organic-chip', text: t('converge.chip'), hidden: true }); const node = el('div', { class: 'qs-converge', id: 'qs-converge-box' }, - el('div', { class: 'qs-actions' }, el('span', { class: 'qs-k', text: 'Your own way' }), chip, el('span', { class: 'qs-grow' }), toggle), + el('div', { class: 'qs-actions' }, el('span', { class: 'qs-k', text: t('converge.title') }), chip, el('span', { class: 'qs-grow' }), toggle), line); let last = null; function label() { const on = organic.on(); - toggle.textContent = on ? 'End: let the assistant propose again' : 'Explore on my own for a while'; - toggle.title = on ? 'The assistant, Alternatives and the Workbench may ask the model again' : 'The assistant only asks; Alternatives and the Workbench do not ask the model; your versions are noted as made on your own'; + toggle.textContent = on ? t('converge.end') : t('converge.start'); + toggle.title = on ? t('converge.end_title') : t('converge.start_title'); toggle.setAttribute('aria-pressed', String(on)); chip.hidden = !on; if (last) line.textContent = convergeText(last, on); diff --git a/wai/quantum-studio/src/course.js b/wai/quantum-studio/src/course.js new file mode 100644 index 000000000..2029f2633 --- /dev/null +++ b/wai/quantum-studio/src/course.js @@ -0,0 +1,275 @@ +// The circuits course in the Studio: the lessons, each with a short text and +// an exercise the engine checks (engine::course, the same course and the same +// check as `wai-quantum learn`). Check runs the program in the editor against +// the exercise and gives the engine's verdict for each case and each rule; +// What went wrong? says, in the reader's language, what the reader's +// diagnostics, the verdict and the critique found; Hint gives the lesson's +// hints one at a time with the engine's pointer toward where to look. The +// assistant, when a model is connected, is asked for one hint and told not to +// write the program; any program in its answer is held back. None of these +// shows the solution. +// +// Progress (which lessons passed, how many checks and hints) is kept only in +// this browser, and says so. +import { el } from './dom.js'; +import { t, locale, machineAssisted } from './i18n.js'; +import { markdown, inline } from './md.js'; +import { levelBadge } from './verdict-view.js'; + +const ITEM = 'wai.quantum-studio.course'; +const OPEN = 'wai.quantum-studio.course-open'; +const SCHEMA = 'wai.quantum.learn-progress/1'; + +// The reader's progress, kept in this browser alone. +export function progressStore() { + const fresh = () => ({ schema: SCHEMA, kept: 'only in this browser; nothing is sent anywhere', lessons: {}, current: null }); + let p = fresh(); + try { + const s = JSON.parse(localStorage.getItem(ITEM) || 'null'); + if (s && s.schema === SCHEMA && s.lessons && typeof s.lessons === 'object') p = { ...fresh(), ...s }; + } catch (e) {} + const save = () => { try { localStorage.setItem(ITEM, JSON.stringify(p)); } catch (e) {} }; + return { + get: () => p, + lesson(id) { p.lessons[id] ??= { passed: false, checks: 0, hints: 0 }; return p.lessons[id]; }, + passed: () => Object.values(p.lessons).filter((l) => l.passed).length, + save, + reset() { p = fresh(); save(); }, + }; +} + +// Code blocks out of a model's answer: a hint in words only. +const prose = (text) => String(text || '').replace(/```[\s\S]*?(```|$)/g, '').trim(); + +export function coursePane({ engine, program, load, ask, status }) { + const store = progressStore(); + let course = null; + let lesson = null; + let last = null; // {qasm, check} + const loc = locale(); + + const button = el('button', { class: 'qs-btn', id: 'qs-course-open', type: 'button', 'aria-expanded': 'false', 'aria-controls': 'qs-course', text: t('course.open'), title: t('course.open_title') }); + const list = el('nav', { class: 'qs-course-list', id: 'qs-course-list', 'aria-label': t('course.lessons') }); + const title = el('h3', { class: 'qs-course-title', id: 'qs-course-title' }); + const where = el('p', { class: 'qs-note', id: 'qs-course-where' }); + const text = el('div', { class: 'qs-course-text', id: 'qs-course-text' }); + const task = el('p', { class: 'qs-course-task', id: 'qs-course-task' }); + const must = el('ul', { class: 'qs-course-must', id: 'qs-course-must' }); + const passedBadge = el('span', { class: 'qs-badge', id: 'qs-course-passed', text: t('course.not_checked') }); + const statement = el('span', { class: 'qs-why', id: 'qs-course-statement' }); + const cases = el('ul', { class: 'qs-course-cases', id: 'qs-course-cases' }); + const rules = el('ul', { class: 'qs-course-rules', id: 'qs-course-rules' }); + const estimate = el('p', { class: 'qs-note', id: 'qs-course-estimate', hidden: true }); + const wrong = el('div', { class: 'qs-course-wrong', id: 'qs-course-wrong', hidden: true, role: 'region', 'aria-label': t('course.why') }); + const hints = el('ol', { class: 'qs-course-hints', id: 'qs-course-hints' }); + const pointer = el('p', { class: 'qs-note', id: 'qs-course-pointer', hidden: true }); + const asked = el('div', { class: 'qs-course-asked', id: 'qs-course-asked', hidden: true }); + const starterBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-course-starter', type: 'button', text: t('course.starter'), title: t('course.starter_title') }); + const checkBtn = el('button', { class: 'qs-btn qs-small qs-primary', id: 'qs-course-check', type: 'button', text: t('course.check'), title: t('course.check_title') }); + const hintBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-course-hint', type: 'button', text: t('course.hint') }); + const whyBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-course-why', type: 'button', text: t('course.why') }); + const askBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-course-ask', type: 'button', text: t('course.ask'), title: t('course.ask_title') }); + const prev = el('button', { class: 'qs-btn qs-small', id: 'qs-course-prev', type: 'button', text: t('course.previous') }); + const next = el('button', { class: 'qs-btn qs-small', id: 'qs-course-next', type: 'button', text: t('course.next') }); + const progress = el('span', { class: 'qs-note', id: 'qs-course-progress' }); + const reset = el('button', { class: 'qs-btn qs-small', id: 'qs-course-reset', type: 'button', text: t('course.reset') }); + const companion = el('p', { class: 'qs-note', id: 'qs-course-companion' }); + const translation = machineAssisted() ? el('p', { class: 'qs-note qs-course-translation', id: 'qs-course-translation', text: t('course.machine_assisted') }) : null; + const body = el('article', { class: 'qs-course-lesson', id: 'qs-course-lesson', 'aria-labelledby': 'qs-course-title' }, + where, title, translation, text, + el('div', { class: 'qs-course-exercise' }, + el('div', { class: 'qs-k', text: t('course.exercise') }), task, must, + el('div', { class: 'qs-actions' }, starterBtn, checkBtn, hintBtn, whyBtn, askBtn), + el('div', { class: 'qs-course-result', id: 'qs-course-result', 'aria-live': 'polite' }, el('p', {}, passedBadge, ' ', statement), cases, rules, estimate), + hints, pointer, wrong, asked), + el('div', { class: 'qs-actions qs-course-nav' }, prev, next)); + const node = el('section', { class: 'qs-pane qs-course', id: 'qs-course', hidden: true, 'aria-labelledby': 'qs-course-h' }, + el('div', { class: 'qs-head' }, el('h2', { id: 'qs-course-h', text: t('course.title') }), el('span', { class: 'qs-grow' }), progress, reset), + el('div', { class: 'qs-course-body' }, list, body), + companion); + + const say = (m, cls) => status?.(m, cls); + const showProgress = () => { + progress.textContent = t('course.progress', { passed: store.passed(), of: course?.lessons ?? '?' }) + ' · ' + t('course.kept'); + for (const b of list.querySelectorAll('[data-lesson]')) { + const done = !!store.get().lessons[b.dataset.lesson]?.passed; + b.classList.toggle('done', done); + b.querySelector('.qs-course-mark').textContent = done ? '✓' : '·'; + b.setAttribute('aria-current', String(b.dataset.lesson === lesson?.id)); + } + }; + + function renderList() { + list.replaceChildren(...course.units.map((u) => el('div', { class: 'qs-course-unit' }, + el('div', { class: 'qs-k', text: u.title }), + el('ol', { start: String(u.lessons[0]?.number ?? 1) }, ...u.lessons.map((l) => el('li', {}, + el('button', { class: 'qs-course-item', type: 'button', 'data-lesson': l.id, onclick: () => show(l.id) }, + el('span', { class: 'qs-course-mark', 'aria-hidden': 'true', text: '·' }), ' ', l.title))))))); + const k = course.companions?.[0]; + companion.replaceChildren(...(k ? [t('course.companion') + ' ', el('a', { href: k.url, target: '_blank', rel: 'noopener', text: k.title }), ` · ${k.summary}`] : [])); + } + + function clearResult() { + last = null; + passedBadge.textContent = t('course.not_checked'); passedBadge.className = 'qs-badge'; + statement.textContent = ''; + cases.replaceChildren(); rules.replaceChildren(); + estimate.hidden = true; wrong.hidden = true; wrong.replaceChildren(); + pointer.hidden = true; asked.hidden = true; asked.replaceChildren(); + } + + async function show(id) { + try { + lesson = await engine.courseLesson(id, loc); + } catch (e) { say(e.message, 'bad'); return; } + store.get().current = id; store.save(); + where.textContent = t('course.where', { number: lesson.number, of: lesson.of, unit: lesson.unit_title }); + title.textContent = lesson.title; + text.replaceChildren(...markdown(lesson.text)); + task.replaceChildren(...inline(lesson.task)); + must.replaceChildren(...lesson.requirements.map((r) => el('li', {}, ...inline(r)))); + prev.disabled = !lesson.previous; next.disabled = !lesson.next; + clearResult(); + // The hints already read stay read. + const seen = Math.min(store.lesson(id).hints || 0, lesson.hints.length); + hints.replaceChildren(...lesson.hints.slice(0, seen).map((h, i) => el('li', {}, el('span', { class: 'qs-k', text: t('course.hint_of', { level: i + 1, of: lesson.hints.length }) }), ' ', ...inline(h)))); + hintBtn.textContent = seen >= lesson.hints.length ? t('course.no_more_hints') : t('course.hint'); + showProgress(); + } + + async function check() { + if (!lesson) return null; + const qasm = program(); + checkBtn.disabled = true; + try { + const c = await engine.courseCheck(lesson.id, qasm, loc); + last = { qasm, check: c }; + const rec = store.lesson(lesson.id); + rec.checks += 1; + if (c.passed && !rec.passed) { rec.passed = true; rec.passed_at = Date.now(); } + store.save(); + passedBadge.textContent = c.passed ? t('course.passed') : t('course.not_yet'); + passedBadge.className = 'qs-badge ' + (c.passed ? 'ok' : 'bad'); + statement.textContent = c.statement; + cases.replaceChildren(...c.cases.map((k) => el('li', { class: k.passed ? 'ok' : 'bad' }, + k.name ? el('span', { class: 'qs-course-case', text: `${k.name}: ` }) : null, levelBadge(k.verdict), el('span', { class: 'qs-why', text: ` ${k.statement ?? ''}` })))); + rules.replaceChildren(...c.constraints.map((r) => el('li', { class: r.holds ? 'ok' : 'bad', 'data-rule': r.rule }, `${r.holds ? '✓' : '✗'} `, ...inline(r.text)))); + estimate.hidden = !c.estimate?.figures; + estimate.textContent = c.estimate?.figures ?? ''; + if (!wrong.hidden) showWrong(c); + pointer.hidden = true; + showProgress(); + say(c.passed ? t('course.passed_status', { title: lesson.title }) : t('course.not_yet_status'), c.passed ? 'ok' : 'bad'); + return c; + } catch (e) { say(e.message, 'bad'); return null; } finally { checkBtn.disabled = false; } + } + + // The last check, if it was of the program as it is now; else a new one. + const current = async () => (last && last.qasm === program() ? last.check : check()); + + function showWrong(c) { + wrong.hidden = false; + const items = c.what_went_wrong.map((w) => el('li', { class: `qs-course-w-${w.kind}` }, ...inline(w.text), + w.engine ? el('span', { class: 'qs-course-engine' }, el('span', { class: 'qs-k', text: ` ${t('course.engine_says')} ` }), w.engine) : null)); + wrong.replaceChildren(el('div', { class: 'qs-k', text: t('course.why') }), + items.length ? el('ul', {}, ...items) : el('p', { class: 'qs-note', text: t('course.nothing_wrong') }), + c.pointer && !c.passed ? el('p', { class: 'qs-note' }, el('span', { class: 'qs-k', text: t('course.where_to_look') + ' ' }), c.pointer) : null); + } + + async function why() { + const c = await current(); + if (c) showWrong(c); + } + + async function hint() { + if (!lesson) return; + const rec = store.lesson(lesson.id); + const level = Math.min((rec.hints || 0) + 1, lesson.hints.length); + try { + const h = await engine.courseHint(lesson.id, program(), level, loc); + if (h.level > (rec.hints || 0)) { + rec.hints = h.level; store.save(); + hints.append(el('li', {}, el('span', { class: 'qs-k', text: h.label }), ' ', ...inline(h.hint))); + } + pointer.hidden = !h.pointer || h.passed; + pointer.textContent = h.pointer ? `${t('course.where_to_look')} ${h.pointer}` : ''; + hintBtn.textContent = h.more ? t('course.hint') : t('course.no_more_hints'); + } catch (e) { say(e.message, 'bad'); } + } + + async function askModel() { + if (!lesson) return; + const c = await current(); + if (!c) return; + asked.hidden = false; + if (!ask.available()) { asked.replaceChildren(el('p', { class: 'qs-note', text: t('course.ask_needs_model') })); return; } + asked.replaceChildren(el('p', { class: 'qs-note', text: t('course.asking') })); + const findings = c.what_went_wrong.map((w) => `- ${w.text}${w.engine ? ` (${w.engine})` : ''}`).join('\n'); + const prompt = [ + `I am doing the exercise "${lesson.title}": ${lesson.task}`, + `It must: ${lesson.requirements.join('; ')}.`, + `The engine checked my program: ${c.statement}`, + findings ? `What went wrong, as the engine found it:\n${findings}` : '', + 'Give me one hint toward the next step, in a sentence or two. Do not write the program, any line of it, or the answer; point me at what to think about.', + loc === 'zh-CN' ? 'Answer in Simplified Chinese.' : '', + ].filter(Boolean).join('\n\n'); + try { + const a = await ask.hint(prompt); + // A program in the answer that passes the exercise would be the solution: it is held back. + let held = false; + for (const p of a.proposals || []) { + const r = await engine.courseCheck(lesson.id, p.qasm, loc).catch(() => null); + if (r?.passed) held = true; + } + const words = prose(a.text); + asked.replaceChildren(el('div', { class: 'qs-k', text: t('course.assistant_says') }), + words ? el('p', {}, ...inline(words)) : null, + held || words !== String(a.text || '').trim() ? el('p', { class: 'qs-note', id: 'qs-course-held', text: held ? t('course.held_solution') : t('course.held_code') }) : null); + } catch (e) { asked.replaceChildren(el('p', { class: 'qs-note', text: t('course.ask_failed', { error: e.message }) })); } + } + + starterBtn.addEventListener('click', async () => { + if (!lesson) return; + if (await load(lesson.starter, lesson)) { clearResult(); say(t('course.starter_loaded', { title: lesson.title }), 'ok'); } + }); + checkBtn.addEventListener('click', check); + hintBtn.addEventListener('click', hint); + whyBtn.addEventListener('click', why); + askBtn.addEventListener('click', askModel); + prev.addEventListener('click', () => lesson?.previous && show(lesson.previous)); + next.addEventListener('click', () => lesson?.next && show(lesson.next)); + // Two clicks, so that one slip does not clear it. + let armed = null; + reset.addEventListener('click', () => { + if (!armed) { + reset.textContent = t('course.reset_confirm'); + armed = setTimeout(() => { armed = null; reset.textContent = t('course.reset'); }, 5000); + return; + } + clearTimeout(armed); armed = null; + reset.textContent = t('course.reset'); + store.reset(); + if (lesson) show(lesson.id); + say(t('course.reset_done'), 'ok'); + }); + + async function open(on = node.hidden) { + node.hidden = !on; + button.setAttribute('aria-expanded', String(on)); + try { localStorage.setItem(OPEN, on ? '1' : '0'); } catch (e) {} + if (!on || course) return; + try { + course = await engine.course(loc); + renderList(); + const p = store.get(); + const first = course.units.flatMap((u) => u.lessons).find((l) => !p.lessons[l.id]?.passed) ?? course.units[0].lessons[0]; + await show(p.current && course.units.some((u) => u.lessons.some((l) => l.id === p.current)) ? p.current : first.id); + } catch (e) { say(e.message, 'bad'); } + } + button.addEventListener('click', () => open()); + let reopen = false; + try { reopen = localStorage.getItem(OPEN) === '1'; } catch (e) {} + if (reopen) open(true); + + return { node, button, open, show, check, store, current: () => lesson }; +} diff --git a/wai/quantum-studio/src/critique-view.js b/wai/quantum-studio/src/critique-view.js index f7e023d64..46021273d 100644 --- a/wai/quantum-studio/src/critique-view.js +++ b/wai/quantum-studio/src/critique-view.js @@ -4,10 +4,11 @@ // and its smells, each with the lines it is on, Show, and Fix where the engine // can rewrite it. Every figure is the engine's; nothing here is a model's guess. import { el } from './dom.js'; +import { t } from './i18n.js'; export const MEASURES = [ - ['qubits', 'qubits'], ['gates', 'gates'], ['two_qubit', 'two-qubit'], ['depth', 'depth'], - ['two_qubit_depth', 'two-qubit depth'], ['t_count', 'T'], ['non_clifford', 'non-Clifford'], + ['qubits', t('critique_view.qubits')], ['gates', t('critique_view.gates')], ['two_qubit', t('critique_view.two_qubit')], ['depth', t('critique_view.depth')], + ['two_qubit_depth', t('critique_view.two_qubit_depth')], ['t_count', 'T'], ['non_clifford', t('critique_view.non_clifford')], ]; // The measures as chips, each with its change from `base` when it has one. @@ -16,7 +17,7 @@ export function chips(c, base) { const d = base && base[k] != null ? c[k] - base[k] : 0; return el('span', { class: 'qs-chip', 'data-k': k }, el('b', { text: String(c[k]) }), ' ' + label, - d ? el('span', { class: 'qs-delta', title: 'since the last run', text: d > 0 ? ` +${d}` : ` −${-d}` }) : null); + d ? el('span', { class: 'qs-delta', title: t('critique_view.since_last_run'), text: d > 0 ? ` +${d}` : ` −${-d}` }) : null); }); } @@ -24,38 +25,38 @@ export function fidelityChip(c) { const f = c.fidelity_model; if (!f) return null; const r = f.rates || {}; - return el('span', { class: 'qs-chip qs-model', title: `The ${f.note}. Rates: ${r.one_qubit} per one-qubit gate, ${r.two_qubit} per two-qubit gate, ${r.readout} per measured qubit.` }, - el('b', { text: Number(f.estimate).toFixed(3) }), ' fidelity, modelled'); + return el('span', { class: 'qs-chip qs-model', title: t('critique_view.fidelity_title', { note: f.note, one_qubit: r.one_qubit, two_qubit: r.two_qubit, readout: r.readout }) }, + el('b', { text: Number(f.estimate).toFixed(3) }), ' ' + t('critique_view.fidelity_modelled')); } -const where = (s) => (s.lines?.length ? (s.lines.length === 1 ? `line ${s.lines[0]}` : `lines ${s.lines.join(', ')}`) : s.qubits?.length ? s.qubits.map((q) => `q${q}`).join(' ') : ''); +const where = (s) => (s.lines?.length ? (s.lines.length === 1 ? t('critique_view.line', { line: s.lines[0] }) : t('critique_view.lines', { lines: s.lines.join(', ') })) : s.qubits?.length ? s.qubits.map((q) => `q${q}`).join(' ') : ''); // The panel. `onShow(smell)` takes the reader to a smell, `onFix(smell)` asks // for its rewrite, and `onMark(smell | null)` marks it on the diagram. export function critiquePanel({ onShow, onFix, onMark }) { const head = el('div', { class: 'qs-chips', id: 'qs-measures' }); const list = el('ul', { class: 'qs-smells', id: 'qs-smells' }); - const node = el('div', { class: 'qs-critique', id: 'qs-critique', role: 'group', 'aria-label': 'The engine’s critique' }, head, list); + const node = el('div', { class: 'qs-critique', id: 'qs-critique', role: 'group', 'aria-label': t('critique_view.label') }, head, list); let last = null; // the latest critique let base = null; // the measures when the program last ran function update(c) { last = c; if (!c?.ok) { - head.replaceChildren(el('span', { class: 'qs-note', text: 'The critique waits for a program the engine can read.' })); + head.replaceChildren(el('span', { class: 'qs-note', text: t('critique_view.waiting') })); list.replaceChildren(); return; } head.replaceChildren(...[...chips(c, base), - c.clifford ? el('span', { class: 'qs-badge ok', id: 'qs-clifford', title: 'Every gate is a Clifford gate: the stabilizer tableau runs it exactly, at any width.', text: 'Clifford' }) : null, + c.clifford ? el('span', { class: 'qs-badge ok', id: 'qs-clifford', title: t('critique_view.clifford_title'), text: 'Clifford' }) : null, fidelityChip(c)].filter(Boolean)); // replaceChildren writes a null as the text "null" list.replaceChildren(...c.smells.map((s) => { const li = el('li', { class: 'qs-smell', 'data-kind': s.kind, tabindex: '-1' }, el('span', { class: 'qs-smell-text', text: s.message }), el('span', { class: 'qs-smell-where qs-note', text: where(s) }), el('span', { class: 'qs-smell-acts' }, - el('button', { class: 'qs-btn qs-small', type: 'button', text: 'Show', onclick: () => onShow(s) }), - s.fixable ? el('button', { class: 'qs-btn qs-small qs-fix', type: 'button', text: 'Fix', title: 'The engine rewrites these lines and checks the result is the same circuit', onclick: () => onFix(s) }) : null)); + el('button', { class: 'qs-btn qs-small', type: 'button', text: t('critique_view.show'), onclick: () => onShow(s) }), + s.fixable ? el('button', { class: 'qs-btn qs-small qs-fix', type: 'button', text: t('critique_view.fix'), title: t('critique_view.fix_title'), onclick: () => onFix(s) }) : null)); for (const [ev, v] of [['mouseenter', s], ['focusin', s], ['mouseleave', null], ['focusout', null]]) li.addEventListener(ev, () => onMark(v)); return li; })); diff --git a/wai/quantum-studio/src/debugger.js b/wai/quantum-studio/src/debugger.js index 681b8574d..6c1a46ea8 100644 --- a/wai/quantum-studio/src/debugger.js +++ b/wai/quantum-studio/src/debugger.js @@ -7,6 +7,7 @@ // path on it. Stepping back runs again from a state the engine saved, so a step // seen twice is the same bits (its digest says so). import { el } from './dom.js'; +import { t } from './i18n.js'; import { amplitudeTable, qubitCards, densityView, overlay } from './state-views.js'; import { declared } from './generators.js'; @@ -16,36 +17,36 @@ const weightText = (b) => b.weight_exact ?? Number(b.weight).toFixed(4); // job's settings (its noise); `inputs()` a generator's values; `onStep(answer)` is // told each step shown (the diagram marks the next gate). export function debuggerPane({ engine, program, job, inputs, onStep }) { - const startBtn = el('button', { class: 'qs-btn qs-small qs-primary', id: 'qs-debug-start', type: 'button', text: 'Step through', title: 'Step through the program on the engine, forward and back' }); + const startBtn = el('button', { class: 'qs-btn qs-small qs-primary', id: 'qs-debug-start', type: 'button', text: t('debugger.start'), title: t('debugger.start_title') }); const btn = (id, text, title) => el('button', { class: 'qs-btn qs-small', id, type: 'button', text, title, disabled: true }); - const toStart = btn('qs-debug-first', '⏮', 'To the start'); - const back = btn('qs-debug-back', '◀', 'One step back'); - const fwd = btn('qs-debug-forward', '▶', 'One step forward'); - const toEnd = btn('qs-debug-last', '⏭', 'To the end'); - const slider = el('input', { type: 'range', id: 'qs-debug-slider', min: '0', max: '0', value: '0', step: '1', disabled: true, 'aria-label': 'The step the state is shown at' }); + const toStart = btn('qs-debug-first', '⏮', t('debugger.to_start')); + const back = btn('qs-debug-back', '◀', t('debugger.back')); + const fwd = btn('qs-debug-forward', '▶', t('debugger.forward')); + const toEnd = btn('qs-debug-last', '⏭', t('debugger.to_end')); + const slider = el('input', { type: 'range', id: 'qs-debug-slider', min: '0', max: '0', value: '0', step: '1', disabled: true, 'aria-label': t('debugger.slider') }); const pos = el('span', { class: 'qs-note', id: 'qs-debug-pos', 'aria-live': 'polite' }); - const method = el('span', { class: 'qs-badge', id: 'qs-debug-method', text: 'not started' }); + const method = el('span', { class: 'qs-badge', id: 'qs-debug-method', text: t('debugger.not_started') }); const next = el('p', { class: 'qs-dbg-next', id: 'qs-debug-next' }); const path = el('p', { class: 'qs-note', id: 'qs-debug-path' }); const ent = el('p', { class: 'qs-note', id: 'qs-debug-ent' }); const qubitsBox = el('div', { class: 'qs-dbg-box' }); const ampsBox = el('div', { class: 'qs-dbg-box' }); - const pick = el('div', { class: 'qs-dbg-pick', id: 'qs-debug-pick', role: 'group', 'aria-label': 'The qubits whose reduced state to show' }); + const pick = el('div', { class: 'qs-dbg-pick', id: 'qs-debug-pick', role: 'group', 'aria-label': t('debugger.pick') }); const densityBox = el('div', { class: 'qs-dbg-box' }); const overlayBox = el('div', { class: 'qs-dbg-box' }); - const overlayPart = el('details', { class: 'qs-dbg-part', id: 'qs-debug-noise', open: true, hidden: true }, el('summary', { text: 'Against the noiseless state' }), overlayBox); + const overlayPart = el('details', { class: 'qs-dbg-part', id: 'qs-debug-noise', open: true, hidden: true }, el('summary', { text: t('debugger.against_noiseless') }), overlayBox); const treeBox = el('div', { class: 'qs-dbg-tree', id: 'qs-debug-tree' }); const body = el('div', { class: 'qs-dbg-body', id: 'qs-debug-body', hidden: true }, el('div', { class: 'qs-dbg-controls' }, toStart, back, slider, fwd, toEnd, pos), next, path, - el('details', { class: 'qs-dbg-part', open: true }, el('summary', { text: 'Qubits' }), qubitsBox, ent), - el('details', { class: 'qs-dbg-part', open: true }, el('summary', { text: 'Amplitudes' }), ampsBox), - el('details', { class: 'qs-dbg-part', id: 'qs-debug-density' }, el('summary', { text: 'Reduced state' }), pick, densityBox), + el('details', { class: 'qs-dbg-part', open: true }, el('summary', { text: t('debugger.qubits') }), qubitsBox, ent), + el('details', { class: 'qs-dbg-part', open: true }, el('summary', { text: t('debugger.amplitudes') }), ampsBox), + el('details', { class: 'qs-dbg-part', id: 'qs-debug-density' }, el('summary', { text: t('debugger.reduced') }), pick, densityBox), overlayPart, - el('details', { class: 'qs-dbg-part', open: true }, el('summary', { text: 'Branches' }), treeBox)); + el('details', { class: 'qs-dbg-part', open: true }, el('summary', { text: t('debugger.branches') }), treeBox)); const node = el('section', { class: 'qs-pane qs-debug', 'aria-labelledby': 'qs-debug-h', id: 'qs-debug' }, - el('div', { class: 'qs-head' }, el('h2', { id: 'qs-debug-h', text: 'Step through' }), method, el('span', { class: 'qs-grow' }), startBtn), - el('p', { class: 'qs-note qs-dbg-empty', id: 'qs-debug-empty', text: 'Step through the program on the engine: the state at each step, each qubit on its Bloch sphere, and every branch of its measurements with its weight.' }), + el('div', { class: 'qs-head' }, el('h2', { id: 'qs-debug-h', text: t('debugger.title') }), method, el('span', { class: 'qs-grow' }), startBtn), + el('p', { class: 'qs-note qs-dbg-empty', id: 'qs-debug-empty', text: t('debugger.empty') }), body); let on = false; @@ -73,7 +74,7 @@ export function debuggerPane({ engine, program, job, inputs, onStep }) { } catch (e) { if (/ends at position/.test(e.message) && to !== 'end') return show('end'); if (mine !== seq) return; - method.textContent = 'refused'; method.className = 'qs-badge bad'; + method.textContent = t('debugger.refused'); method.className = 'qs-badge bad'; next.textContent = e.message; return; } @@ -87,15 +88,16 @@ export function debuggerPane({ engine, program, job, inputs, onStep }) { slider.max = String(v.length); slider.value = String(v.position); slider.disabled = false; for (const b of [toStart, back]) b.disabled = v.position === 0; for (const b of [fwd, toEnd]) b.disabled = v.at_end; - pos.textContent = `position ${v.position} of ${v.length}`; - const line = (s) => (s?.line ? ` (line ${s.line})` : ''); - next.replaceChildren(el('b', { text: v.next ? 'Next: ' : 'The end of the program.' }), v.next ? `${v.next.label}${line(v.next)}` : ''); + pos.textContent = t('debugger.position', { position: v.position, length: v.length }); + const line = (s) => (s?.line ? t('debugger.line', { line: s.line }) : ''); + next.replaceChildren(el('b', { text: v.next ? t('debugger.next') : t('debugger.end') }), v.next ? `${v.next.label}${line(v.next)}` : ''); const d = v.decisions.at(-1); - const read = d && d.position + 1 === v.position ? ` · just now: ${d.kind} q${d.qubit}${d.line ? ` (line ${d.line})` : ''} read ${d.outcome} with p ${Number(d.p).toFixed(4)} (${d.by === 'recorded' ? 'the branch chosen' : d.by === 'likelier' ? 'the likelier' : 'certain'})` : ''; - path.textContent = `Path: ${v.outcomes.length ? `outcomes ${v.outcomes.join(' ')}` : 'no measurement yet'} · weight ${Number(v.weight).toFixed(6)} · bits ${v.bits ? v.classical : 'none'}${read}`; + const by = (x) => (x === 'recorded' ? t('debugger.by_recorded') : x === 'likelier' ? t('debugger.by_likelier') : t('debugger.by_certain')); + const read = d && d.position + 1 === v.position ? t('debugger.just_now', { kind: d.kind, qubit: d.qubit, line: line(d), outcome: d.outcome, p: Number(d.p).toFixed(4), by: by(d.by) }) : ''; + path.textContent = t('debugger.path', { outcomes: v.outcomes.length ? t('debugger.outcomes', { outcomes: v.outcomes.join(' ') }) : t('debugger.no_measurement'), weight: Number(v.weight).toFixed(6), bits: v.bits ? v.classical : t('debugger.no_bits'), read }); qubitsBox.replaceChildren(qubitCards(v.bloch, v.ideal?.bloch)); const e = v.entanglement; - ent.textContent = !e ? '' : e.entropy_bits != null ? `Entangled across q${e.cut.join(' q')} | the rest: ${Number(e.entropy_bits).toFixed(6)} bits (Schmidt rank ${e.schmidt_rank}).` : e.log_negativity != null ? `Across q${e.cut.join(' q')} | the rest: logarithmic negativity ${Number(e.log_negativity).toFixed(6)} bits.` : e.note || ''; + ent.textContent = !e ? '' : e.entropy_bits != null ? t('debugger.entangled', { cut: e.cut.join(' q'), bits: Number(e.entropy_bits).toFixed(6), rank: e.schmidt_rank }) : e.log_negativity != null ? t('debugger.negativity', { cut: e.cut.join(' q'), value: Number(e.log_negativity).toFixed(6) }) : e.note || ''; ampsBox.replaceChildren(amplitudeTable(v.state)); if (pick.childElementCount !== v.qubits) { if (chosen == null || chosen.some((q) => q >= v.qubits)) chosen = Array.from({ length: Math.min(2, v.qubits) }, (_, q) => q); @@ -139,20 +141,20 @@ export function debuggerPane({ engine, program, job, inputs, onStep }) { const b = tree.answer; const taken = last?.outcomes ?? []; const onPath = (prefix) => prefix.every((o, i) => taken[i] === o); - const leafText = (l) => `final: ${l.final.map((x) => `${x.bits} ${Number(x.p).toFixed(4)}`).join(', ')}${l.final_outcomes > l.final.length ? ` and ${l.final_outcomes - l.final.length} more` : ''}`; + const leafText = (l) => t('debugger.final', { final: l.final.map((x) => `${x.bits} ${Number(x.p).toFixed(4)}`).join(', '), more: l.final_outcomes > l.final.length ? t('debugger.more', { n: l.final_outcomes - l.final.length }) : '' }); const walk = (n, prefix) => el('li', {}, - el('span', { class: 'qs-dbg-node', text: `${n.label}${n.line ? ` (line ${n.line})` : ''}` }), + el('span', { class: 'qs-dbg-node', text: `${n.label}${n.line ? t('debugger.line', { line: n.line }) : ''}` }), el('ul', {}, ...n.branches.map((br) => { const here = [...prefix, br.outcome]; const leaf = br.leaf != null ? b.leaves[br.leaf] : null; const choose = el('button', { class: `qs-btn qs-small qs-dbg-branch${onPath(here) ? ' on' : ''}`, type: 'button', 'data-path': here.join(''), 'aria-pressed': String(onPath(here)), - text: `reads ${br.outcome} · p ${br.p_exact ?? Number(br.p).toFixed(4)} · weight ${weightText(br)}`, + text: t('debugger.reads', { outcome: br.outcome, p: br.p_exact ?? Number(br.p).toFixed(4), weight: weightText(br) }), onclick: () => { outcomes = leaf ? leaf.outcomes.slice() : here; show(Number(slider.value)); } }); return el('li', { class: leaf ? 'qs-dbg-leaf' : '' }, choose, leaf ? el('span', { class: 'qs-note', text: ` ${leafText(leaf)}` }) : null, br.then ? el('ul', {}, walk(br.then, here)) : null); }))); treeBox.replaceChildren( - el('p', { class: 'qs-note', text: `${b.leaves.length} branch${b.leaves.length === 1 ? '' : 'es'}; weights ${b.weights}.` }), - b.tree ? el('ul', { class: 'qs-dbg-root', 'aria-label': 'The branches at the program’s measurements in the middle' }, walk(b.tree, [])) : el('p', { class: 'qs-note', id: 'qs-debug-one', text: `No measurement in the middle: one path; ${leafText(b.leaves[0])}.` })); + el('p', { class: 'qs-note', text: b.leaves.length === 1 ? t('debugger.branch_count_one', { n: b.leaves.length, weights: b.weights }) : t('debugger.branch_count', { n: b.leaves.length, weights: b.weights }) }), + b.tree ? el('ul', { class: 'qs-dbg-root', 'aria-label': t('debugger.tree_label') }, walk(b.tree, [])) : el('p', { class: 'qs-note', id: 'qs-debug-one', text: t('debugger.one_path', { final: leafText(b.leaves[0]) }) })); } async function start() { @@ -160,7 +162,7 @@ export function debuggerPane({ engine, program, job, inputs, onStep }) { outcomes = []; node.querySelector('#qs-debug-empty').hidden = true; body.hidden = false; - startBtn.textContent = 'Again'; + startBtn.textContent = t('debugger.again'); await Promise.all([show(0), branches()]); } diff --git a/wai/quantum-studio/src/device.js b/wai/quantum-studio/src/device.js index 0fdeb5ff9..973b55fdc 100644 --- a/wai/quantum-studio/src/device.js +++ b/wai/quantum-studio/src/device.js @@ -17,6 +17,7 @@ // thicker on a pair, every mark with its numbers on hover, and the whole as a // table beside it. import { el } from './dom.js'; +import { t as tr } from './i18n.js'; const NS = 'http://www.w3.org/2000/svg'; const KEPT = 'wai.quantum-studio.targets'; @@ -45,9 +46,9 @@ export function bin(x, b) { // What a qubit is coloured by. export const COLOUR_BY = { - readout: { label: 'readout error', of: (q) => q.readout_error }, - one: { label: 'one-qubit error', of: (q) => q.error_1q }, - t1: { label: 'T1 (shorter is worse)', of: (q) => (q.t1_us == null ? null : -q.t1_us), show: (x) => fmt(-x) + ' µs', longer: true }, + readout: { label: tr('device.by_readout'), of: (q) => q.readout_error }, + one: { label: tr('device.by_one'), of: (q) => q.error_1q }, + t1: { label: tr('device.by_t1'), of: (q) => (q.t1_us == null ? null : -q.t1_us), show: (x) => fmt(-x) + ' µs', longer: true }, }; // The coupling map of `view` (the engine's), qubits coloured by `by`, and the @@ -66,13 +67,13 @@ export function couplingSvg(view, { by = 'readout', placement = null, width = 34 const colour = COLOUR_BY[by] || COLOUR_BY.readout; const qb = bounds(qs.map(colour.of)); const pb = bounds(pairs.map((p) => p.error)); - const g = svgNode('svg', { viewBox: `0 0 ${w.toFixed(1)} ${h.toFixed(1)}`, width: w.toFixed(0), height: h.toFixed(0), class: 'qs-dev-svg', role: 'img', 'aria-label': `${view.name}: ${qs.length} qubits and ${pairs.length} coupled pairs, each pair coloured by its two-qubit error and each qubit by its ${colour.label}, in five bins from lowest to highest${placement ? `; the program placed on physical qubits ${placement.layout.join(', ')}` : ''}` }); + const g = svgNode('svg', { viewBox: `0 0 ${w.toFixed(1)} ${h.toFixed(1)}`, width: w.toFixed(0), height: h.toFixed(0), class: 'qs-dev-svg', role: 'img', 'aria-label': tr('device.map_label', { name: view.name, qubits: qs.length, pairs: pairs.length, by: colour.label, placed: placement ? tr('device.map_placed', { layout: placement.layout.join(', ') }) : '' }) }); for (const p of pairs) { const [a, b] = p.qubits; const [x1, y1] = at(qs[a]), [x2, y2] = at(qs[b]); const k = bin(p.error, pb); const line = svgNode('line', { x1: x1.toFixed(1), y1: y1.toFixed(1), x2: x2.toFixed(1), y2: y2.toFixed(1), class: `qs-dev-pair ${k == null ? 'qs-dev-none' : `qs-dev-b${k}`}`, 'data-bin': k ?? 'none', 'data-pair': `${a}-${b}` }, - svgNode('title', {}, `pair ${a}–${b}: ${p.gate || 'two-qubit gate'} error ${fmt(p.error)}`)); + svgNode('title', {}, tr('device.pair_tip', { a, b, gate: p.gate || tr('device.two_qubit_gate'), error: fmt(p.error) }))); g.append(line); } const placed = new Map((placement?.layout || []).map((p, i) => [p, i])); @@ -81,7 +82,7 @@ export function couplingSvg(view, { by = 'readout', placement = null, width = 34 const [x, y] = at(q); const v = colour.of(q); const k = bin(v, qb); - const tip = `qubit ${q.qubit}${q.label ? ` (${q.label})` : ''}: readout error ${fmt(q.readout_error)}, one-qubit error ${fmt(q.error_1q)}, T1 ${fmt(q.t1_us)} µs, T2 ${fmt(q.t2_us)} µs`; + const tip = tr('device.qubit_tip', { qubit: q.qubit, label: q.label ? tr('device.qubit_label', { label: q.label }) : '', readout: fmt(q.readout_error), single: fmt(q.error_1q), t_one: fmt(q.t1_us), t_two: fmt(q.t2_us) }); const grp = svgNode('g', { class: 'qs-dev-q', 'data-q': q.qubit, 'data-bin': k ?? 'none' }, svgNode('circle', { cx: x.toFixed(1), cy: y.toFixed(1), r, class: `qs-dev-dot ${k == null ? 'qs-dev-none' : `qs-dev-b${k}`}` }), svgNode('text', { x: x.toFixed(1), y: y.toFixed(1), class: `qs-dev-n qs-dev-ink${k == null ? 'n' : k}`, 'text-anchor': 'middle', 'dominant-baseline': 'central' }, String(q.qubit)), @@ -95,31 +96,31 @@ export function couplingSvg(view, { by = 'readout', placement = null, width = 34 if (ended.has(q.qubit) && placement.final_layout.join() !== placement.layout.join() && placed.get(q.qubit) !== ended.get(q.qubit)) { g.append(svgNode('g', { class: 'qs-dev-ended' }, svgNode('circle', { cx: x.toFixed(1), cy: y.toFixed(1), r: r + 7, class: 'qs-dev-ring2' }), - svgNode('text', { x: (x - r - 4).toFixed(1), y: (y + r + 10).toFixed(1), class: 'qs-dev-l', 'text-anchor': 'end' }, `q${ended.get(q.qubit)} ends`))); + svgNode('text', { x: (x - r - 4).toFixed(1), y: (y + r + 10).toFixed(1), class: 'qs-dev-l', 'text-anchor': 'end' }, tr('device.ends', { logical: ended.get(q.qubit) })))); } } return { svg: g, qubitBounds: qb, pairBounds: pb, colour }; } // The legend: the five bins with their ranges, for the pairs and the qubits. -function legend(pb, qb, colour) { +export function legend(pb, qb, colour) { // A scale of times shown as their negation (a longer T1 is better) reads ≥ and <. const row = (what, b, show = fmt, longer = false) => el('div', { class: 'qs-dev-key' }, el('span', { class: 'qs-k', text: what }), ...Array.from({ length: BINS }, (_, k) => el('span', { class: 'qs-dev-sw' }, el('i', { class: `qs-dev-b${k}`, 'aria-hidden': 'true' }), el('span', { text: !b.length ? '' : k === BINS - 1 ? `${longer ? '<' : '>'} ${show(b[BINS - 2])}` : `${longer ? '≥' : '≤'} ${show(b[k])}` })))); - return el('div', { class: 'qs-dev-legend' }, row('pairs: two-qubit error', pb), row(`qubits: ${colour.label}`, qb, colour.show || fmt, !!colour.longer), - el('p', { class: 'qs-note', text: 'Each scale is this device’s own: five bins by quantile, lowest first. A dashed mark states no value. Hover a qubit or a pair for its numbers.' })); + return el('div', { class: 'qs-dev-legend' }, row(tr('device.legend_pairs'), pb), row(tr('device.legend_qubits', { by: colour.label }), qb, colour.show || fmt, !!colour.longer), + el('p', { class: 'qs-note', text: tr('device.legend_note') })); } // The same numbers as a table. -function table(view) { - return el('details', { class: 'qs-dev-table' }, el('summary', { text: 'As a table' }), +export function table(view) { + return el('details', { class: 'qs-dev-table' }, el('summary', { text: tr('device.as_table') }), el('table', { class: 'qs-table' }, - el('thead', {}, el('tr', {}, ...['qubit', 'T1 µs', 'T2 µs', 'readout', '1q error'].map((t) => el('th', { text: t })))), + el('thead', {}, el('tr', {}, ...[tr('device.th_qubit'), 'T1 µs', 'T2 µs', tr('device.th_readout'), tr('device.th_error_1q')].map((t) => el('th', { text: t })))), el('tbody', {}, ...(view.qubits || []).map((q) => el('tr', {}, el('th', { scope: 'row', text: String(q.qubit) }), ...[q.t1_us, q.t2_us, q.readout_error, q.error_1q].map((x) => el('td', { class: 'num', text: fmt(x) })))))), el('table', { class: 'qs-table' }, - el('thead', {}, el('tr', {}, el('th', { text: 'pair' }), el('th', { text: 'error' }), el('th', { text: 'gate' }))), + el('thead', {}, el('tr', {}, el('th', { text: tr('device.th_pair') }), el('th', { text: tr('device.th_error') }), el('th', { text: tr('device.th_gate') }))), el('tbody', {}, ...(view.pairs || []).map((p) => el('tr', {}, el('th', { scope: 'row', text: `${p.qubits[0]}–${p.qubits[1]}` }), el('td', { class: 'num', text: fmt(p.error) }), el('td', { text: p.gate || '—' })))))); } @@ -133,18 +134,18 @@ function saveKept(k) { // The pane: `engine` answers targets(), target(spec) and targetPlace(qasm, target); // `changed` is called when the target chosen changes. export function devicePane({ engine, changed = () => {}, say = () => {} }) { - const select = el('select', { class: 'qs-input', id: 'qs-target', 'aria-label': 'Device target' }, el('option', { value: '', text: 'None: the engine’s own methods' })); - const colourBy = el('select', { class: 'qs-select', id: 'qs-dev-colour', 'aria-label': 'Colour qubits by' }, + const select = el('select', { class: 'qs-input', id: 'qs-target', 'aria-label': tr('device.target_label') }, el('option', { value: '', text: tr('device.none') })); + const colourBy = el('select', { class: 'qs-select', id: 'qs-dev-colour', 'aria-label': tr('device.colour_by') }, ...Object.entries(COLOUR_BY).map(([k, v]) => el('option', { value: k, text: v.label }))); const fileIn = el('input', { type: 'file', id: 'qs-target-file', accept: '.json,application/json', hidden: true, 'aria-hidden': 'true' }); - const importBtn = el('button', { class: 'qs-btn qs-small', type: 'button', id: 'qs-target-import', text: 'Import…', title: 'A device’s calibration or target snapshot, in any format the engine reads', onclick: () => fileIn.click() }); + const importBtn = el('button', { class: 'qs-btn qs-small', type: 'button', id: 'qs-target-import', text: tr('device.import'), title: tr('device.import_title'), onclick: () => fileIn.click() }); const about = el('p', { class: 'qs-note', id: 'qs-dev-about' }); const map = el('div', { class: 'qs-dev-map', id: 'qs-device-map' }); const keyBox = el('div', { class: 'qs-dev-keybox' }); const placedNote = el('p', { class: 'qs-note', id: 'qs-dev-placed' }); - const node = el('details', { class: 'qs-noise qs-device', id: 'qs-device' }, el('summary', { text: 'Device' }), - el('div', { class: 'qs-row' }, el('label', { class: 'qs-field', for: 'qs-target' }, el('span', { text: 'Run as on' }), select), importBtn, fileIn), - about, el('div', { class: 'qs-dev-ctl' }, el('label', { class: 'qs-field', for: 'qs-dev-colour' }, el('span', { text: 'Colour qubits by' }), colourBy)), + const node = el('details', { class: 'qs-noise qs-device', id: 'qs-device' }, el('summary', { text: tr('device.title') }), + el('div', { class: 'qs-row' }, el('label', { class: 'qs-field', for: 'qs-target' }, el('span', { text: tr('device.run_as_on') }), select), importBtn, fileIn), + about, el('div', { class: 'qs-dev-ctl' }, el('label', { class: 'qs-field', for: 'qs-dev-colour' }, el('span', { text: tr('device.colour_by') }), colourBy)), map, placedNote, keyBox); let views = new Map(); // option value → {view, summary, document} let kept = loadKept(); @@ -170,7 +171,7 @@ export function devicePane({ engine, changed = () => {}, say = () => {} }) { const s = v.summary || {}; const cal = s.calibration || {}; const native = s.native || {}; - about.textContent = `${s.qubits} qubits, ${s.pairs} pairs · compiles to ${native.basis || '—'}${native.exact === false ? ', each gate costed as the device’s own' : ''} · calibration ${cal.id || 'unnamed'}${cal.timestamp ? `, ${cal.timestamp}` : ''} · ${s.source?.note || s.source?.format || ''} · hash ${String(s.hash || '').slice(0, 16)}…`; + about.textContent = tr('device.about', { qubits: s.qubits, pairs: s.pairs, basis: native.basis || '—', costed: native.exact === false ? tr('device.costed_own') : '', calibration: cal.id || tr('device.unnamed'), when: cal.timestamp ? tr('device.when', { timestamp: cal.timestamp }) : '', source: s.source?.note || s.source?.format || '', hash: String(s.hash || '').slice(0, 16) }); } async function load(value) { if (!value || views.has(value)) return; @@ -180,13 +181,13 @@ export function devicePane({ engine, changed = () => {}, say = () => {} }) { async function refresh() { const list = await engine.targets(); const prev = valueOf(); - select.replaceChildren(el('option', { value: '', text: 'None: the engine’s own methods' })); - const built = el('optgroup', { label: 'Built in (synthetic)' }, ...(list.targets || []).map((t) => el('option', { value: t.name, text: `${t.name} · ${t.qubits} qubits` }))); + select.replaceChildren(el('option', { value: '', text: tr('device.none') })); + const built = el('optgroup', { label: tr('device.built_in') }, ...(list.targets || []).map((t) => el('option', { value: t.name, text: tr('device.option', { name: t.name, qubits: t.qubits }) }))); select.append(built); const yours = (list.yours || []).filter((t) => !t.unreadable); - if (yours.length) select.append(el('optgroup', { label: 'Yours (wai-quantum targets)' }, ...yours.map((t) => el('option', { value: `yours:${t.kept_as}`, text: `${t.kept_as} · ${t.qubits} qubits` })))); + if (yours.length) select.append(el('optgroup', { label: tr('device.yours') }, ...yours.map((t) => el('option', { value: `yours:${t.kept_as}`, text: tr('device.option', { name: t.kept_as, qubits: t.qubits }) })))); const names = Object.keys(kept); - if (names.length) select.append(el('optgroup', { label: 'Imported here' }, ...names.map((n) => el('option', { value: `imported:${n}`, text: `${n} · ${kept[n].qubits?.length ?? '?'} qubits` })))); + if (names.length) select.append(el('optgroup', { label: tr('device.imported_here') }, ...names.map((n) => el('option', { value: `imported:${n}`, text: tr('device.option', { name: n, qubits: kept[n].qubits?.length ?? '?' }) })))); if ([...select.options].some((o) => o.value === prev)) select.value = prev; } async function choose() { @@ -212,8 +213,8 @@ export function devicePane({ engine, changed = () => {}, say = () => {} }) { await refresh(); select.value = `imported:${name}`; await choose(); - say(`Imported ${name} (${t.summary.source?.format}): ${t.summary.qubits} qubits, kept in this ${'__TAURI__' in window ? 'app' : 'browser'}.`, 'ok'); - } catch (e) { say(`The snapshot did not read: ${e.message}`, 'bad'); } + say(tr('__TAURI__' in window ? 'device.imported_app' : 'device.imported_browser', { name, format: t.summary.source?.format, qubits: t.summary.qubits }), 'ok'); + } catch (e) { say(tr('device.import_failed', { error: e.message }), 'bad'); } }); return { node, @@ -233,7 +234,7 @@ export function devicePane({ engine, changed = () => {}, say = () => {} }) { showPlacement(p, noise) { placement = p || null; draw(); - placedNote.textContent = p ? `Placed on ${p.layout.join(', ')}${p.final_layout.join() !== p.layout.join() ? `, ending on ${p.final_layout.join(', ')}` : ''}; ${p.swaps} swaps; expected success ${Number(p.expected_success).toFixed(4)} against ${Number(p.fewest_swaps?.expected_success).toFixed(4)} for the fewest swaps${noise ? `; ${noise.channels} noise channels over ${Math.round(noise.duration_ns)} ns` : ''}.` : ''; + placedNote.textContent = p ? tr('device.placed', { layout: p.layout.join(', '), ending: p.final_layout.join() !== p.layout.join() ? tr('device.ending_on', { layout: p.final_layout.join(', ') }) : '', swaps: p.swaps, success: Number(p.expected_success).toFixed(4), fewest: Number(p.fewest_swaps?.expected_success).toFixed(4), noise: noise ? tr('device.placed_noise', { channels: noise.channels, ns: Math.round(noise.duration_ns) }) : '' }) : ''; }, }; } @@ -243,14 +244,14 @@ export function deviceBlock(t, ideal) { const p = t.placement || {}; const n = t.noise || {}; const notes = [...(p.notes || []), ...(n.notes || [])]; - return el('div', { class: 'qs-block', id: 'qs-device-result' }, el('div', { class: 'qs-k', text: 'Device' }), + return el('div', { class: 'qs-block', id: 'qs-device-result' }, el('div', { class: 'qs-k', text: tr('device.title') }), el('table', { class: 'qs-table' }, el('tbody', {}, - el('tr', {}, el('th', { scope: 'row', text: 'target' }), el('td', { id: 'qs-dev-name', text: `${t.name} · calibration ${t.calibration?.id || 'unnamed'}` })), - el('tr', {}, el('th', { scope: 'row', text: 'hash' }), el('td', { class: 'mono', id: 'qs-dev-hash', text: t.hash, title: t.hash_is })), - el('tr', {}, el('th', { scope: 'row', text: 'placed' }), el('td', { text: `on ${(p.layout || []).join(', ')} → ${(p.final_layout || []).join(', ')}; ${p.swaps} swaps; ${p.native_gates} gates in ${p.basis}` })), - el('tr', {}, el('th', { scope: 'row', text: 'expected success' }), el('td', { title: p.expected_success_is, text: `${Number(p.expected_success).toFixed(4)} (fewest swaps: ${Number(p.fewest_swaps?.expected_success).toFixed(4)})` })), - el('tr', {}, el('th', { scope: 'row', text: 'noise' }), el('td', { text: `${n.channels} channels over ${Math.round(n.duration_ns ?? 0)} ns, ${Math.round(n.idle_ns ?? 0)} ns idle` })), - ideal?.computed ? el('tr', {}, el('th', { scope: 'row', text: 'from ideal' }), el('td', { id: 'qs-dev-ideal', text: `total variation distance ${Number(ideal.total_variation_distance).toFixed(4)}, Hellinger fidelity ${Number(ideal.hellinger_fidelity).toFixed(4)}` })) : null)), - el('p', { class: 'qs-note', text: `A model of the device, not a run on it: ${n.model || ''}` }), + el('tr', {}, el('th', { scope: 'row', text: tr('device.row_target') }), el('td', { id: 'qs-dev-name', text: tr('device.name_calibration', { name: t.name, calibration: t.calibration?.id || tr('device.unnamed') }) })), + el('tr', {}, el('th', { scope: 'row', text: tr('device.row_hash') }), el('td', { class: 'mono', id: 'qs-dev-hash', text: t.hash, title: t.hash_is })), + el('tr', {}, el('th', { scope: 'row', text: tr('device.row_placed') }), el('td', { text: tr('device.placed_row', { layout: (p.layout || []).join(', '), final: (p.final_layout || []).join(', '), swaps: p.swaps, gates: p.native_gates, basis: p.basis }) })), + el('tr', {}, el('th', { scope: 'row', text: tr('device.row_success') }), el('td', { title: p.expected_success_is, text: tr('device.success_row', { success: Number(p.expected_success).toFixed(4), fewest: Number(p.fewest_swaps?.expected_success).toFixed(4) }) })), + el('tr', {}, el('th', { scope: 'row', text: tr('device.row_noise') }), el('td', { text: tr('device.noise_row', { channels: n.channels, ns: Math.round(n.duration_ns ?? 0), idle: Math.round(n.idle_ns ?? 0) }) })), + ideal?.computed ? el('tr', {}, el('th', { scope: 'row', text: tr('device.row_ideal') }), el('td', { id: 'qs-dev-ideal', text: tr('device.ideal_row', { tvd: Number(ideal.total_variation_distance).toFixed(4), fidelity: Number(ideal.hellinger_fidelity).toFixed(4) }) })) : null)), + el('p', { class: 'qs-note', text: tr('device.model_note', { model: n.model || '' }) }), ...notes.map((s) => el('p', { class: 'qs-note', text: s }))); } diff --git a/wai/quantum-studio/src/dom.js b/wai/quantum-studio/src/dom.js index ee9d0dffd..2de133b77 100644 --- a/wai/quantum-studio/src/dom.js +++ b/wai/quantum-studio/src/dom.js @@ -1,3 +1,5 @@ +import { t } from './i18n.js'; + // An element with attributes and children: `class`, `text`, `on…` handlers, and // boolean attributes as true. Built as nodes, never from HTML. export const el = (tag, attrs = {}, ...kids) => { @@ -15,11 +17,11 @@ export const el = (tag, attrs = {}, ...kids) => { // The engine's equivalence verdict in words. export function sameness(eq) { - if (!eq) return { text: 'not checked', cls: '' }; + if (!eq) return { text: t('dom.not_checked'), cls: '' }; if (eq.verdict === 'equivalent') { - const how = eq.how === 'tableau' ? 'equal tableaus' : eq.how === 'exact' ? 'on every basis state' : eq.how === 'rewrites' ? 'by checked rewrites' : eq.how === 'random' ? 'on random inputs' : eq.how; - return { text: `same circuit · ${how}`, cls: 'ok', why: eq.why }; + const how = eq.how === 'tableau' ? t('dom.how_tableau') : eq.how === 'exact' ? t('dom.how_exact') : eq.how === 'rewrites' ? t('dom.how_rewrites') : eq.how === 'random' ? t('dom.how_random') : eq.how; + return { text: t('dom.same_circuit', { how }), cls: 'ok', why: eq.why }; } - if (eq.verdict === 'different') return { text: 'a different circuit', cls: 'bad', why: eq.why }; - return { text: 'not checked', cls: '', why: eq.why }; + if (eq.verdict === 'different') return { text: t('dom.different_circuit'), cls: 'bad', why: eq.why }; + return { text: t('dom.not_checked'), cls: '', why: eq.why }; } diff --git a/wai/quantum-studio/src/engine.js b/wai/quantum-studio/src/engine.js index 79a4ac5cb..fcf26f744 100644 --- a/wai/quantum-studio/src/engine.js +++ b/wai/quantum-studio/src/engine.js @@ -76,6 +76,8 @@ export class TabEngine { // 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 }); } + // Error correction: `name` is code, decode or threshold; each answer sealed with this browser's key. + qec(name, args) { return this.call('qec', { name, args }); } // A job's verdict ({request, result} or {request, error}), worked out here; with // an intent, that intent checked against the job's program. verdict(job, intent) { return this.call('verdict', { job, intent }); } @@ -112,6 +114,18 @@ export class TabEngine { // A drawing's edit, made in the program's text; the circuit as svg, latex or a description. visualEdit(qasm, edit) { return this.call('visual_edit', { qasm, edit }); } picture(qasm, format) { return this.call('picture', { qasm, format }); } + // The answer view layer: `name` is parse, snapshot, value, evaluate, verify, marker + // or prompt; a value's calls run on the engine here, a seal signed with this browser's key. + // A refusal comes back as data ({ok: false, error, code}), not as a failure. + answer(name, args) { return this.call('answer', { name, args }); } + // Correctness records: `name` is record or corpus. The Studio keeps the records. + telemetry(name, args) { return this.call('telemetry', { name, args }); } + // The circuits course (engine::course): the course and a lesson in a locale, + // a program checked against a lesson's exercise, and a hint with its pointer. + course(locale) { return this.call('course', { locale }); } + courseLesson(lesson, locale) { return this.call('course_lesson', { lesson, locale }); } + courseCheck(lesson, qasm, locale) { return this.call('course_check', { lesson, qasm, locale }); } + courseHint(lesson, qasm, level, locale) { return this.call('course_hint', { lesson, qasm, level, locale }); } async signer() { return (await this.call('signer')).did; } cancel() { this.worker?.terminate(); @@ -149,6 +163,8 @@ export class ServiceEngine { } } async methods() { return this.json(await fetch(`${this.base}/v1/methods`)); } + // Error correction on the service, sealed with its key. + async qec(name, args) { return this.json(await fetch(`${this.base}/v1/qec/${encodeURIComponent(name)}`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(args ?? {}) })); } async describe() { return this.json(await fetch(`${this.base}/v1`)); } cancel() { this.stopped = true; @@ -189,6 +205,8 @@ export class DesktopEngine { // A resource estimate, sealed with this user's key, and its check and re-run here. estimate(request) { return this.call('estimate', { request: JSON.stringify(request) }); } estimateVerify(body) { return this.call('estimate_verify', { body: JSON.stringify(body) }); } + // Error correction, sealed with this user's key. + qec(name, args) { return this.call('qec', { op: name, args: JSON.stringify(args ?? {}) }); } verdict(job, intent) { return this.call('verdict', { job: JSON.stringify(job), intent: intent ? JSON.stringify(intent) : '' }); } repair(qasm, options) { return this.call('repair', { qasm, options: JSON.stringify(options ?? {}) }); } instantiate(qasm, inputs) { return this.call('instantiate', { qasm, inputs: JSON.stringify(inputs ?? {}) }); } @@ -209,6 +227,14 @@ export class DesktopEngine { debug(name, args) { return this.call('debug', { name, args: JSON.stringify(args) }); } visualEdit(qasm, edit) { return this.call('visual_edit', { qasm, edit: JSON.stringify(edit) }); } picture(qasm, format) { return this.call('picture', { qasm, format }); } + // The answer view layer and correctness records, as the tab's engine gives them; + // a refusal comes back as data. + async answer(name, args) { return JSON.parse(await this.invoke('answer', { op: name, args: JSON.stringify(args ?? {}) })); } + async telemetry(name, args) { return JSON.parse(await this.invoke('telemetry', { op: name, args: JSON.stringify(args ?? {}) })); } + course(locale) { return this.call('course', { locale }); } + courseLesson(lesson, locale) { return this.call('course_lesson', { lesson, locale }); } + courseCheck(lesson, qasm, locale) { return this.call('course_check', { lesson, qasm, locale }); } + courseHint(lesson, qasm, level, locale) { return this.call('course_hint', { lesson, qasm, level, locale }); } async signer() { return (await this.call('signer', {})).did; } saveJob(name, job) { return this.call('save_job', { name, job: JSON.stringify(job, null, 2) }); } saveProgram(name, text) { return this.call('save_program', { name, text }); } diff --git a/wai/quantum-studio/src/estimate-view.js b/wai/quantum-studio/src/estimate-view.js index 646744236..dc3c1ea9e 100644 --- a/wai/quantum-studio/src/estimate-view.js +++ b/wai/quantum-studio/src/estimate-view.js @@ -7,15 +7,16 @@ // estimate comes sealed with this browser's key (or the desktop user's), and the // seals and a re-run are checked here. import { el } from './dom.js'; +import { t } from './i18n.js'; const NS = 'http://www.w3.org/2000/svg'; const QUBITS = [ - ['ns-e4', 'ns-e4 · 50 ns gates, 1e-4'], - ['ns-e3', 'ns-e3 · 50 ns gates, 1e-3'], - ['us-e4', 'us-e4 · 100 µs gates, 1e-4'], - ['us-e3', 'us-e3 · 100 µs gates, 1e-3'], - ['meas-e4', 'meas-e4 · measurement-based, 1e-4'], - ['meas-e6', 'meas-e6 · measurement-based, 1e-6'], + ['ns-e4', t('estimate_view.qubit_ns_e4')], + ['ns-e3', t('estimate_view.qubit_ns_e3')], + ['us-e4', t('estimate_view.qubit_us_e4')], + ['us-e3', t('estimate_view.qubit_us_e3')], + ['meas-e4', t('estimate_view.qubit_meas_e4')], + ['meas-e6', t('estimate_view.qubit_meas_e6')], ]; function node(tag, attrs = {}, ...kids) { @@ -43,8 +44,8 @@ export function duration(s) { if (x < 60) return `${x.toFixed(2)} s`; if (x < 3600) return `${(x / 60).toFixed(1)} min`; if (x < 86400) return `${(x / 3600).toFixed(1)} h`; - if (x < 3.15576e7) return `${(x / 86400).toFixed(1)} days`; - return `${(x / 3.15576e7).toFixed(1)} years`; + if (x < 3.15576e7) return t('estimate_view.days', { n: (x / 86400).toFixed(1) }); + return t('estimate_view.years', { n: (x / 3.15576e7).toFixed(1) }); } // A decade's tick: 1 µs, 100 ms, 10 s, 17 min, 28 h, 1.2 days. @@ -54,8 +55,8 @@ function tick(s) { if (s < 1000) return `${Math.round(s)} s`; if (s < 1e5) return `${Math.round(s / 60)} min`; if (s < 1e6) return `${Math.round(s / 3600)} h`; - if (s < 1e8) return `${Math.round(s / 86400)} days`; - return `${(s / 3.15576e7).toPrecision(2)} years`; + if (s < 1e8) return t('estimate_view.days', { n: Math.round(s / 86400) }); + return t('estimate_view.years', { n: (s / 3.15576e7).toPrecision(2) }); } const exp = (x) => (x == null ? '—' : x === 0 ? '0' : Number(x).toExponential(1)); @@ -76,7 +77,7 @@ export function frontierSvg(points, headline, onPoint) { const [x0, x1] = span(xs), [y0, y1] = span(ys); const X = (v) => L + ((v - x0) / (x1 - x0)) * (W - L - R); const Y = (v) => H - B - ((v - y0) / (y1 - y0)) * (H - T - B); - const svg = node('svg', { viewBox: `0 0 ${W} ${H}`, class: 'qs-frontier', role: 'img', 'aria-label': `The frontier: ${points.length} estimate${points.length === 1 ? '' : 's'}, runtime against physical qubits on log scales` }); + const svg = node('svg', { viewBox: `0 0 ${W} ${H}`, class: 'qs-frontier', role: 'img', 'aria-label': points.length === 1 ? t('estimate_view.frontier_label_one', { n: points.length }) : t('estimate_view.frontier_label_other', { n: points.length }) }); const grid = node('g', { class: 'qs-frontier-grid' }); const decades = (lo, hi) => { const out = []; for (let d = Math.ceil(lo); d <= Math.floor(hi); d++) out.push(d); return out; }; for (const d of decades(x0, x1)) { @@ -88,21 +89,21 @@ export function frontierSvg(points, headline, onPoint) { grid.append(node('text', { x: L - 8, y: Y(d), 'text-anchor': 'end', 'dominant-baseline': 'central', class: 'qs-frontier-tick' }, compact(10 ** d))); } svg.append(grid); - svg.append(node('text', { x: (L + W - R) / 2, y: H - 6, 'text-anchor': 'middle', class: 'qs-frontier-axis' }, 'runtime')); - svg.append(node('text', { x: 14, y: (T + H - B) / 2, 'text-anchor': 'middle', transform: `rotate(-90 14 ${(T + H - B) / 2})`, class: 'qs-frontier-axis' }, 'physical qubits')); + svg.append(node('text', { x: (L + W - R) / 2, y: H - 6, 'text-anchor': 'middle', class: 'qs-frontier-axis' }, t('estimate_view.runtime'))); + svg.append(node('text', { x: 14, y: (T + H - B) / 2, 'text-anchor': 'middle', transform: `rotate(-90 14 ${(T + H - B) / 2})`, class: 'qs-frontier-axis' }, t('estimate_view.physical_qubits'))); if (points.length > 1) svg.append(node('polyline', { class: 'qs-frontier-line', points: points.map((_, i) => `${X(xs[i])},${Y(ys[i])}`).join(' ') })); points.forEach((p, i) => { - const g = node('g', { class: `qs-frontier-point${i === headline ? ' head' : ''}`, tabindex: '0', 'data-i': i, role: 'button', 'aria-label': `slowdown ${p.slowdown}: ${p.physical_qubits} physical qubits, ${duration(p.runtime_s)}, distance ${p.distance}, ${p.factories} factories` }, + const g = node('g', { class: `qs-frontier-point${i === headline ? ' head' : ''}`, tabindex: '0', 'data-i': i, role: 'button', 'aria-label': t('estimate_view.point_label', { slowdown: p.slowdown, qubits: p.physical_qubits, runtime: duration(p.runtime_s), distance: p.distance, factories: p.factories }) }, node('circle', { cx: X(xs[i]), cy: Y(ys[i]), r: 12, class: 'qs-frontier-hit' }), node('circle', { cx: X(xs[i]), cy: Y(ys[i]), r: i === headline ? 6 : 4, class: 'qs-frontier-dot' }), - node('title', {}, `slowdown ${p.slowdown}: ${compact(p.physical_qubits)} physical qubits, ${duration(p.runtime_s)}`)); + node('title', {}, t('estimate_view.point_title', { slowdown: p.slowdown, qubits: compact(p.physical_qubits), runtime: duration(p.runtime_s) }))); for (const ev of ['pointerenter', 'focus']) g.addEventListener(ev, () => onPoint(i)); svg.append(g); }); const h = points[headline]; if (h) { const right = X(xs[headline]) > (L + W - R) / 2; - svg.append(node('text', { x: X(xs[headline]) + (right ? -10 : 10), y: Y(ys[headline]) - 10, 'text-anchor': right ? 'end' : 'start', class: 'qs-frontier-label' }, 'the estimate')); + svg.append(node('text', { x: X(xs[headline]) + (right ? -10 : 10), y: Y(ys[headline]) - 10, 'text-anchor': right ? 'end' : 'start', class: 'qs-frontier-label' }, t('estimate_view.the_estimate'))); } return svg; } @@ -110,15 +111,15 @@ export function frontierSvg(points, headline, onPoint) { // `engine` answers estimate(request), estimateVerify(body) and instance(qasm); // `program()` is the editor's text. export function estimatePanel({ engine, program }) { - const qubit = el('select', { class: 'qs-input qs-small-input', id: 'qs-estimate-qubit', 'aria-label': 'Physical qubit', title: 'The physical qubit the machine is built of' }, - ...QUBITS.map(([v, t]) => el('option', { value: v, text: t }))); - const budget = el('input', { class: 'qs-input qs-small-input', id: 'qs-estimate-budget', type: 'text', value: '1e-3', inputmode: 'decimal', spellcheck: 'false', 'aria-label': 'Error budget', title: 'The probability the run may fail, shared by logical errors, distillation and the rotations’ synthesis' }); - const calibration = el('select', { class: 'qs-input qs-small-input', id: 'qs-estimate-calibration', 'aria-label': 'Code model', title: 'The gate-based surface code’s model: fitted to this engine’s exact-matching or union-find decoder, or the planar model’s assumed one' }, - el('option', { value: 'exact_matching', text: 'fit: exact matching' }), el('option', { value: 'union_find', text: 'fit: union-find' }), el('option', { value: 'assumed', text: 'assumed model' })); - const go = el('button', { class: 'qs-btn qs-small', id: 'qs-estimate', type: 'button', text: 'Estimate', title: 'What a fault-tolerant machine would need to run this program, each estimate signed' }); + const qubit = el('select', { class: 'qs-input qs-small-input', id: 'qs-estimate-qubit', 'aria-label': t('estimate_view.qubit_label'), title: t('estimate_view.qubit_title') }, + ...QUBITS.map(([v, label]) => el('option', { value: v, text: label }))); + const budget = el('input', { class: 'qs-input qs-small-input', id: 'qs-estimate-budget', type: 'text', value: '1e-3', inputmode: 'decimal', spellcheck: 'false', 'aria-label': t('estimate_view.budget_aria'), title: t('estimate_view.budget_title') }); + const calibration = el('select', { class: 'qs-input qs-small-input', id: 'qs-estimate-calibration', 'aria-label': t('estimate_view.model_label'), title: t('estimate_view.model_title') }, + el('option', { value: 'exact_matching', text: t('estimate_view.fit_exact_matching') }), el('option', { value: 'union_find', text: t('estimate_view.fit_union_find') }), el('option', { value: 'assumed', text: t('estimate_view.assumed_model') })); + const go = el('button', { class: 'qs-btn qs-small', id: 'qs-estimate', type: 'button', text: t('estimate_view.estimate'), title: t('estimate_view.estimate_title') }); const body = el('div', { class: 'qs-shortcuts-body', id: 'qs-estimate-body' }); const node_ = el('div', { class: 'qs-shortcuts qs-estimate', id: 'qs-estimate-panel' }, - el('div', { class: 'qs-shortcuts-head' }, el('span', { class: 'qs-k', text: 'Estimate' }), el('span', { class: 'qs-grow' }), qubit, el('label', { class: 'qs-inline', for: 'qs-estimate-budget' }, el('span', { text: 'budget' }), budget), calibration, go), + el('div', { class: 'qs-shortcuts-head' }, el('span', { class: 'qs-k', text: t('estimate_view.heading') }), el('span', { class: 'qs-grow' }), qubit, el('label', { class: 'qs-inline', for: 'qs-estimate-budget' }, el('span', { text: t('estimate_view.budget') }), budget), calibration, go), body); let estimatedFor = null; let busy = false; @@ -131,45 +132,48 @@ export function estimatePanel({ engine, program }) { const head = r.frontier.findIndex((p) => p.headline); const qname = r.options.qubit.name; const share = (100 * e.factory_fraction).toFixed(1); - const anc = c.ancillas ? ` (${c.ancillas} ancilla${c.ancillas === 1 ? '' : 's'})` : ''; - const depths = c.t_depth != null ? `; T depth ${compact(c.t_depth)}, depth ${compact(c.depth)}` : ''; - const counts = `${compact(c.logical_qubits)} logical qubits${anc}, ${compact(c.measurements)} measurements, ${compact(c.t_gates)} T, ${compact(c.toffolis)} Toffolis, ${compact(c.rotations)} rotations (${compact(c.rotation_t)} T in their words)${depths}${c.bound === 'upper' ? '; the program branches, so these bound every run' : ''}.`; - const synth = eps.each != null ? `Each rotation within ${exp(eps.each)} (the synthesis share ${exp(eps.synthesis)} of the budget ${exp(eps.budget)}), ${eps.t_per_rotation.min === eps.t_per_rotation.max ? eps.t_per_rotation.max : `${eps.t_per_rotation.min}–${eps.t_per_rotation.max}`} T apiece.` : `Budget ${exp(eps.budget)}; nothing to synthesise.`; + const anc = c.ancillas ? (c.ancillas === 1 ? t('estimate_view.ancillas_one', { n: c.ancillas }) : t('estimate_view.ancillas_other', { n: c.ancillas })) : ''; + const depths = c.t_depth != null ? t('estimate_view.depths', { t_depth: compact(c.t_depth), depth: compact(c.depth) }) : ''; + const counts = t('estimate_view.counts', { logical: compact(c.logical_qubits), ancillas: anc, measurements: compact(c.measurements), t: compact(c.t_gates), toffolis: compact(c.toffolis), rotations: compact(c.rotations), rotation_t: compact(c.rotation_t), depths, bound: c.bound === 'upper' ? t('estimate_view.bound_upper') : '' }); + const synth = eps.each != null ? t('estimate_view.synth', { each: exp(eps.each), synthesis: exp(eps.synthesis), budget: exp(eps.budget), per: eps.t_per_rotation.min === eps.t_per_rotation.max ? eps.t_per_rotation.max : `${eps.t_per_rotation.min}–${eps.t_per_rotation.max}` }) : t('estimate_view.synth_none', { budget: exp(eps.budget) }); const cal = r.calibration; - const calText = cal.applies && cal.prefactor != null && cal.label ? `Code distance from P(d) = a·(p/p*)^((d+1)/2) with a = ${cal.prefactor.toFixed(4)}, p* = ${cal.threshold.toFixed(5)}, fitted to this engine’s ${cal.label} decoder${cal.extrapolated ? ', extrapolated past the measured p = 0.002–0.005' : ''}.` : cal.decoder; + const calText = cal.applies && cal.prefactor != null && cal.label ? t('estimate_view.calibration', { prefactor: cal.prefactor.toFixed(4), threshold: cal.threshold.toFixed(5), label: cal.label, extrapolated: cal.extrapolated ? t('estimate_view.extrapolated') : '' }) : cal.decoder; + const factories = (n) => (n === 1 ? t('estimate_view.factories_one', { n }) : t('estimate_view.factories_other', { n })); const readout = el('p', { class: 'qs-note', id: 'qs-estimate-point' }); const show = (i) => { const p = r.frontier[i]; - readout.textContent = `${p.headline ? 'The estimate' : `Slowed ×${p.slowdown}`}: ${compact(p.physical_qubits)} physical qubits (${p.factories} factor${p.factories === 1 ? 'y' : 'ies'}), ${duration(p.runtime_s)}, code distance ${p.distance}.`; + readout.textContent = t('estimate_view.readout', { which: p.headline ? t('estimate_view.the_estimate_cap') : t('estimate_view.slowed', { slowdown: p.slowdown }), qubits: compact(p.physical_qubits), factories: factories(p.factories), runtime: duration(p.runtime_s), distance: p.distance }); for (const tr of table.querySelectorAll('tbody tr')) tr.classList.toggle('on', Number(tr.dataset.i) === i); }; const table = el('table', { class: 'qs-passes', id: 'qs-frontier-table' }, - el('thead', {}, el('tr', {}, ...['slowdown', 'd', 'factories', 'physical qubits', 'runtime'].map((h, k) => el('th', { class: k ? 'num' : '', text: h })))), + el('thead', {}, el('tr', {}, ...[t('estimate_view.col_slowdown'), t('estimate_view.col_d'), t('estimate_view.col_factories'), t('estimate_view.physical_qubits'), t('estimate_view.runtime')].map((h, k) => el('th', { class: k ? 'num' : '', text: h })))), el('tbody', {}, ...r.frontier.map((p, i) => el('tr', { 'data-i': i, class: p.headline ? 'head' : '' }, - el('th', { scope: 'row', text: `×${p.slowdown}${p.headline ? ' · the estimate' : ''}` }), + el('th', { scope: 'row', text: `×${p.slowdown}${p.headline ? ` · ${t('estimate_view.the_estimate')}` : ''}` }), el('td', { class: 'num', text: String(p.distance) }), el('td', { class: 'num', text: String(p.factories) }), el('td', { class: 'num', text: p.physical_qubits.toLocaleString('en-US') }), el('td', { class: 'num', text: duration(p.runtime_s) }))))); const figure = el('div', { class: 'qs-diagram qs-frontier-figure', id: 'qs-frontier' }, frontierSvg(r.frontier, head, show)); - const verified = check.valid === true ? `${check.estimates} estimate${check.estimates === 1 ? '' : 's'} sealed with ${check.signer ?? 'this key'}: the seals verify, and the request re-run here gives every figure.` : `The seals DO NOT VERIFY here (signed ${check.signed}, receipts ${check.receipts}, reproduced ${check.reproduced}).`; + const signer = check.signer ?? t('estimate_view.this_key'); + const verified = check.valid === true ? (check.estimates === 1 ? t('estimate_view.verified_one', { n: check.estimates, signer }) : t('estimate_view.verified_other', { n: check.estimates, signer })) : t('estimate_view.not_verified', { signed: check.signed, receipts: check.receipts, reproduced: check.reproduced }); + const tFactories = e.factories === 1 ? t('estimate_view.t_factories_one', { n: e.factories }) : t('estimate_view.t_factories_other', { n: e.factories }); body.replaceChildren( - el('p', { class: 'qs-why', id: 'qs-estimate-summary', text: `On ${qname}: ${e.physical_qubits.toLocaleString('en-US')} physical qubits (${e.factories} T factor${e.factories === 1 ? 'y' : 'ies'}, ${share}% of them) for ${duration(e.runtime_s)}, at code distance ${e.distance} (${e.code}).` }), + el('p', { class: 'qs-why', id: 'qs-estimate-summary', text: t('estimate_view.summary', { qubit: qname, qubits: e.physical_qubits.toLocaleString('en-US'), factories: tFactories, share, runtime: duration(e.runtime_s), distance: e.distance, code: e.code }) }), el('p', { class: 'qs-note', id: 'qs-estimate-counts', text: counts }), el('p', { class: 'qs-note', id: 'qs-estimate-eps', text: `${synth} ${calText}` }), figure, readout, el('div', { class: 'qs-scroll' }, table), el('p', { class: 'qs-note', id: 'qs-estimate-receipts', text: verified }), - el('p', { class: 'qs-note', text: 'An estimate of a model: the planar model’s assumptions (uniform independent noise, free transport of T states, smooth T consumption) hold for every figure here. Slowing the program needs fewer factories; the frontier keeps each estimate no other beats on both.' })); + el('p', { class: 'qs-note', text: t('estimate_view.model_note') })); show(head); } async function estimate() { if (busy) return; const b = Number(budget.value); - if (!(b > 0 && b < 1)) { body.replaceChildren(el('p', { class: 'qs-note', text: 'The budget is a probability between 0 and 1, such as 1e-3.' })); return; } + if (!(b > 0 && b < 1)) { body.replaceChildren(el('p', { class: 'qs-note', text: t('estimate_view.bad_budget') })); return; } busy = true; go.disabled = true; const src = program(); node_.classList.remove('stale'); - body.replaceChildren(el('p', { class: 'qs-note', text: 'Estimating: counting the program, synthesising each rotation, searching the factories…' })); + body.replaceChildren(el('p', { class: 'qs-note', text: t('estimate_view.estimating') })); try { const qasm = engine.instance ? await engine.instance(src) : src; const job = await engine.estimate({ qasm, budget: b, qubit: qubit.value, calibration: calibration.value, max_slowdown: 256 }); @@ -186,7 +190,7 @@ export function estimatePanel({ engine, program }) { const old = src !== estimatedFor; node_.classList.toggle('stale', old); body.querySelector('.qs-stale-note')?.remove(); - if (old) body.prepend(el('p', { class: 'qs-note qs-stale-note', text: 'For an earlier version of the program: estimate again.' })); + if (old) body.prepend(el('p', { class: 'qs-note qs-stale-note', text: t('estimate_view.stale') })); } go.addEventListener('click', estimate); return { node: node_, estimate, stale }; diff --git a/wai/quantum-studio/src/examples.js b/wai/quantum-studio/src/examples.js index 2da08e704..d9ecc728f 100644 --- a/wai/quantum-studio/src/examples.js +++ b/wai/quantum-studio/src/examples.js @@ -1,29 +1,31 @@ // Worked examples, each a whole job and what it shows. +import { t } from './i18n.js'; + const H = 'OPENQASM 3.0;\ninclude "stdgates.inc";\n'; export const EXAMPLES = [ { id: 'bell', - title: 'Bell pair', - shows: 'Two qubits entangled: only 00 and 11 come up, and ⟨Z0 Z1⟩ is exactly 1.', + title: t('examples.bell_title'), + shows: t('examples.bell_shows'), job: { qasm: `${H}qubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n`, shots: 1000, observables: ['Z0 Z1', 'X0 X1'] }, }, { id: 'ghz', - title: 'GHZ state, 40 qubits', - shows: 'A Clifford circuit past any state vector, run exactly on the stabilizer tableau: all zeros or all ones.', + title: t('examples.ghz_title'), + shows: t('examples.ghz_shows'), job: { qasm: `${H}qubit[40] q;\nbit[40] c;\nh q[0];\n${Array.from({ length: 39 }, (_, i) => `cx q[${i}], q[${i + 1}];`).join('\n')}\nc = measure q;\n`, shots: 1000 }, }, { id: 'rotation', - title: 'A rotation', - shows: 'Any angle runs on the double-precision state vector: ⟨Z0⟩ = cos 0.3 and ⟨Y0⟩ = −sin 0.3.', + title: t('examples.rotation_title'), + shows: t('examples.rotation_shows'), job: { qasm: `${H}qubit[1] q;\nrx(0.3) q[0];\n`, observables: ['Z0', 'Y0'], amplitudes: ['0', '1'] }, }, { id: 'qft', - title: 'Quantum Fourier transform', - shows: 'The Fourier transform of a basis state on three qubits: every outcome equally likely, the information in the amplitudes\' phases.', + title: t('examples.qft_title'), + shows: t('examples.qft_shows'), job: { qasm: `${H}qubit[3] q;\nx q[2];\nh q[0];\ncp(pi/2) q[1], q[0];\ncp(pi/4) q[2], q[0];\nh q[1];\ncp(pi/2) q[2], q[1];\nh q[2];\nswap q[0], q[2];\n`, amplitudes: ['000', '001', '010', '011'], @@ -32,8 +34,8 @@ export const EXAMPLES = [ }, { id: 'teleport', - title: 'Teleportation', - shows: 'A state moved from qubit 0 to qubit 2 by measurement and correction: ⟨Z2⟩ = cos 0.7, ⟨X2⟩ = sin 0.7.', + title: t('examples.teleport_title'), + shows: t('examples.teleport_shows'), job: { qasm: `${H}qubit[3] q;\nbit[3] c;\nry(0.7) q[0];\nh q[1];\ncx q[1], q[2];\ncx q[0], q[1];\nh q[0];\nc[0] = measure q[0];\nc[1] = measure q[1];\nif (c[1]) x q[2];\nif (c[0]) z q[2];\nc[2] = measure q[2];\n`, observables: ['Z2', 'X2'], @@ -42,8 +44,8 @@ export const EXAMPLES = [ }, { id: 'grover', - title: 'Grover search, 3 qubits', - shows: 'Two rounds of amplitude amplification find the marked item 101 with probability 0.945.', + title: t('examples.grover_title'), + shows: t('examples.grover_shows'), job: { qasm: `${H}qubit[3] q;\nbit[3] c;\nh q;\n${'x q[1];\nh q[2];\nccx q[0], q[1], q[2];\nh q[2];\nx q[1];\nh q;\nx q;\nh q[2];\nccx q[0], q[1], q[2];\nh q[2];\nx q;\nh q;\n'.repeat(2)}c = measure q;\n`, shots: 2000, @@ -51,8 +53,8 @@ export const EXAMPLES = [ }, { id: 'noisy-bell', - title: 'A noisy Bell pair', - shows: 'The same pair on a device whose gates go wrong at random: 01 and 10 appear, and ⟨Z0 Z1⟩ falls below 1.', + title: t('examples.noisy_bell_title'), + shows: t('examples.noisy_bell_shows'), job: { qasm: `${H}qubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n`, shots: 2000, observables: ['Z0 Z1'], noise: { depolarizing_1q: 0.01, depolarizing_2q: 0.03, readout: 0.02 } }, }, ]; diff --git a/wai/quantum-studio/src/flags.js b/wai/quantum-studio/src/flags.js index 7643cdb75..4ec58df06 100644 --- a/wai/quantum-studio/src/flags.js +++ b/wai/quantum-studio/src/flags.js @@ -4,20 +4,21 @@ // leave it, so each can be compared against a control; a feature it does not // name stays the reader's. import { el } from './dom.js'; +import { t } from './i18n.js'; const SAVED = 'wai.quantum-studio.flags'; export const FLAGS = [ - ['critique', 'the engine’s critique on the live diagram'], - ['assistant_asks', 'the assistant asks before it proposes, and speaks only when asked'], - ['alternatives', 'Alternatives: improve and explore, on the engine’s axes'], - ['predict_first', 'predict before a run'], - ['workbench', 'the Workbench'], - ['rules', 'rules for proposals'], - ['shortcuts', 'the classical-shortcut lens'], - ['novelty', 'what is new: labels and Found'], - ['taste', 'your taste'], - ['rewrites', 'the rewrite lens'], - ['convergence', 'your own way'], + ['critique', t('flags.critique')], + ['assistant_asks', t('flags.assistant_asks')], + ['alternatives', t('flags.alternatives')], + ['predict_first', t('flags.predict_first')], + ['workbench', t('flags.workbench')], + ['rules', t('flags.rules')], + ['shortcuts', t('flags.shortcuts')], + ['novelty', t('flags.novelty')], + ['taste', t('flags.taste')], + ['rewrites', t('flags.rewrites')], + ['convergence', t('flags.convergence')], ]; // `root` is the Studio's element: each feature turned off sets `qs-off-NAME` on it. @@ -54,7 +55,7 @@ export function flagsView(flags) { const list = el('ul', { class: 'qs-flags', id: 'qs-flags' }); const note = el('p', { class: 'qs-note', id: 'qs-flags-note' }); function show() { - note.textContent = flags.locked() ? 'The study you take part in sets the ones it names, so that each can be compared against a control; leave the study to change those.' : 'Turn any feature off; the Studio keeps your choice in this browser.'; + note.textContent = flags.locked() ? t('flags.note_locked') : t('flags.note'); list.replaceChildren(...FLAGS.map(([f, what]) => { const box = el('input', { type: 'checkbox', id: `qs-flag-${f}`, checked: flags.on(f), disabled: flags.held(f) }); box.addEventListener('change', () => flags.set(f, box.checked)); @@ -63,5 +64,5 @@ export function flagsView(flags) { } flags.onChange(show); show(); - return el('details', { class: 'qs-study-flags', id: 'qs-features' }, el('summary', { text: 'Features' }), note, list); + return el('details', { class: 'qs-study-flags', id: 'qs-features' }, el('summary', { text: t('flags.title') }), note, list); } diff --git a/wai/quantum-studio/src/gallery.js b/wai/quantum-studio/src/gallery.js index 469e3021c..ca31abcd7 100644 --- a/wai/quantum-studio/src/gallery.js +++ b/wai/quantum-studio/src/gallery.js @@ -10,6 +10,7 @@ // construction it is, a known result reached by a different route (flagged in // Found), or how far it is from the nearest known construction or the draft. import { el, sameness } from './dom.js'; +import { t } from './i18n.js'; import { chips } from './critique-view.js'; import { noveltyLabel } from './novelty.js'; import { declared } from './generators.js'; @@ -50,18 +51,18 @@ export function atlas(draft, cands, onPick) { const ymax = Math.max(1, ...pts.map((p) => p.depth)) * 1.15; const sx = (v) => L + (v / xmax) * (W - L - R), sy = (v) => H - B - (v / ymax) * (H - B - T); const ticks = (max) => { const step = Math.max(1, Math.ceil(max / 4)); const out = []; for (let v = 0; v <= max; v += step) out.push(v); return out; }; - const g = svg('svg', { class: 'qs-atlas', id: 'qs-atlas', viewBox: `0 0 ${W} ${H}`, role: 'img', 'aria-label': `The draft and ${cands.length} alternatives by two-qubit gates and depth` }); + const g = svg('svg', { class: 'qs-atlas', id: 'qs-atlas', viewBox: `0 0 ${W} ${H}`, role: 'img', 'aria-label': t('gallery.atlas_label', { n: cands.length }) }); for (const v of ticks(xmax)) g.append(svg('line', { x1: sx(v), y1: T, x2: sx(v), y2: H - B, class: 'qs-gridline' }), svg('text', { x: sx(v), y: H - B + 13, class: 'qs-tick', 'text-anchor': 'middle' }, String(v))); for (const v of ticks(ymax)) g.append(svg('line', { x1: L, y1: sy(v), x2: W - R, y2: sy(v), class: 'qs-gridline' }), svg('text', { x: L - 6, y: sy(v) + 3, class: 'qs-tick', 'text-anchor': 'end' }, String(v))); - g.append(svg('text', { x: W - R, y: H - 2, class: 'qs-axis', 'text-anchor': 'end' }, 'two-qubit gates →'), - svg('text', { x: L - 6, y: 10, class: 'qs-axis', 'text-anchor': 'start' }, '↑ depth')); - const d = svg('g', { class: 'qs-pt qs-pt-draft' }, svg('title', {}, `the draft: ${draft.two_qubit} two-qubit gates, depth ${draft.depth}`), + g.append(svg('text', { x: W - R, y: H - 2, class: 'qs-axis', 'text-anchor': 'end' }, t('gallery.axis_two_qubit')), + svg('text', { x: L - 6, y: 10, class: 'qs-axis', 'text-anchor': 'start' }, t('gallery.axis_depth'))); + const d = svg('g', { class: 'qs-pt qs-pt-draft' }, svg('title', {}, t('gallery.draft_point', { two_qubit: draft.two_qubit, depth: draft.depth })), svg('circle', { cx: sx(draft.two_qubit), cy: sy(draft.depth), r: 6, class: 'qs-ring' })); g.append(d); cands.forEach((c, i) => { const x = sx(c.measures.two_qubit), y = sy(c.measures.depth); - const p = svg('g', { class: 'qs-pt', tabindex: '0', role: 'button', 'data-i': i, 'aria-label': `alternative ${i + 1}: ${c.measures.two_qubit} two-qubit gates, depth ${c.measures.depth}` }, - svg('title', {}, `${i + 1}. ${c.label}: ${c.measures.two_qubit} two-qubit gates, depth ${c.measures.depth}`), + const p = svg('g', { class: 'qs-pt', tabindex: '0', role: 'button', 'data-i': i, 'aria-label': t('gallery.point_label', { n: i + 1, two_qubit: c.measures.two_qubit, depth: c.measures.depth }) }, + svg('title', {}, t('gallery.point_title', { n: i + 1, label: c.label, two_qubit: c.measures.two_qubit, depth: c.measures.depth })), svg('circle', { cx: x, cy: y, r: 12, class: 'qs-hit' }), svg('circle', { cx: x, cy: y, r: 5, class: 'qs-dot' }), svg('text', { x: x + 8, y: y - 7, class: 'qs-tick qs-pt-n' }, String(i + 1))); @@ -87,13 +88,13 @@ export function atlas(draft, cands, onPick) { // `features.on(name)` says whether a feature is on (flags.js); `onChoice` hears // which of Improve's candidates the reader applied, and the engine's best of them. export function galleryPane({ engine, draft, model, apply, check = null, found = null, taste = null, paused = () => false, features = { on: () => true }, onChoice = null }) { - const intent = el('input', { class: 'qs-input', id: 'qs-explore-aim', placeholder: 'Toward… fewer qubits, another entangler, more symmetry', autocomplete: 'off', 'aria-label': 'What Explore should aim for' }); - const improveBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-improve', type: 'button', text: 'Improve', title: 'The same circuit, cheaper: each checked equivalent to the draft' }); - const exploreBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-explore', type: 'button', text: 'Explore', title: 'Different circuits toward the aim, from the model, each measured by the engine' }); - const note = el('p', { class: 'qs-note', id: 'qs-alt-note', text: 'Improve: the same circuit, cheaper, each checked equivalent. Explore: different circuits toward an aim, from the model, each measured by the engine. Nothing changes until you apply one.' }); + const intent = el('input', { class: 'qs-input', id: 'qs-explore-aim', placeholder: t('gallery.aim_placeholder'), autocomplete: 'off', 'aria-label': t('gallery.aim_label') }); + const improveBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-improve', type: 'button', text: t('gallery.improve'), title: t('gallery.improve_title') }); + const exploreBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-explore', type: 'button', text: t('gallery.explore'), title: t('gallery.explore_title') }); + const note = el('p', { class: 'qs-note', id: 'qs-alt-note', text: t('gallery.intro') }); const body = el('div', { class: 'qs-alt-body', id: 'qs-alt-body' }); const node = el('section', { class: 'qs-pane qs-alt', 'aria-labelledby': 'qs-alt-h' }, - el('div', { class: 'qs-head' }, el('h2', { id: 'qs-alt-h', text: 'Alternatives' }), el('span', { class: 'qs-grow' }), improveBtn, exploreBtn), + el('div', { class: 'qs-head' }, el('h2', { id: 'qs-alt-h', text: t('gallery.title') }), el('span', { class: 'qs-grow' }), improveBtn, exploreBtn), el('div', { class: 'qs-alt-aim' }, intent), note, body); let busy = false; @@ -101,11 +102,11 @@ export function galleryPane({ engine, draft, model, apply, check = null, found = const chainView = (ch) => { const n = ch.a_steps.length + ch.b_steps.length; return el('details', { class: 'qs-chain' }, - el('summary', { text: n ? `How: ${n} rewrite${n === 1 ? '' : 's'}, each checked` : 'How: the same gates, in another order' }), + el('summary', { text: n ? (n === 1 ? t('gallery.how_one', { n }) : t('gallery.how_many', { n })) : t('gallery.how_reordered') }), ch.a_steps.length ? el('ol', {}, ...ch.a_steps.map((s) => el('li', { text: s.why }))) : null, - ch.b_steps.length ? el('p', { class: 'qs-note', text: 'and this one reduces to the same by:' }) : null, + ch.b_steps.length ? el('p', { class: 'qs-note', text: t('gallery.and_this_one') }) : null, ch.b_steps.length ? el('ol', {}, ...ch.b_steps.map((s) => el('li', { text: s.why }))) : null, - ch.reordered ? el('p', { class: 'qs-note', text: 'The two are then the same up to the order of gates that commute.' }) : null); + ch.reordered ? el('p', { class: 'qs-note', text: t('gallery.commute') }) : null); }; // A candidate {label, qasm, source, idea?, equivalence?} measured against the draft. // Why a candidate is left out by the reader's rules: every change it makes @@ -120,7 +121,7 @@ export function galleryPane({ engine, draft, model, apply, check = null, found = async function measure(src, c, base) { const lint = await engine.lint(c.qasm); - if (!lint.ok) return { ...c, refused: `does not parse: ${lint.error}` }; + if (!lint.ok) return { ...c, refused: t('gallery.does_not_parse', { error: lint.error }) }; const crit = await engine.critique(c.qasm); if (!crit.ok) return { ...c, refused: crit.error }; const equivalence = c.equivalence ?? await engine.equivalent(src, c.qasm); @@ -152,7 +153,7 @@ export function galleryPane({ engine, draft, model, apply, check = null, found = if (!taste || !features.on('taste') || cands.length < 2 || declared(src).length || !taste.profile().choices.length) return { cands, said: '' }; const r = await taste.rank(cands.map((c, i) => ({ name: String(i), qasm: c.qasm })), {}, 0).catch(() => null); if (!r) return { cands, said: '' }; - return { cands: r.ranked.map((x) => cands[Number(x.name)]), said: `Ordered by your taste (${taste.name()}, ${r.choices} choice${r.choices === 1 ? '' : 's'}).` }; + return { cands: r.ranked.map((x) => cands[Number(x.name)]), said: r.choices === 1 ? t('gallery.by_taste_one', { profile: taste.name(), n: r.choices }) : t('gallery.by_taste_many', { profile: taste.name(), n: r.choices }) }; } function show(mode, src, base, cands, refused, said) { @@ -161,22 +162,22 @@ export function galleryPane({ engine, draft, model, apply, check = null, found = if (mode === 'explore' && s.cls === 'bad') s.cls = ''; // a different circuit is what Explore asks for const nv = features.on('novelty') ? noveltyLabel(c.novelty) : null; const by = c.source === 'engine' ? { kind: 'engine', name: 'Alternatives' } : { kind: 'assistant', name: model()?.model?.name || undefined }; - if (nv?.flag === 'route') found?.flag({ kind: 'route', title: `${c.idea || c.label}: ${nv.text}`, why: nv.why, where: mode === 'improve' ? 'Improve' : 'Explore', source: c.qasm, by }); + if (nv?.flag === 'route') found?.flag({ kind: 'route', title: `${c.idea || c.label}: ${nv.text}`, why: nv.why, where: mode === 'improve' ? t('gallery.improve') : t('gallery.explore'), source: c.qasm, by }); const card = el('li', { class: `qs-cand${nv?.flag ? ' find' : ''}`, 'data-i': i, id: `qs-cand-${i}`, tabindex: '-1' }, el('div', { class: 'qs-cand-head' }, el('span', { class: 'qs-cand-n', text: String(i + 1) }), el('span', { class: 'qs-cand-title', text: c.idea || c.label }), - el('span', { class: 'qs-badge', text: c.source === 'engine' ? 'the engine' : 'the model' })), + el('span', { class: 'qs-badge', text: c.source === 'engine' ? t('gallery.from_engine') : t('gallery.from_model') })), el('div', { class: 'qs-chips' }, ...chips(c.measures, base).filter((x) => x.dataset.k !== 'qubits' || c.measures.qubits !== base.qubits)), el('div', { class: 'qs-cand-facts' }, el('span', { class: `qs-badge ${s.cls}`, title: s.why || '', text: s.text }), - el('span', { class: 'qs-note', text: c.distance.changed ? `${c.distance.changed} lines changed` : 'the same lines' }), + el('span', { class: 'qs-note', text: c.distance.changed ? t('gallery.lines_changed', { n: c.distance.changed }) : t('gallery.same_lines') }), nv ? el('span', { class: `qs-badge qs-novelty ${nv.cls}`, title: nv.why, text: nv.text }) : null), c.changes?.length ? el('p', { class: 'qs-note', text: c.changes.join('; ') }) : null, c.chain && features.on('rewrites') ? chainView(c.chain) : null, - el('details', { class: 'qs-cand-src' }, el('summary', { text: 'Program' }), el('pre', { class: 'qs-ai-code', text: c.qasm })), + el('details', { class: 'qs-cand-src' }, el('summary', { text: t('gallery.program') }), el('pre', { class: 'qs-ai-code', text: c.qasm })), el('div', { class: 'qs-actions' }, - el('button', { class: 'qs-btn qs-small qs-apply', type: 'button', text: 'Apply', onclick: async () => { chose(mode, cands, i); learn(await apply(c.qasm, `${mode === 'improve' ? 'An improvement' : 'An exploration'} from ${c.source === 'engine' ? 'the engine' : 'the model'}: ${c.idea || c.label}. ${s.text[0].toUpperCase() + s.text.slice(1)}.`, by), c, src); } }))); + el('button', { class: 'qs-btn qs-small qs-apply', type: 'button', text: t('gallery.apply'), onclick: async () => { chose(mode, cands, i); learn(await apply(c.qasm, `${mode === 'improve' ? 'An improvement' : 'An exploration'} from ${c.source === 'engine' ? 'the engine' : 'the model'}: ${c.idea || c.label}. ${s.text[0].toUpperCase() + s.text.slice(1)}.`, by), c, src); } }))); return card; }); const pick = (i, go) => { @@ -185,8 +186,8 @@ export function galleryPane({ engine, draft, model, apply, check = null, found = }; body.replaceChildren(...[ cands.length ? atlas(base, cands, pick) : null, - cands.length ? el('ol', { class: 'qs-cands', id: 'qs-cands' }, ...cards) : el('p', { class: 'qs-empty', text: mode === 'improve' ? 'Nothing to improve that the engine can check: no gates cancel or merge, and the model offered none it could confirm.' : 'The model offered nothing the engine could read.' }), - refused.length ? el('p', { class: 'qs-note', id: 'qs-alt-refused', text: `Not offered: ${refused.join('; ')}.` }) : null, + cands.length ? el('ol', { class: 'qs-cands', id: 'qs-cands' }, ...cards) : el('p', { class: 'qs-empty', text: mode === 'improve' ? t('gallery.nothing_to_improve') : t('gallery.nothing_read') }), + refused.length ? el('p', { class: 'qs-note', id: 'qs-alt-refused', text: t('gallery.not_offered', { list: refused.join('; ') }) }) : null, said ? el('p', { class: 'qs-note', text: said }) : null].filter(Boolean)); // replaceChildren writes a null as the text "null" } @@ -194,25 +195,25 @@ export function galleryPane({ engine, draft, model, apply, check = null, found = if (busy) return; busy = true; improveBtn.disabled = exploreBtn.disabled = true; const src = draft(); - body.replaceChildren(el('p', { class: 'qs-note', text: 'The engine is looking…' })); + body.replaceChildren(el('p', { class: 'qs-note', text: t('gallery.engine_looking') })); try { const base = await engine.critique(src); - if (!base.ok) { body.replaceChildren(el('p', { class: 'qs-empty', text: `The engine cannot read the draft: ${base.error}` })); return; } + if (!base.ok) { body.replaceChildren(el('p', { class: 'qs-empty', text: t('gallery.cannot_read', { error: base.error }) })); return; } const v = await engine.variants(src); const raw = (v.candidates || []).map((c) => ({ label: c.label, qasm: c.qasm, changes: c.changes, source: 'engine', equivalence: c.equivalence })); const m = paused() ? null : model(); - let said = paused() ? 'You are exploring on your own: these are the engine’s alone.' : m ? '' : 'Connect a model under “Model…” in the Assistant for its rewrites as well.'; + let said = paused() ? t('gallery.paused_engine_only') : m ? '' : t('gallery.connect_for_rewrites'); if (m) { - try { for (const p of await m.improve(src, 3)) raw.push({ label: p.idea || 'a rewrite', idea: p.idea, qasm: p.qasm, source: 'model' }); } - catch (e) { said = `The model did not answer (${e.message}); these are the engine’s alone.`; } + try { for (const p of await m.improve(src, 3)) raw.push({ label: p.idea || t('gallery.a_rewrite'), idea: p.idea, qasm: p.qasm, source: 'model' }); } + catch (e) { said = t('gallery.model_failed_engine_only', { error: e.message }); } } const cands = [], refused = []; for (const c of raw) { const r = await measure(src, c, base); const ruled = r.refused ? null : await breaks(src, r.qasm); if (r.refused) refused.push(`${r.label}: ${r.refused}`); - else if (ruled) refused.push(`${r.label}: it breaks your rules: ${ruled}`); - else if (r.equivalence.verdict === 'different') refused.push(`${r.label}: not the same circuit (${r.equivalence.why})`); + else if (ruled) refused.push(t('gallery.breaks_rules', { label: r.label, why: ruled })); + else if (r.equivalence.verdict === 'different') refused.push(t('gallery.not_same', { label: r.label, why: r.equivalence.why })); else if (!cands.some((x) => x.qasm.trim() === r.qasm.trim()) && r.qasm.trim() !== src.trim()) cands.push(r); } const ordered = await byTaste(src, cands); @@ -223,26 +224,26 @@ export function galleryPane({ engine, draft, model, apply, check = null, found = async function explore() { if (busy) return; - if (paused()) { body.replaceChildren(el('p', { class: 'qs-empty', id: 'qs-alt-paused', text: 'You are exploring on your own: Explore asks the model, so it waits until you end that under Versions. Improve works without it.' })); return; } + if (paused()) { body.replaceChildren(el('p', { class: 'qs-empty', id: 'qs-alt-paused', text: t('gallery.paused_explore') })); return; } const m = model(); - if (!m) { body.replaceChildren(el('p', { class: 'qs-empty', id: 'qs-alt-nomodel', text: 'Explore asks a model for ideas, and none is connected. Connect one under “Model…” in the Assistant; Improve works without one.' })); return; } + if (!m) { body.replaceChildren(el('p', { class: 'qs-empty', id: 'qs-alt-nomodel', text: t('gallery.explore_needs_model') })); return; } busy = true; improveBtn.disabled = exploreBtn.disabled = true; const src = draft(); - body.replaceChildren(el('p', { class: 'qs-note', text: 'The model is imagining; the engine will measure each idea…' })); + body.replaceChildren(el('p', { class: 'qs-note', text: t('gallery.model_imagining') })); try { const base = await engine.critique(src); - if (!base.ok) { body.replaceChildren(el('p', { class: 'qs-empty', text: `The engine cannot read the draft: ${base.error}` })); return; } + if (!base.ok) { body.replaceChildren(el('p', { class: 'qs-empty', text: t('gallery.cannot_read', { error: base.error }) })); return; } const ideas = await m.explore(src, intent.value.trim(), 4); const cands = [], refused = []; for (const p of ideas) { - const r = await measure(src, { label: p.idea || 'an idea', idea: p.idea, qasm: p.qasm, source: 'model' }, base); + const r = await measure(src, { label: p.idea || t('gallery.an_idea'), idea: p.idea, qasm: p.qasm, source: 'model' }, base); const ruled = r.refused ? null : await breaks(src, r.qasm); if (r.refused) refused.push(`${r.label}: ${r.refused}`); - else if (ruled) refused.push(`${r.label}: it breaks your rules: ${ruled}`); + else if (ruled) refused.push(t('gallery.breaks_rules', { label: r.label, why: ruled })); else if (!cands.some((x) => x.qasm.trim() === r.qasm.trim())) cands.push(r); } show('explore', src, base, cands, refused, ''); - } catch (e) { body.replaceChildren(el('p', { class: 'qs-empty', text: `The model did not answer: ${e.message}` })); } + } catch (e) { body.replaceChildren(el('p', { class: 'qs-empty', text: t('gallery.model_failed', { error: e.message }) })); } finally { busy = false; improveBtn.disabled = exploreBtn.disabled = false; } } diff --git a/wai/quantum-studio/src/generators.js b/wai/quantum-studio/src/generators.js index 17ee99fb6..f89dbf95a 100644 --- a/wai/quantum-studio/src/generators.js +++ b/wai/quantum-studio/src/generators.js @@ -5,6 +5,7 @@ // diagram, the plan, a run, the lens, the alternatives) takes the circuit the // generator writes out at those values. import { el } from './dom.js'; +import { t } from './i18n.js'; const INPUT_RE = /^\s*input\s+u?int(?:\s*\[\s*\d+\s*\])?\s+([A-Za-z_]\w*)\s*;/gm; const VALUES = 'wai.quantum-studio.inputs'; @@ -46,9 +47,9 @@ export function inputsBar(onChange) { let saved = {}; try { saved = JSON.parse(localStorage.getItem(VALUES) || '{}') || {}; } catch (e) {} const fields = el('span', { class: 'qs-inputs-fields' }); - const node = el('div', { class: 'qs-inputs', id: 'qs-inputs', hidden: true, role: 'group', 'aria-label': 'The generator’s inputs' }, - el('span', { class: 'qs-inputs-label', text: 'A generator, read at' }), fields, - el('span', { class: 'qs-note', text: 'one circuit of its family; the Workbench checks it at every size.' })); + const node = el('div', { class: 'qs-inputs', id: 'qs-inputs', hidden: true, role: 'group', 'aria-label': t('generators.inputs') }, + el('span', { class: 'qs-inputs-label', text: t('generators.read_at') }), fields, + el('span', { class: 'qs-note', text: t('generators.one_circuit') })); let names = []; const values = () => Object.fromEntries(names.map((n) => [n, Number.isFinite(saved[n]) ? saved[n] : 4])); function sync(qasm) { @@ -57,7 +58,7 @@ export function inputsBar(onChange) { names = now; node.hidden = names.length === 0; fields.replaceChildren(...names.map((n) => { - const input = el('input', { class: 'qs-input qs-small', type: 'number', min: '0', max: '4096', step: '1', id: `qs-input-${n}`, value: String(values()[n]), 'aria-label': `the value of ${n}` }); + const input = el('input', { class: 'qs-input qs-small', type: 'number', min: '0', max: '4096', step: '1', id: `qs-input-${n}`, value: String(values()[n]), 'aria-label': t('generators.value_of', { name: n }) }); input.addEventListener('change', () => { const v = Math.max(0, Math.min(4096, Math.round(Number(input.value) || 0))); input.value = String(v); diff --git a/wai/quantum-studio/src/hardware.js b/wai/quantum-studio/src/hardware.js index 2f5f23481..c9fdf1464 100644 --- a/wai/quantum-studio/src/hardware.js +++ b/wai/quantum-studio/src/hardware.js @@ -10,8 +10,9 @@ // not free asks the person in a dialog before it is submitted. The answer is // a job like every other, its counts measured on the device and sealed. import { el } from './dom.js'; +import { t as tr } from './i18n.js'; -export const NOTE = 'Runs on your own hardware go through the desktop app or the command line (wai-quantum run --provider NAME): a browser tab must not hold the credentials a provider needs.'; +export const NOTE = tr('hardware.note'); // Whether a plan (the dry run's answer) needs the person's confirmation: any // run not stated free, an unknown cost included. @@ -20,10 +21,10 @@ export const needsConfirmation = (plan) => plan?.free !== true; // The estimate in words. export function costText(plan) { const e = plan?.estimate || {}; - if (e.unknown) return `cost unknown: ${e.unknown}`; - if (e.free === true) return 'free'; - const unit = e.unit ? ` (billed by the ${e.unit})` : ''; - const secs = e.seconds !== undefined ? `, about ${e.seconds} s` : ''; + if (e.unknown) return tr('hardware.cost_unknown', { why: e.unknown }); + if (e.free === true) return tr('hardware.free'); + const unit = e.unit ? tr('hardware.billed_by', { unit: e.unit }) : ''; + const secs = e.seconds !== undefined ? tr('hardware.about_seconds', { seconds: e.seconds }) : ''; return `${e.amount} ${e.currency}${unit}${secs}`; } @@ -32,10 +33,10 @@ export function confirmText(plan) { const p = plan?.provider || {}; const c = plan?.plan?.compile || {}; return [ - `Run on ${p.name} (${p.device}, ${p.provenance})?`, - `It is not free: ${costText(plan)}.`, - `The program is compiled to ${c.native_gates} native gates in ${c.basis}, ${c.swaps} swaps, and was checked against your circuit here; ${plan?.plan?.shots} shots.`, - 'Running it spends your account\'s money or quota.', + tr('hardware.confirm_run', { name: p.name, device: p.device, provenance: p.provenance }), + tr('hardware.confirm_cost', { cost: costText(plan) }), + tr('hardware.confirm_compiled', { gates: c.native_gates, basis: c.basis, swaps: c.swaps, shots: plan?.plan?.shots }), + tr('hardware.confirm_spend'), ]; } @@ -57,7 +58,7 @@ export class HardwareEngine { estimate(request) { return this.call('provider_estimate', { name: this.provider, request: JSON.stringify({ ...request, provider: this.provider }) }); } // The estimate first; the person's yes before anything that is not free. async run(request) { - if (!this.provider) throw new Error('choose one of your providers first'); + if (!this.provider) throw new Error(tr('hardware.choose_provider')); const req = { qasm: request.qasm, shots: request.shots, seed: request.seed, provider: this.provider }; const plan = await this.estimate(req); let confirm = false; @@ -76,10 +77,10 @@ export class HardwareEngine { // A dialog that asks before a run that is not free: resolves true on "Run". export function askDialog(host) { return (plan) => new Promise((resolve) => { - const yes = el('button', { class: 'qs-btn qs-primary', type: 'button', id: 'qs-hw-yes', text: 'Run it' }); - const no = el('button', { class: 'qs-btn', type: 'button', id: 'qs-hw-no', text: 'Cancel' }); + const yes = el('button', { class: 'qs-btn qs-primary', type: 'button', id: 'qs-hw-yes', text: tr('hardware.run_it') }); + const no = el('button', { class: 'qs-btn', type: 'button', id: 'qs-hw-no', text: tr('hardware.cancel') }); const d = el('dialog', { class: 'qs-dialog', id: 'qs-hw-confirm', 'aria-labelledby': 'qs-hw-confirm-title' }, - el('h2', { id: 'qs-hw-confirm-title', text: 'This run is not free' }), + el('h2', { id: 'qs-hw-confirm-title', text: tr('hardware.not_free_title') }), ...confirmText(plan).map((t) => el('p', { text: t })), el('div', { class: 'qs-row' }, no, yes)); const end = (v) => { d.close(); d.remove(); resolve(v); }; @@ -95,13 +96,13 @@ export function askDialog(host) { // The switch's segment and, on the desktop, the provider picker. `onPick` // is told when the person picks this place to run. export function hardwareSegment({ desktop, engine, onPick, say }) { - const seg = el('button', { class: 'qs-seg', type: 'button', 'aria-pressed': 'false', 'data-where': 'hardware', id: 'qs-where-hardware', text: 'Your hardware', title: desktop ? 'Run on one of your own providers; a run that is not free asks first' : NOTE }); - const picker = el('select', { class: 'qs-select', id: 'qs-provider', 'aria-label': 'Your provider', hidden: true }); + const seg = el('button', { class: 'qs-seg', type: 'button', 'aria-pressed': 'false', 'data-where': 'hardware', id: 'qs-where-hardware', text: tr('hardware.segment'), title: desktop ? tr('hardware.segment_title') : NOTE }); + const picker = el('select', { class: 'qs-select', id: 'qs-provider', 'aria-label': tr('hardware.provider_label'), hidden: true }); seg.addEventListener('click', async () => { if (!desktop) { say(NOTE); return; } try { const list = await engine.providers(); - if (!list.length) { say('No providers yet: add one with wai-quantum providers add, then reopen this list.'); return; } + if (!list.length) { say(tr('hardware.no_providers')); return; } picker.replaceChildren(...list.map((p) => el('option', { value: p.name, text: `${p.name} (${p.kind})` }))); picker.hidden = false; engine.select(picker.value); diff --git a/wai/quantum-studio/src/i18n.js b/wai/quantum-studio/src/i18n.js new file mode 100644 index 000000000..ac4138aad --- /dev/null +++ b/wai/quantum-studio/src/i18n.js @@ -0,0 +1,69 @@ +// The Studio's words in the reader's language. +// +// The words are data: locales//.json, one file for each part of +// the interface, gathered by locales/index.js; English is the source. +// test/locales.test.mjs holds every locale to every key, with the same +// placeholders, and every non-English file to its machine-assisted mark. A key +// a locale lacks reads in English. The Studio follows the browser's language +// until the reader chooses one with the switch, and that choice is kept in +// this browser. The engine's own messages (a verdict's statement, a +// diagnostic) stay as the engine writes them. +import LOCALES from '../locales/index.js'; + +export { LOCALES }; +// Each locale by its own name, for the switch. +export const NAMES = { en: 'English', 'zh-CN': '简体中文' }; +const ITEM = 'wai.quantum-studio.locale'; + +// The locale for a language tag, as the engine picks it (engine::course::locale): +// Simplified Chinese for a tag that names it, English for every other. +export function localeOf(tag) { + const t = String(tag || '').trim().split(/[.@]/)[0].replace(/_/g, '-').toLowerCase(); + return t === 'zh' || t.startsWith('zh-cn') || t.startsWith('zh-sg') || t.startsWith('zh-hans') ? 'zh-CN' : 'en'; +} + +// The reader's own choice when there is one; else the first of the browser's +// languages the Studio is written in; else English. +export function chooseLocale({ stored, languages = [] } = {}) { + if (stored && LOCALES[stored]) return stored; + for (const tag of languages) { + const t = String(tag || '').toLowerCase(); + if (t.startsWith('zh')) { const l = localeOf(t); if (l !== 'en') return l; continue; } + if (t.startsWith('en')) return 'en'; + } + return 'en'; +} + +// The page's locale, chosen once as the Studio loads: a host may pin one with +// data-lang on the mount element. Outside a page (the tests under Node), English. +function detect() { + if (typeof document === 'undefined') return 'en'; + const pinned = document.getElementById('studio')?.dataset?.lang; + if (pinned && LOCALES[pinned]) return pinned; + let stored = null; + try { stored = localStorage.getItem(ITEM); } catch (e) {} + const languages = navigator.languages?.length ? navigator.languages : [navigator.language]; + return chooseLocale({ stored, languages }); +} + +let current = detect(); +if (typeof document !== 'undefined') document.documentElement.lang = current; + +export const locale = () => current; + +// The reader's choice, kept in this browser; the page reloads into it. +export function chooseAndReload(l) { + if (!LOCALES[l]) return; + try { localStorage.setItem(ITEM, l); } catch (e) {} + location.reload(); +} + +// `key`'s words, each {name} replaced by its value. +export function t(key, vars) { + let s = LOCALES[current]?.[key] ?? LOCALES.en[key] ?? key; + if (vars) for (const [k, v] of Object.entries(vars)) s = s.split(`{${k}}`).join(String(v)); + return s; +} + +// Whether this locale's words are a machine-assisted translation, not yet reviewed. +export const machineAssisted = () => current !== 'en' && /^machine-assisted/.test(LOCALES[current]?._meta?.translation ?? ''); diff --git a/wai/quantum-studio/src/main.js b/wai/quantum-studio/src/main.js index 4233a3c5d..693da96ac 100644 --- a/wai/quantum-studio/src/main.js +++ b/wai/quantum-studio/src/main.js @@ -30,6 +30,8 @@ import * as History from './history.js'; import { circuitSvg, mark, annotate } from './circuit-svg.js'; import { Assistant, HttpModel, DesktopModel, STANCES } from './assistant.js'; import { el, sameness } from './dom.js'; +import { t as tr, locale, NAMES, LOCALES, chooseAndReload, machineAssisted } from './i18n.js'; +import { coursePane } from './course.js'; import { critiquePanel } from './critique-view.js'; import { galleryPane } from './gallery.js'; import { workbenchPane } from './workbench.js'; @@ -45,31 +47,49 @@ import { shortcutsPanel } from './shortcuts-view.js'; import { rewritePanel } from './rewrite-view.js'; import { compilePanel } from './compile-view.js'; import { estimatePanel } from './estimate-view.js'; +import { qecPane } from './qec-view.js'; import * as Predict from './predict.js'; import { HardwareEngine, askDialog, hardwareSegment } from './hardware.js'; import { debuggerPane } from './debugger.js'; import { circuitEditor } from './visual-edit.js'; import { devicePane, deviceBlock } from './device.js'; +import { METHOD_NAMES, ACCURACY, compact, accuracyExtras, countsBlock, expectationsBlock, amplitudesBlock, energyText } from './result-parts.js'; +import { answerRenderer, answerRecord } from './answer-view.js'; +import { telemetryStore, telemetryPanel } from './telemetry.js'; -const METHOD_NAMES = { - exact: 'Exact state vector', statevector: 'State vector, double precision', statevector_single: 'State vector on the GPU, single precision', - stabilizer: 'Stabilizer tableau', pauli: 'Sparse Pauli dynamics', mps: 'Matrix product state', tensor_network: 'Tensor network', - cutrank: 'Cut-rank sum', cutrank_single: 'Cut-rank sum on the GPU, single precision', density_matrix: 'Density matrix', trajectories: 'Trajectories', -}; -const ACCURACY = { - 'bit-exact': 'The same bits on every machine.', - exact: 'No approximation beyond double-precision rounding.', - single: 'No approximation beyond single-precision rounding, about 10⁻⁷ per gate.', - bounded: 'An approximation, each value with a proven bound on its error.', - estimate: 'A statistical estimate, each value with its standard error.', - heuristic: 'An approximation, with the standard estimate of its error but no bound.', -}; -const NOISE = [['depolarizing_1q', 'one-qubit depolarizing'], ['depolarizing_2q', 'two-qubit depolarizing'], ['amplitude_damping', 'amplitude damping'], ['dephasing', 'dephasing'], ['readout', 'readout flips']]; +const NOISE = ['depolarizing_1q', 'depolarizing_2q', 'amplitude_damping', 'dephasing', 'readout'].map((k) => [k, tr(`noise.${k}`)]); +const MITIGATE = [['none', 'none'], ['zne', 'zero-noise extrapolation'], ['pea', 'error amplification (learned noise)'], ['pec', 'error cancellation (learned noise)'], ['cdr', 'Clifford data regression'], ['vd', 'virtual distillation'], ['readout', 'readout inversion']]; + +// A job's mitigation: each value raw and mitigated with its standard error, the +// ideal one the simulation knows, the method's assumptions and its cost. +function mitigationBlock(m) { + const n = (x, d = 5) => (x == null ? '—' : Number(x).toFixed(d)); + const rows = (m.observables ?? []).map((o) => el('tr', {}, + el('th', { scope: 'row', text: `⟨${o.observable}⟩` }), + el('td', { class: 'num', text: `${n(o.raw)} ± ${n(o.raw_stderr, 4)}` }), + el('td', { class: 'num', text: `${n(o.mitigated)} ± ${n(o.stderr, 4)}` }), + el('td', { class: 'num qs-note', text: n(o.ideal) }))); + const dist = (m.distribution ?? []).slice(0, 16).map((d) => el('tr', {}, + el('th', { scope: 'row', text: d.bits }), el('td', { class: 'num', text: n(d.raw, 4) }), el('td', { class: 'num', text: `${n(d.mitigated, 4)} ± ${n(d.stderr, 4)}` }), el('td', {}))); + return el('div', { class: 'qs-block', id: 'qs-mitigation' }, el('div', { class: 'qs-k', text: `Mitigated: ${m.method}` }), + el('table', { class: 'qs-table' }, + el('thead', {}, el('tr', {}, el('th', { text: '' }), el('th', { class: 'num', text: 'raw' }), el('th', { class: 'num', text: 'mitigated' }), el('th', { class: 'num', text: rows.length ? 'ideal (simulation)' : '' }))), + el('tbody', {}, ...rows, ...dist)), + el('p', { class: 'qs-note', id: 'qs-mitigation-bias', text: `Bias: ${m.assumptions?.bias ?? ''}` }), + el('p', { class: 'qs-note', text: `Error bars: ${m.assumptions?.variance ?? ''} Cost: ${m.cost?.circuits ?? 0} circuits, ${m.cost?.shots ?? 0} shots.` })); +} const DRAFT = 'wai.quantum-studio.draft'; const PREDICT_FIRST = 'wai.quantum-studio.predict-first'; -const ms = (t) => (t < 1 ? 'under 1 ms' : t < 10000 ? `${Math.round(t)} ms` : `${(t / 1000).toFixed(1)} s`); -const compact = (b) => (b.length <= 24 ? b : `${b.slice(0, 8)}…${b.slice(-8)} (${b.length} bits)`); +const ms = (t) => (t < 1 ? tr('frame.under_ms') : t < 10000 ? `${Math.round(t)} ms` : `${(t / 1000).toFixed(1)} s`); const split = (s) => s.split(/[,\n]/).map((x) => x.trim()).filter(Boolean); +// Where a job ran, in the reader's words (the history keeps the engine's). +const WHERE = { 'this browser': 'frame.where_browser', 'the service': 'frame.where_service', 'this computer': 'frame.where_computer' }; +const whereWord = (w) => { + if (WHERE[w]) return tr(WHERE[w]); + if (w === 'your hardware') return tr('frame.where_hardware'); + const m = /^your provider (.+)$/.exec(w || ''); + return m ? tr('frame.where_provider', { name: m[1] }) : w; +}; // Whose hand a line is in, in the editor's gutter. class HandMark extends GutterMarker { @@ -109,22 +129,43 @@ function mount(root) { // ---- the frame ---- const status = el('span', { class: 'qs-status', role: 'status', 'aria-live': 'polite' }); - const runBtn = el('button', { class: 'qs-btn qs-primary', id: 'qs-run', type: 'button', title: 'Run (Ctrl or ⌘ + Enter)', text: '▶ Run' }); - const cancelBtn = el('button', { class: 'qs-btn', id: 'qs-cancel', type: 'button', hidden: true, text: 'Cancel' }); + const runBtn = el('button', { class: 'qs-btn qs-primary', id: 'qs-run', type: 'button', title: tr('frame.run_title'), text: tr('frame.run') }); + const cancelBtn = el('button', { class: 'qs-btn', id: 'qs-cancel', type: 'button', hidden: true, text: tr('frame.cancel') }); const predictFirst = el('input', { type: 'checkbox', id: 'qs-predict-first' }); try { predictFirst.checked = localStorage.getItem(PREDICT_FIRST) === '1'; } catch (e) {} predictFirst.addEventListener('change', () => { try { localStorage.setItem(PREDICT_FIRST, predictFirst.checked ? '1' : '0'); } catch (e) {} }); - const predictLabel = el('label', { class: 'qs-check', for: 'qs-predict-first', title: 'Say what you expect before each run, and see how close you come' }, predictFirst, el('span', { text: 'Predict first' })); - const whereLocal = el('button', { class: 'qs-seg on', type: 'button', 'aria-pressed': 'true', 'data-where': 'local', text: desktop ? 'This computer' : 'This browser' }); - const whereService = el('button', { class: 'qs-seg', type: 'button', 'aria-pressed': 'false', 'data-where': 'service', text: 'The service', disabled: !service }); + const predictLabel = el('label', { class: 'qs-check', for: 'qs-predict-first', title: tr('frame.predict_title') }, predictFirst, el('span', { text: tr('frame.predict') })); + const whereLocal = el('button', { class: 'qs-seg on', type: 'button', 'aria-pressed': 'true', 'data-where': 'local', text: desktop ? tr('frame.this_computer') : tr('frame.this_browser') }); + const whereService = el('button', { class: 'qs-seg', type: 'button', 'aria-pressed': 'false', 'data-where': 'service', text: tr('frame.the_service'), disabled: !service }); const hw = hardwareSegment({ desktop, engine: hardware, onPick: () => choose(hw.seg), say: (m) => setStatus(m) }); - const examples = el('select', { class: 'qs-select', id: 'qs-example', 'aria-label': 'Load an example' }, - el('option', { value: '', text: 'Examples…' }), ...EXAMPLES.map((e) => el('option', { value: e.id, text: e.title }))); + const examples = el('select', { class: 'qs-select', id: 'qs-example', 'aria-label': tr('frame.examples_label') }, + el('option', { value: '', text: tr('frame.examples') }), ...EXAMPLES.map((e) => el('option', { value: e.id, text: e.title }))); + // ---- the course: lessons with exercises the engine checks ---- + const course = coursePane({ + engine: native, + program: () => text(), + load: async (qasm, l) => { + if (!(await from(`the lesson ${l.id}`, () => setText(qasm)))) return false; + shows.textContent = tr('frame.lesson_loaded', { title: l.title }); + return true; + }, + ask: { + available: () => aiConnected, + hint: (prompt) => new Promise((resolve, reject) => { + assistantFor().ask(prompt, (ev) => { if (ev.type === 'answer') resolve({ text: ev.text, proposals: ev.proposals || [] }); }).then(() => resolve({ text: '', proposals: [] }), reject); + }), + }, + status: (m, cls) => setStatus(m, cls), + }); + // ---- the language, followed from the browser until the reader picks one ---- + const langSwitch = el('select', { class: 'qs-select', id: 'qs-lang', 'aria-label': tr('frame.language') }, + ...Object.keys(LOCALES).map((l) => el('option', { value: l, text: NAMES[l] || l, selected: l === locale() }))); + langSwitch.addEventListener('change', () => chooseAndReload(langSwitch.value)); const header = el('header', { class: 'qs-top' }, - el('div', { class: 'qs-brand' }, el('span', { class: 'qs-mark', 'aria-hidden': 'true', text: '⟨ψ⟩' }), el(embedded ? 'span' : 'h1', { class: 'qs-title', text: 'Quantum Studio' })), - examples, - el('div', { class: 'qs-where', role: 'group', 'aria-label': 'Run on' }, whereLocal, whereService, hw.seg), hw.picker, - predictLabel, runBtn, cancelBtn); + el('div', { class: 'qs-brand' }, el('span', { class: 'qs-mark', 'aria-hidden': 'true', text: '⟨ψ⟩' }), el(embedded ? 'span' : 'h1', { class: 'qs-title', text: tr('frame.studio') })), + examples, course.button, + el('div', { class: 'qs-where', role: 'group', 'aria-label': tr('frame.run_on') }, whereLocal, whereService, hw.seg), hw.picker, + predictLabel, runBtn, cancelBtn, langSwitch); // ---- the reader's rules for proposals ---- const genInputs = () => (declared(text()).length ? inputs.values() : {}); @@ -139,7 +180,7 @@ function mount(root) { hold: (a, z) => { const doc = view.state.doc; view.dispatch({ changes: [{ from: doc.line(a).from, insert: `${LOCK}\n` }, { from: doc.line(z).to, insert: `\n${UNLOCK}` }] }); - setStatus(`Held lines ${a} to ${z}: no proposal may change them. Delete the markers to let them go.`, 'ok'); + setStatus(tr('frame.held', { a, z }), 'ok'); }, changed: () => paintHeld(), }); @@ -154,25 +195,28 @@ function mount(root) { const lintBadge = el('span', { class: 'qs-badge', id: 'qs-lint', text: '…' }); const fileIn = el('input', { type: 'file', accept: '.qasm,.txt,text/plain', hidden: true, 'aria-hidden': 'true' }); const shows = el('p', { class: 'qs-shows', id: 'qs-shows' }); - const reviewBox = el('div', { class: 'qs-review', id: 'qs-review', hidden: true, role: 'region', 'aria-label': 'A proposal to review' }); + const reviewBox = el('div', { class: 'qs-review', id: 'qs-review', hidden: true, role: 'region', 'aria-label': tr('frame.review_label') }); const editorHost = el('div', { class: 'qs-editor', id: 'qs-editor' }); - const shareBtn = el('button', { class: 'qs-btn qs-small', type: 'button', text: 'Copy link', title: 'A link with the program in its fragment, which no server sees' }); - const openBtn = el('button', { class: 'qs-btn qs-small', type: 'button', text: 'Open' }); - const saveBtn = el('button', { class: 'qs-btn qs-small', type: 'button', text: 'Save' }); + const shareBtn = el('button', { class: 'qs-btn qs-small', type: 'button', text: tr('frame.copy_link'), title: tr('frame.copy_link_title') }); + const openBtn = el('button', { class: 'qs-btn qs-small', type: 'button', text: tr('frame.open') }); + const saveBtn = el('button', { class: 'qs-btn qs-small', type: 'button', text: tr('frame.save') }); const field = (id, label, attrs = {}) => { const input = el(attrs.tag || 'input', { class: 'qs-input', id, ...attrs, tag: null }); return [input, el('label', { class: 'qs-field', for: id }, el('span', { text: label }), input)]; }; - const [shots, shotsF] = field('qs-shots', 'Shots', { type: 'number', min: '0', step: '1', placeholder: '1024', inputmode: 'numeric' }); - const [seed, seedF] = field('qs-seed', 'Seed', { type: 'number', min: '0', step: '1', placeholder: '1', inputmode: 'numeric' }); - const method = el('select', { class: 'qs-input', id: 'qs-method' }, el('option', { value: 'auto', text: 'auto', title: 'The strongest method the program admits' })); - const methodF = el('label', { class: 'qs-field', for: 'qs-method' }, el('span', { text: 'Method' }), method); - const [obs, obsF] = field('qs-obs', 'Observables', { placeholder: 'Z0 Z1, 0.5 X2', autocomplete: 'off', spellcheck: 'false' }); - const [amps, ampsF] = field('qs-amps', 'Amplitudes', { placeholder: '000, 111', autocomplete: 'off', spellcheck: 'false' }); + const [shots, shotsF] = field('qs-shots', tr('frame.shots'), { type: 'number', min: '0', step: '1', placeholder: '1024', inputmode: 'numeric' }); + const [seed, seedF] = field('qs-seed', tr('frame.seed'), { type: 'number', min: '0', step: '1', placeholder: '1', inputmode: 'numeric' }); + const method = el('select', { class: 'qs-input', id: 'qs-method' }, el('option', { value: 'auto', text: 'auto', title: tr('frame.auto_title') })); + const methodF = el('label', { class: 'qs-field', for: 'qs-method' }, el('span', { text: tr('frame.method') }), method); + const [obs, obsF] = field('qs-obs', tr('frame.observables'), { placeholder: 'Z0 Z1, 0.5 X2', autocomplete: 'off', spellcheck: 'false' }); + const [amps, ampsF] = field('qs-amps', tr('frame.amplitudes'), { placeholder: '000, 111', autocomplete: 'off', spellcheck: 'false' }); const noiseInputs = NOISE.map(([k, label]) => field(`qs-noise-${k}`, label, { type: 'number', min: '0', max: '1', step: '0.001', placeholder: '0', 'data-noise': k })); + // Mitigation of the job's values under its noise (engine::mitigate). + const mitigate = el('select', { class: 'qs-input', id: 'qs-mitigate' }, ...MITIGATE.map(([v, t]) => el('option', { value: v, text: t }))); + const mitigateF = el('label', { class: 'qs-field', for: 'qs-mitigate' }, el('span', { text: 'Mitigate' }), mitigate); // A device target: its noise is its own, so the noise fields wait while one is chosen. - const noiseNote = el('p', { class: 'qs-note', id: 'qs-noise-note', hidden: true, text: 'A device is chosen: the noise is the device’s own, from its calibration.' }); + const noiseNote = el('p', { class: 'qs-note', id: 'qs-noise-note', hidden: true, text: tr('frame.device_noise') }); const device = devicePane({ engine: native, changed: (t) => { @@ -185,14 +229,14 @@ function mount(root) { const jobBox = el('div', { class: 'qs-job' }, el('div', { class: 'qs-row' }, shotsF, seedF, methodF), el('div', { class: 'qs-row' }, obsF, ampsF), - el('details', { class: 'qs-noise' }, el('summary', { text: 'Noise' }), noiseNote, el('div', { class: 'qs-row' }, ...noiseInputs.map((n) => n[1]))), + el('details', { class: 'qs-noise' }, el('summary', { text: tr('frame.noise') }), noiseNote, el('div', { class: 'qs-row' }, ...noiseInputs.map((n) => n[1])), el('div', { class: 'qs-row' }, mitigateF)), device.node); const program = el('section', { class: 'qs-pane qs-program', 'aria-labelledby': 'qs-program-h' }, - el('div', { class: 'qs-head' }, el('h2', { id: 'qs-program-h', text: 'Program' }), lintBadge, el('span', { class: 'qs-grow' }), openBtn, saveBtn, shareBtn, fileIn), + el('div', { class: 'qs-head' }, el('h2', { id: 'qs-program-h', text: tr('frame.program') }), lintBadge, el('span', { class: 'qs-grow' }), openBtn, saveBtn, shareBtn, fileIn), shows, reviewBox, editorHost, inputs.node, rules.node, jobBox); // ---- the circuit ---- - const circuit = el('pre', { class: 'qs-circuit', id: 'qs-circuit', tabindex: '0', 'aria-label': 'The circuit, drawn as text', hidden: true }); + const circuit = el('pre', { class: 'qs-circuit', id: 'qs-circuit', tabindex: '0', 'aria-label': tr('frame.circuit_text_label'), hidden: true }); // The engine's account of the circuit in words, which a screen reader reads for the diagram. const described = el('p', { class: 'qs-sr', id: 'qs-describe' }); const diagram = el('div', { class: 'qs-diagram', id: 'qs-diagram', tabindex: '0', 'aria-describedby': 'qs-describe' }); @@ -201,29 +245,29 @@ function mount(root) { const picture = async (format, name, type) => { try { const v = await local.picture(await local.instance(text()), format); - if (desktop) { const r = await local.saveProgram(name, v[format]); setStatus(`Saved ${r.path}.`, 'ok'); } - else { download(name, v[format], type); setStatus(`Saved ${name}, the engine's drawing.`, 'ok'); } + if (desktop) { const r = await local.saveProgram(name, v[format]); setStatus(tr('frame.saved', { path: r.path }), 'ok'); } + else { download(name, v[format], type); setStatus(tr('frame.saved_drawing', { name }), 'ok'); } } catch (e) { setStatus(e.message, 'bad'); } }; - const asSvg = el('button', { class: 'qs-btn qs-small', id: 'qs-export-svg', type: 'button', text: 'SVG', title: 'Save the drawing as an SVG image', onclick: () => picture('svg', 'circuit.svg', 'image/svg+xml') }); - const asTex = el('button', { class: 'qs-btn qs-small', id: 'qs-export-tex', type: 'button', text: 'LaTeX', title: 'Save the drawing as LaTeX (plain TikZ)', onclick: () => picture('latex', 'circuit.tex', 'text/x-tex') }); + const asSvg = el('button', { class: 'qs-btn qs-small', id: 'qs-export-svg', type: 'button', text: 'SVG', title: tr('frame.svg_title'), onclick: () => picture('svg', 'circuit.svg', 'image/svg+xml') }); + const asTex = el('button', { class: 'qs-btn qs-small', id: 'qs-export-tex', type: 'button', text: 'LaTeX', title: tr('frame.latex_title'), onclick: () => picture('latex', 'circuit.tex', 'text/x-tex') }); // Editing by the drawing: each edit made in the program's text by the engine, and recorded as the reader's. const drawEdits = circuitEditor({ engine: local, program: () => text(), apply: (v) => { - if (versions.busy()) { setStatus('Choose on the proposal under review first.', 'bad'); return false; } + if (versions.busy()) { setStatus(tr('frame.choose_first'), 'bad'); return false; } view.dispatch({ changes: { from: v.edit.from_utf16, to: v.edit.to_utf16, insert: v.edit.insert } }); return true; }, status: (m, cls) => setStatus(m, cls), }); drawEdits.bind(diagram); - const asText = el('button', { class: 'qs-btn qs-small', id: 'qs-astext', type: 'button', 'aria-pressed': 'false', text: 'Text', title: 'The circuit as text, to copy' }); + const asText = el('button', { class: 'qs-btn qs-small', id: 'qs-astext', type: 'button', 'aria-pressed': 'false', text: tr('frame.text'), title: tr('frame.text_title') }); asText.addEventListener('click', () => { const on = circuit.hidden; circuit.hidden = !on; diagram.hidden = on; - asText.setAttribute('aria-pressed', String(on)); asText.textContent = on ? 'Diagram' : 'Text'; + asText.setAttribute('aria-pressed', String(on)); asText.textContent = on ? tr('frame.diagram') : tr('frame.text'); }); // The critique, kept up as the program changes, and marked on the diagram. // A smell's gates are marked; a smell about whole qubits (idle, unmeasured) marks their wires. @@ -250,7 +294,7 @@ function mount(root) { // The estimate pane, asked for: the program's counts, what a fault-tolerant machine needs, the frontier. const estimator = estimatePanel({ engine: local, program: () => text() }); const circuitPane = el('section', { class: 'qs-pane qs-circ', 'aria-labelledby': 'qs-circuit-h' }, - el('div', { class: 'qs-head' }, el('h2', { id: 'qs-circuit-h', text: 'Circuit' }), el('span', { class: 'qs-grow' }), el('span', { class: 'qs-note', id: 'qs-plan' }), asText, asSvg, asTex), + el('div', { class: 'qs-head' }, el('h2', { id: 'qs-circuit-h', text: tr('frame.circuit') }), el('span', { class: 'qs-grow' }), el('span', { class: 'qs-note', id: 'qs-plan' }), asText, asSvg, asTex), drawEdits.node, diagram, described, circuit, legend, critique.node, lens.node, rewrites.node, compiler.node, estimator.node); // Step through: the engine's debugger, the next gate marked on the diagram. const debug = debuggerPane({ @@ -299,68 +343,83 @@ function mount(root) { }); // ---- the result ---- - const state = el('span', { class: 'qs-badge', id: 'qs-state', text: 'ready' }); - const resultBody = el('div', { class: 'qs-result', id: 'qs-result' }, el('p', { class: 'qs-empty', text: 'Run the program to see its result here. Counts are keyed by the classical bits, highest bit first.' })); + const state = el('span', { class: 'qs-badge', id: 'qs-state', text: tr('frame.ready') }); + const resultBody = el('div', { class: 'qs-result', id: 'qs-result' }, el('p', { class: 'qs-empty', text: tr('frame.result_empty') })); const resultPane = el('section', { class: 'qs-pane qs-res', 'aria-labelledby': 'qs-result-h' }, - el('div', { class: 'qs-head' }, el('h2', { id: 'qs-result-h', text: 'Result' }), state), resultBody); + el('div', { class: 'qs-head' }, el('h2', { id: 'qs-result-h', text: tr('frame.result') }), state), resultBody); // ---- the history ---- const historyList = el('ol', { class: 'qs-history', id: 'qs-history' }); const historyPane = el('section', { class: 'qs-pane qs-hist', 'aria-labelledby': 'qs-history-h' }, - el('div', { class: 'qs-head' }, el('h2', { id: 'qs-history-h', text: 'History' }), el('span', { class: 'qs-grow' }), - el('button', { class: 'qs-btn qs-small', type: 'button', text: 'Clear', onclick: async () => { await History.clear(); showHistory(); } })), + el('div', { class: 'qs-head' }, el('h2', { id: 'qs-history-h', text: tr('frame.history') }), el('span', { class: 'qs-grow' }), + el('button', { class: 'qs-btn qs-small', type: 'button', text: tr('frame.clear'), onclick: async () => { await History.clear(); showHistory(); } })), historyList); // ---- the assistant ---- const AI_ITEM = 'wai.quantum-studio.assistant'; const aiConf = { endpoint: 'http://127.0.0.1:8080/v1', model: '', stance: 'questions', initiative: 'asked' }; try { Object.assign(aiConf, JSON.parse(localStorage.getItem(AI_ITEM) || '{}')); } catch (e) {} - const [aiEndpoint, aiEndpointF] = field('qs-ai-endpoint', 'Model server', { value: aiConf.endpoint, spellcheck: 'false', autocomplete: 'off' }); - const [aiModel, aiModelF] = field('qs-ai-model', 'Model', { value: aiConf.model, placeholder: 'the server’s own', spellcheck: 'false', autocomplete: 'off' }); - const aiState = el('span', { class: 'qs-badge', id: 'qs-ai-state', text: 'not connected' }); - const aiConnect = el('button', { class: 'qs-btn qs-small', id: 'qs-ai-connect', type: 'button', text: 'Connect' }); + const [aiEndpoint, aiEndpointF] = field('qs-ai-endpoint', tr('ai.server'), { value: aiConf.endpoint, spellcheck: 'false', autocomplete: 'off' }); + const [aiModel, aiModelF] = field('qs-ai-model', tr('ai.model'), { value: aiConf.model, placeholder: tr('ai.model_placeholder'), spellcheck: 'false', autocomplete: 'off' }); + const aiState = el('span', { class: 'qs-badge', id: 'qs-ai-state', text: tr('ai.not_connected') }); + const aiConnect = el('button', { class: 'qs-btn qs-small', id: 'qs-ai-connect', type: 'button', text: tr('ai.connect') }); const aiStance = el('select', { class: 'qs-select', id: 'qs-ai-stance' }, ...Object.entries(STANCES).map(([k, v]) => el('option', { value: k, text: v.label }))); aiStance.value = STANCES[aiConf.stance] ? aiConf.stance : 'questions'; const aiInitiative = el('select', { class: 'qs-select', id: 'qs-ai-initiative' }, - el('option', { value: 'asked', text: 'Only when I ask' }), el('option', { value: 'hint', text: 'Offer a hint when I seem stuck' })); + el('option', { value: 'asked', text: tr('ai.when_asked') }), el('option', { value: 'hint', text: tr('ai.when_stuck') })); aiInitiative.value = aiConf.initiative === 'hint' ? 'hint' : 'asked'; const keepConf = () => { try { localStorage.setItem(AI_ITEM, JSON.stringify({ endpoint: aiEndpoint.value.trim(), model: aiModel.value.trim(), stance: aiStance.value, initiative: aiInitiative.value })); } catch (e) {} }; aiStance.addEventListener('change', keepConf); aiInitiative.addEventListener('change', () => { keepConf(); if (aiInitiative.value === 'asked') stuck.hidden = true; }); const aiModes = el('div', { class: 'qs-ai-modes' }, - el('label', { class: 'qs-field', for: 'qs-ai-stance' }, el('span', { text: 'How it helps' }), aiStance), - el('label', { class: 'qs-field', for: 'qs-ai-initiative' }, el('span', { text: 'When it speaks' }), aiInitiative)); + el('label', { class: 'qs-field', for: 'qs-ai-stance' }, el('span', { text: tr('ai.how') }), aiStance), + el('label', { class: 'qs-field', for: 'qs-ai-initiative' }, el('span', { text: tr('ai.when') }), aiInitiative)); const aiSettings = el('div', { class: 'qs-ai-settings', id: 'qs-ai-settings', hidden: true }, el('div', { class: 'qs-row' }, aiEndpointF, aiModelF), el('div', { class: 'qs-actions' }, aiConnect), - el('p', { class: 'qs-note', text: 'A model on this machine by default: any server with the chat-completions interface and tool calls. Nothing leaves this machine unless the address points elsewhere.' })); + el('p', { class: 'qs-note', text: tr('ai.settings_note') })); const aiLog = el('ol', { class: 'qs-ai-log', id: 'qs-ai-log', 'aria-live': 'polite' }, - el('li', { class: 'qs-note', text: 'Ask about the program. The assistant speaks only when asked, and uses the engine to check what it says: every number comes from a run, and every program it proposes is read and run here before it is offered.' })); - const aiInput = el('textarea', { class: 'qs-input qs-ai-input', id: 'qs-ai-input', rows: '2', placeholder: 'Ask about this program…', 'aria-label': 'Ask the assistant' }); - const aiSend = el('button', { class: 'qs-btn qs-primary', id: 'qs-ai-send', type: 'button', text: 'Ask' }); + el('li', { class: 'qs-note', text: tr('ai.intro') })); + const aiInput = el('textarea', { class: 'qs-input qs-ai-input', id: 'qs-ai-input', rows: '2', placeholder: tr('ai.placeholder'), 'aria-label': tr('ai.ask_label') }); + const aiSend = el('button', { class: 'qs-btn qs-primary', id: 'qs-ai-send', type: 'button', text: tr('ai.ask') }); // Offered, never sent: with “Offer a hint when I seem stuck”, a problem the // engine has reported for a while, or runs refused twice in a row, show this. - const stuck = el('button', { class: 'qs-btn qs-small qs-stuck', id: 'qs-ai-stuck', type: 'button', hidden: true, text: 'Stuck? Ask for a hint', onclick: () => { stuck.hidden = true; ask(HINT, 'Hint'); } }); + const stuck = el('button', { class: 'qs-btn qs-small qs-stuck', id: 'qs-ai-stuck', type: 'button', hidden: true, text: tr('ai.stuck'), onclick: () => { stuck.hidden = true; ask(HINT, tr('ai.q.hint')); } }); const STUCK_MS = Number(root.dataset.stuckMs) || 45000; let badSince = null, stuckTimer = null, refusals = 0; const offerHint = () => { if (aiInitiative.value === 'hint') stuck.hidden = false; }; const HINT = 'Give me one hint toward what is wrong or could be better in this program: point at where to look, not at the fix. Use the tools to find it first.'; const QUICK = [ - ['Ask me a question', 'Ask me one question about this program that would help me see what it does or how to make it better. Use the tools first if they help, and do not give the answer.'], - ['Hint', HINT], - ['Explain this circuit', 'Explain what this program does, step by step, and what its result shows.'], - ['Why this method?', 'Why does the engine pick this method for this job, and what would make it pick another?'], - ['Fix the problem', 'The program has a problem. Find it with the tools and give the corrected whole program.'], + [tr('ai.q.question'), 'Ask me one question about this program that would help me see what it does or how to make it better. Use the tools first if they help, and do not give the answer.'], + [tr('ai.q.hint'), HINT], + [tr('ai.q.explain'), 'Explain what this program does, step by step, and what its result shows.'], + [tr('ai.q.method'), 'Why does the engine pick this method for this job, and what would make it pick another?'], + [tr('ai.q.fix'), 'The program has a problem. Find it with the tools and give the corrected whole program.'], ]; + // A correctness record of the model's answers, on this device, off until the reader turns it on. + const telemetry = telemetryStore({ engine: local }); const aiPane = el('section', { class: 'qs-pane qs-ai', 'aria-labelledby': 'qs-ai-h' }, - el('div', { class: 'qs-head' }, el('h2', { id: 'qs-ai-h', text: 'Assistant' }), aiState, el('span', { class: 'qs-grow' }), - el('button', { class: 'qs-btn qs-small', type: 'button', id: 'qs-ai-gear', text: 'Model…', 'aria-controls': 'qs-ai-settings', onclick: () => { aiSettings.hidden = !aiSettings.hidden; } })), - aiSettings, aiModes, + el('div', { class: 'qs-head' }, el('h2', { id: 'qs-ai-h', text: tr('ai.title') }), aiState, el('span', { class: 'qs-grow' }), + el('button', { class: 'qs-btn qs-small', type: 'button', id: 'qs-ai-gear', text: tr('ai.gear'), 'aria-controls': 'qs-ai-settings', onclick: () => { aiSettings.hidden = !aiSettings.hidden; } })), + aiSettings, aiModes, telemetryPanel({ store: telemetry, save: (name, body, type) => saveFile(name, body, type) }), el('div', { class: 'qs-ai-quick' }, ...QUICK.map(([label, q]) => el('button', { class: 'qs-btn qs-small', type: 'button', text: label, onclick: () => ask(q, label) })), stuck), aiLog, el('div', { class: 'qs-ai-ask' }, aiInput, aiSend)); - const footer = el('footer', { class: 'qs-foot' }, el('span', { class: 'qs-signer', id: 'qs-signer' }), status); + // ---- answers in the answer view layer, drawn as they arrive ---- + const avInput = el('textarea', { class: 'qs-input qs-av-input', id: 'qs-av-input', rows: '6', spellcheck: 'false', placeholder: 'root = Answer([bell, counts])\n```qasm bell\n…\n```\ncounts = Histogram(@Sample(bell, 1000))', 'aria-label': 'An answer in the answer view layer' }); + const avQ = el('input', { class: 'qs-input', id: 'qs-av-q', placeholder: 'Ask the model for an answer about this program…', 'aria-label': 'Ask the model for an answer' }); + const avDraw = el('button', { class: 'qs-btn qs-small', type: 'button', id: 'qs-av-draw', text: 'Draw' }); + const avAsk = el('button', { class: 'qs-btn qs-small', type: 'button', id: 'qs-av-ask', text: 'Ask the model' }); + const avAfter = el('div', { class: 'qs-actions', id: 'qs-av-after', hidden: true }); + const avStatus = el('p', { class: 'qs-note', id: 'qs-av-status' }); + const answers = answerRenderer({ engine: local, onEnded: (done) => answerEnded(done) }); + const answerPane = el('details', { class: 'qs-pane qs-answers', id: 'qs-answers' }, el('summary', { text: 'Answer views' }), + el('p', { class: 'qs-note', text: 'An answer in the answer view layer: the program, and views whose numbers come from engine calls written with @. Each view is drawn as its statement arrives, its call carried out by the engine here; a number the model wrote in a view is refused with its code.' }), + el('div', { class: 'qs-row' }, avQ, avAsk), avInput, el('div', { class: 'qs-actions' }, avDraw), avStatus, answers.node, avAfter); + + const footer = el('footer', { class: 'qs-foot' }, el('span', { class: 'qs-signer', id: 'qs-signer' }), status, + machineAssisted() ? el('span', { class: 'qs-note', id: 'qs-translation-note', text: tr('frame.machine_assisted') }) : null); root.classList.add('qs'); // ---- the versions ---- const editable = new Compartment(); @@ -398,7 +457,10 @@ function mount(root) { flags.onChange(settle); flags.apply(); - root.replaceChildren(header, el('div', { class: 'qs-grid' }, program, el('div', { class: 'qs-side' }, circuitPane, debug.node, gallery.node, workbench.node, found.node, resultPane, versions.node, aiPane, historyPane, studyUi.node)), footer); + // ---- error correction: a code, an error placed on it, its decode, a threshold ---- + const qec = qecPane({ engine: local }); + + root.replaceChildren(header, course.node, el('div', { class: 'qs-grid' }, program, el('div', { class: 'qs-side' }, circuitPane, debug.node, gallery.node, workbench.node, found.node, resultPane, qec.node, versions.node, aiPane, answerPane, historyPane, studyUi.node)), footer); // ---- the editor ---- const style = HighlightStyle.define([ @@ -415,16 +477,16 @@ function mount(root) { const lintAt = async (view) => { const text = view.state.doc.toString(); let v; - try { v = await local.lint(text); } catch (e) { lintBadge.textContent = 'engine not loaded'; lintBadge.className = 'qs-badge bad'; return []; } + try { v = await local.lint(text); } catch (e) { lintBadge.textContent = tr('frame.engine_not_loaded'); lintBadge.className = 'qs-badge bad'; return []; } if (v.ok) { - const at = declared(text).length ? ` at ${Object.entries(inputs.values()).map(([n, x]) => `${n} = ${x}`).join(', ')}` : ''; - lintBadge.textContent = `${v.qubits} qubits · ${v.gates} gates${at}${v.dynamic_steps != null ? ' · measures in the middle' : ''}`; + const at = declared(text).length ? tr('frame.lint_at', { values: Object.entries(inputs.values()).map(([n, x]) => `${n} = ${x}`).join(', ') }) : ''; + lintBadge.textContent = `${tr('frame.lint', { qubits: v.qubits, gates: v.gates })}${at}${v.dynamic_steps != null ? tr('frame.lint_dynamic') : ''}`; lintBadge.className = 'qs-badge ok'; badSince = null; clearTimeout(stuckTimer); if (!refusals) stuck.hidden = true; return []; } - lintBadge.textContent = 'problem'; lintBadge.className = 'qs-badge bad'; + lintBadge.textContent = tr('frame.problem'); lintBadge.className = 'qs-badge bad'; if (badSince == null) { badSince = Date.now(); clearTimeout(stuckTimer); stuckTimer = setTimeout(() => { if (badSince != null) offerHint(); }, STUCK_MS); } const doc = view.state.doc; const line = v.line && v.line <= doc.lines ? doc.line(v.line) : doc.line(doc.lines); @@ -449,7 +511,7 @@ function mount(root) { highlightSelectionMatches(), openqasm, syntaxHighlighting(style), autocompletion({ override: [complete] }), // A new value for a generator's input re-reads the program, as an edit does. linter(lintAt, { delay: 300, needsRefresh: (u) => u.transactions.some((tr) => tr.effects.some((e) => e.is(inputsChanged))) }), lintGutter(), theme, - EditorView.contentAttributes.of({ 'aria-label': 'The OpenQASM program' }), + EditorView.contentAttributes.of({ 'aria-label': tr('frame.editor_label') }), keymap.of([{ key: 'Mod-Enter', run: () => { run(); return true; } }, { key: 'Escape', run: () => { if (running) { cancel(); return true; } return false; } }, ...closeBracketsKeymap, ...defaultKeymap, ...searchKeymap, ...historyKeymap, ...completionKeymap, indentWithTab]), EditorView.updateListener.of((u) => { if (u.docChanged) { edited(); versions.edited(); } }), @@ -472,6 +534,7 @@ function mount(root) { if (target) job.target = target; else if (Object.keys(noise).length) job.noise = noise; if (method.value !== 'auto') job.method = method.value; + if (mitigate.value !== 'none' && job.noise) job.mitigate = mitigate.value; return job; } function fill(job) { @@ -480,6 +543,8 @@ function mount(root) { obs.value = (job.observables || []).join(', '); amps.value = (job.amplitudes || []).join(', '); for (const [input] of noiseInputs) input.value = job.noise?.[input.dataset.noise] ?? ''; method.value = [...method.options].some((o) => o.value === job.method) ? job.method : 'auto'; + const m = typeof job.mitigate === 'string' ? job.mitigate : job.mitigate?.method; + mitigate.value = [...mitigate.options].some((o) => o.value === m) ? m : 'none'; if (job.noise && Object.keys(job.noise).length) jobBox.querySelector('details').open = true; if (job.target) { device.set(job.target).catch(() => {}); @@ -533,7 +598,7 @@ function mount(root) { if (where !== 'local') { p.textContent = ''; return; } try { const pl = await local.plan(request()); - p.textContent = `${METHOD_NAMES[pl.method] || pl.method} · ${(pl.memory_bytes / 2 ** 20).toFixed(0)} MiB at most`; + p.textContent = tr('frame.plan', { method: METHOD_NAMES[pl.method] || pl.method, mib: (pl.memory_bytes / 2 ** 20).toFixed(0) }); p.title = pl.why; // On a device target, the layout the plan chose is drawn on its map. device.showPlacement(pl.target?.placement, pl.target?.noise); @@ -550,7 +615,7 @@ function mount(root) { // The program as it is now, not as the last redraw saw it. const c = await local.critique(text()).catch(() => null); resultBody.replaceChildren(Predict.form({ width: c?.ok ? c.bits : 0, onRun: go })); - state.textContent = 'your prediction'; state.className = 'qs-badge run'; + state.textContent = tr('frame.your_prediction'); state.className = 'qs-badge run'; } async function go(prediction) { if (running) return; @@ -560,29 +625,29 @@ function mount(root) { job.verdict = true; const eng = where === 'service' ? service : where === 'hardware' ? hardware : local; running = eng; busy(true); - state.textContent = 'running'; state.className = 'qs-badge run'; - setStatus(`Running on ${eng.where}…`); + state.textContent = tr('frame.running'); state.className = 'qs-badge run'; + setStatus(tr('frame.running_on', { where: whereWord(eng.where) })); // The history's account of the program: the receipt binds whose hand each gate is in. try { job.history = await versions.attest(); } catch (e) {} const t0 = performance.now(); try { - const body = await eng.run(job, (j) => setStatus(`On the service: ${j.status}…`)); + const body = await eng.run(job, (j) => setStatus(tr('frame.on_service', { status: j.status }))); const took = performance.now() - t0; current = { job: body, where: eng.where, took, prediction }; refusals = 0; critique.ran(); await show(current); - setStatus(`Done on ${eng.where} in ${ms(took)}.`, 'ok'); + setStatus(tr('frame.done', { where: whereWord(eng.where), took: ms(took) }), 'ok'); await History.save({ key: body.result?.receipt?.digest || String(Date.now()), saved: Date.now(), where: eng.where, title: firstLine(job.qasm), job: body }); showHistory(); } catch (e) { - state.textContent = e.message === 'cancelled' ? 'cancelled' : 'refused'; state.className = 'qs-badge bad'; + state.textContent = e.message === 'cancelled' ? tr('frame.cancelled_badge') : tr('frame.refused'); state.className = 'qs-badge bad'; if (e.message !== 'cancelled' && ++refusals >= 2) offerHint(); - setStatus(e.message === 'cancelled' ? 'Cancelled.' : e.message, 'bad'); + setStatus(e.message === 'cancelled' ? tr('frame.cancelled') : e.message, 'bad'); // A refusal states its level too: what the engine read, and the code it refused with. if (e.message !== 'cancelled') { const v = await local.verdict({ request: job, error: e.message }).catch(() => null); - if (v) resultBody.replaceChildren(el('div', { class: 'qs-block' }, el('div', { class: 'qs-k', text: 'Verdict' }), el('div', {}, verdictLine(v, { id: 'qs-verdict' })))); + if (v) resultBody.replaceChildren(el('div', { class: 'qs-block' }, el('div', { class: 'qs-k', text: tr('frame.verdict') }), el('div', {}, verdictLine(v, { id: 'qs-verdict' })))); } } finally { running = null; busy(false); } } @@ -594,113 +659,67 @@ function mount(root) { const r = job.result || {}; state.textContent = job.status || 'done'; state.className = 'qs-badge ok'; const acc = r.accuracy || {}; - const extra = []; - if (acc.fidelity_estimate !== undefined) extra.push(`fidelity estimate ${Number(acc.fidelity_estimate).toFixed(4)}`); - if (acc.discarded_weight !== undefined) extra.push(`discarded weight ${Number(acc.discarded_weight).toExponential(2)}`); - if (acc.histories !== undefined) extra.push(`${acc.histories} error histories`); - if (acc.device) extra.push(`on ${acc.device}`); - if (acc.max_cut !== undefined) extra.push(`the widest cut ${acc.max_cut} bits`); - if (r.global_phase) extra.push(`global phase ${r.global_phase}`); - const dy = r.dynamic; - if (dy) { - const parts = [`${dy.measurements_and_resets} measurements and resets`]; - if (dy.branches_run !== undefined) parts.push(`the shots split into ${dy.branches_run} branches`); - if (dy.branches_weighed !== undefined) parts.push(`the expectation values weigh ${dy.branches_weighed} branches exactly`); - extra.push(parts.join('; ')); - } + const extra = accuracyExtras(r); const blocks = [ prediction ? Predict.block(prediction, r) : null, - el('div', { class: 'qs-block' }, el('div', { class: 'qs-k', text: 'Method' }), + el('div', { class: 'qs-block' }, el('div', { class: 'qs-k', text: tr('frame.method') }), el('div', {}, el('b', { class: 'qs-method', text: METHOD_NAMES[job.method] || job.method || '' }), el('span', { class: 'qs-why', text: ' ' + (job.why || '') }))), - el('div', { class: 'qs-block' }, el('div', { class: 'qs-k', text: 'Accuracy' }), + el('div', { class: 'qs-block' }, el('div', { class: 'qs-k', text: tr('frame.accuracy') }), el('div', {}, el('span', { class: 'qs-badge ok', text: acc.class || '?' }), el('span', { class: 'qs-why', text: ' ' + (ACCURACY[acc.class] || '') + (extra.length ? ' ' + extra.join(', ') + '.' : '') })), r.warning ? el('p', { class: 'qs-warn', text: r.warning }) : null), - el('div', { class: 'qs-block', id: 'qs-verdict-block' }, el('div', { class: 'qs-k', text: 'Verdict' }), el('div', { id: 'qs-verdict-line' }, r.verdict ? verdictLine(r.verdict, { sealed: true, id: 'qs-verdict' }) : el('span', { class: 'qs-note', text: 'working it out here…' }))), + el('div', { class: 'qs-block', id: 'qs-verdict-block' }, el('div', { class: 'qs-k', text: tr('frame.verdict') }), el('div', { id: 'qs-verdict-line' }, r.verdict ? verdictLine(r.verdict, { sealed: true, id: 'qs-verdict' }) : el('span', { class: 'qs-note', text: tr('frame.working_out') }))), ]; - if (r.counts) { - const entries = Object.entries(r.counts).sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1)); - const total = entries.reduce((s, [, n]) => s + n, 0) + (r.omitted_shots || 0); - const max = entries.length ? entries[0][1] : 1; - blocks.push(el('div', { class: 'qs-block' }, el('div', { class: 'qs-k', text: `Counts · ${total} shots · highest bit first` }), - el('div', { class: 'qs-bars' }, ...entries.slice(0, 32).map(([k, n]) => el('div', { class: 'qs-bar' }, - el('span', { class: 'qs-key', title: k, text: compact(k) }), - el('span', { class: 'qs-track' }, el('span', { class: 'qs-fill', style: `width:${(100 * n) / max}%` })), - el('span', { class: 'qs-n', text: `${n} · ${((100 * n) / total).toFixed(1)}%` })))), - entries.length > 32 ? el('p', { class: 'qs-note', text: `${entries.length - 32} more outcomes.` }) : null)); - } - if (r.expectations) { - blocks.push(el('div', { class: 'qs-block' }, el('div', { class: 'qs-k', text: 'Expectation values' }), - el('table', { class: 'qs-table' }, el('tbody', {}, ...r.expectations.map((e) => el('tr', {}, - el('th', { scope: 'row', text: `⟨${e.observable}⟩` }), el('td', { class: 'num', text: Number(e.value).toFixed(6) }), - el('td', { class: 'qs-note', text: e.stderr !== undefined ? `± ${Number(e.stderr).toExponential(1)}` : e.bound !== undefined ? `error ≤ ${Number(e.bound).toExponential(1)}` : '' }))))))); - } + if (r.counts) blocks.push(countsBlock(r)); + if (r.expectations) blocks.push(expectationsBlock(r)); + if (r.mitigation) blocks.push(mitigationBlock(r.mitigation)); if (r.target) { blocks.push(deviceBlock(r.target, r.ideal)); device.showPlacement(r.target.placement, r.target.noise); } - if (r.amplitudes) { - blocks.push(el('div', { class: 'qs-block' }, el('div', { class: 'qs-k', text: 'Amplitudes' }), - el('table', { class: 'qs-table' }, el('thead', {}, el('tr', {}, el('th', { text: 'state' }), el('th', { text: 'amplitude' }), el('th', { text: 'probability' }), el('th', { text: 'phase' }))), - el('tbody', {}, ...r.amplitudes.map((a) => { - const p = a.re * a.re + a.im * a.im; - const ph = Math.atan2(a.im, a.re) / Math.PI; - return el('tr', {}, el('th', { scope: 'row', text: `|${compact(a.bits)}⟩` }), - el('td', { class: 'num', text: `${Number(a.re).toFixed(6)} ${a.im < 0 ? '−' : '+'} ${Math.abs(a.im).toFixed(6)}i` }), - el('td', { class: 'num', text: p.toFixed(6) }), el('td', { class: 'num', text: p < 1e-24 ? '—' : `${ph.toFixed(4)}π` })); - }))))); - } + if (r.amplitudes) blocks.push(amplitudesBlock(r)); // The receipt, checked here. const rc = r.receipt || {}; - const account = el('span', { class: 'qs-badge', id: 'qs-account', text: 'checking…' }); - const check = el('span', { class: 'qs-badge', id: 'qs-verified', text: 'checking…' }); - const en = r.energy || {}; - // A figure is shown with its class, its coverage and its declared uncertainty, as the - // service states them, so a reading is never shown without what it is - // (energy-measurement §2); no figure is shown as unmetered, never as 0 J. - const amount = (j) => j < 1e3 ? j + ' µJ' : j < 1e6 ? (j / 1e3).toFixed(1) + ' mJ' : (j / 1e6).toFixed(2) + ' J'; - const ppm = en.energy_uncertainty && en.energy_uncertainty.relative_ppm; - const energy = en.joules_micro && en.energy_provenance - ? `${amount(en.joules_micro)} (${en.energy_provenance}${en.coverage ? ', ' + en.coverage : ''}${ppm ? ', ±' + (ppm / 1e4).toFixed(ppm < 1e4 ? 2 : 1) + ' %' : ''})` - : en.joules_micro ? `${amount(en.joules_micro)} (no class stated: not a reading)` - : en.unmetered ? `unmetered: ${en.unmetered}` : 'unmetered'; - const dl = el('button', { class: 'qs-btn qs-small', type: 'button', text: 'Download job', onclick: () => saveJob(job) }); - blocks.push(el('div', { class: 'qs-block' }, el('div', { class: 'qs-k', text: 'Receipt' }), + const account = el('span', { class: 'qs-badge', id: 'qs-account', text: tr('frame.checking') }); + const check = el('span', { class: 'qs-badge', id: 'qs-verified', text: tr('frame.checking') }); + const energy = energyText(r.energy || {}); + const dl = el('button', { class: 'qs-btn qs-small', type: 'button', text: tr('frame.download_job'), onclick: () => saveJob(job) }); + blocks.push(el('div', { class: 'qs-block' }, el('div', { class: 'qs-k', text: tr('frame.receipt') }), el('table', { class: 'qs-table' }, el('tbody', {}, - el('tr', {}, el('th', { scope: 'row', text: 'ran on' }), el('td', { text: ran })), - el('tr', {}, el('th', { scope: 'row', text: 'signed by' }), el('td', { class: 'mono', id: 'qs-signed-by', text: (rc.key || '').slice(0, 16) + '…' })), - el('tr', {}, el('th', { scope: 'row', text: 'check' }), el('td', {}, check)), - el('tr', {}, el('th', { scope: 'row', text: 'ran in' }), el('td', { text: r.elapsed_ms != null ? ms(r.elapsed_ms) : `${ms(took)}, timed by this page` })), - el('tr', {}, el('th', { scope: 'row', text: 'energy' }), el('td', { text: energy })), - job.request?.history ? el('tr', {}, el('th', { scope: 'row', text: 'written by' }), el('td', { id: 'qs-written-by', text: writtenBy(job.request.history) })) : null, - job.request?.history ? el('tr', {}, el('th', { scope: 'row', text: 'account' }), el('td', {}, account)) : null)), - el('p', { class: 'qs-note', text: 'A receipt shows that this key signed this request and this result; anyone can run the request again to compare. It does not say who holds the key.' }), + el('tr', {}, el('th', { scope: 'row', text: tr('frame.ran_on') }), el('td', { text: whereWord(ran) })), + el('tr', {}, el('th', { scope: 'row', text: tr('frame.signed_by') }), el('td', { class: 'mono', id: 'qs-signed-by', text: (rc.key || '').slice(0, 16) + '…' })), + el('tr', {}, el('th', { scope: 'row', text: tr('frame.check') }), el('td', {}, check)), + el('tr', {}, el('th', { scope: 'row', text: tr('frame.ran_in') }), el('td', { text: r.elapsed_ms != null ? ms(r.elapsed_ms) : tr('frame.timed_here', { took: ms(took) }) })), + el('tr', {}, el('th', { scope: 'row', text: tr('frame.energy') }), el('td', { text: energy })), + job.request?.history ? el('tr', {}, el('th', { scope: 'row', text: tr('frame.written_by') }), el('td', { id: 'qs-written-by', text: writtenBy(job.request.history) })) : null, + job.request?.history ? el('tr', {}, el('th', { scope: 'row', text: tr('frame.account') }), el('td', {}, account)) : null)), + el('p', { class: 'qs-note', text: tr('frame.receipt_note') }), el('div', { class: 'qs-actions' }, dl))); resultBody.replaceChildren(...blocks.filter(Boolean)); // A result that carries no verdict (from a host before verdicts) has one worked out here. if (!r.verdict) { const v = await local.verdict({ request: job.request, result: job.result }).catch(() => null); - document.getElementById('qs-verdict-line')?.replaceChildren(v ? verdictLine(v, { sealed: false, id: 'qs-verdict' }) : el('span', { class: 'qs-note', text: 'no verdict' })); + document.getElementById('qs-verdict-line')?.replaceChildren(v ? verdictLine(v, { sealed: false, id: 'qs-verdict' }) : el('span', { class: 'qs-note', text: tr('frame.no_verdict') })); } try { const v = await local.verify({ request: job.request, result: job.result }); - const whose = v.key === serviceKey ? 'the service’s key' : v.signer && v.signer === mySigner ? (desktop ? 'this computer’s key' : 'this browser’s key') : 'the key it names'; + const whose = v.key === serviceKey ? tr('frame.key_service') : v.signer && v.signer === mySigner ? (desktop ? tr('frame.key_computer') : tr('frame.key_browser')) : tr('frame.key_named'); document.getElementById('qs-signed-by').textContent = `${whose} ${v.signer ? v.signer.slice(0, 28) + '…' : ''}`; - check.textContent = v.valid ? `✓ verifies, checked on ${desktop ? 'this computer' : 'this device'}` : '✗ does not verify'; + check.textContent = v.valid ? tr('frame.verifies', { where: desktop ? tr('frame.here_computer') : tr('frame.here_device') }) : tr('frame.does_not_verify'); check.className = 'qs-badge ' + (v.valid ? 'ok' : 'bad'); - } catch (e) { check.textContent = `not checked: ${e.message}`; check.className = 'qs-badge bad'; } + } catch (e) { check.textContent = tr('frame.not_checked', { error: e.message }); check.className = 'qs-badge bad'; } if (job.request?.history) { try { const a = await versions.checkJob(job); - account.textContent = a.ok ? `✓ recomputed from ${a.version} in Versions` : `✗ ${a.problems.join('; ')}`; + account.textContent = a.ok ? tr('frame.recomputed', { version: a.version }) : `✗ ${a.problems.join('; ')}`; account.className = 'qs-badge ' + (a.ok ? 'ok' : 'bad'); - } catch (e) { account.textContent = `not in this ${desktop ? 'computer’s' : 'browser’s'} versions`; account.className = 'qs-badge'; account.title = e.message; } + } catch (e) { account.textContent = desktop ? tr('frame.not_in_computer') : tr('frame.not_in_browser'); account.className = 'qs-badge'; account.title = e.message; } } } // A history's account as a line: each hand's gates. function writtenBy(h) { - const parts = (h.contributors || []).filter((c) => c.gates).map((c) => `${c.gates} by ${HANDS[c.hand] || c.hand}${c.name ? ` (${c.name})` : ''}`); - return `${parts.join(', ') || 'no gates'} · of ${h.of?.gates ?? 0} gates, ${h.version}`; + const parts = (h.contributors || []).filter((c) => c.gates).map((c) => tr('frame.gates_by', { n: c.gates, who: `${HANDS[c.hand] || c.hand}${c.name ? ` (${c.name})` : ''}` })); + return tr('frame.written', { parts: parts.join(', ') || tr('frame.no_gates'), of: h.of?.gates ?? 0, version: h.version }); } // ---- whose hand: the gutter and the diagram ---- @@ -735,18 +754,18 @@ function mount(root) { // ---- a proposal, reviewed change by change ---- async function offer(qasm, by, note, opts = {}) { - if (versions.busy()) { setStatus('Choose on the proposal under review first.', 'bad'); return null; } + if (versions.busy()) { setStatus(tr('frame.choose_first'), 'bad'); return null; } const r = await versions.review(qasm, by || { kind: 'person' }, note, opts); if (r) { shows.textContent = note || ''; - const stops = r.blocked ? `; ${r.blocked} blocked by your rules` : ''; - setStatus(r.accepted ? `Took ${r.accepted} of ${r.offered} change${r.offered === 1 ? '' : 's'} from ${whoName(by)}${stops}, as ${r.version}.` : `Took none of the proposal from ${whoName(by)}${stops}; ${r.version} in Versions keeps it.`, 'ok'); + const stops = r.blocked ? tr('frame.blocked', { n: r.blocked }) : ''; + setStatus(r.accepted ? tr('frame.took', { accepted: r.accepted, offered: r.offered, who: whoName(by), stops, version: r.version }) : tr('frame.took_none', { who: whoName(by), stops, version: r.version }), 'ok'); } return r; } // A program from elsewhere, recorded as where it came from. async function from(where, f) { - if (versions.busy()) { setStatus('Choose on the proposal under review first.', 'bad'); return false; } + if (versions.busy()) { setStatus(tr('frame.choose_first'), 'bad'); return false; } await versions.record(); f(); await versions.record({ kind: 'origin', name: where }); @@ -759,14 +778,14 @@ function mount(root) { try { const v = await local.variants(src); const c = (v.candidates || []).find((x) => x.changes.length === 1 && x.changes[0] === s.message); - if (!c) { setStatus('The engine has no rewrite for that one now; the program may have changed.', 'bad'); return; } + if (!c) { setStatus(tr('frame.no_rewrite'), 'bad'); return; } if (text() !== src) return; // edited meanwhile: the rewrite is of an older draft const same = sameness(c.equivalence); const r = await offer(c.qasm, { kind: 'engine', name: 'a fix' }, `Fixed: ${s.message}`, { all: true }); if (!r) return; - if (!r.accepted) { shows.textContent = ''; setStatus(`Your rules block that fix: it would change what they hold. ${r.version} in Versions keeps it.`, 'bad'); return; } - shows.textContent = `Fixed: ${s.message}. The engine checked the result: ${same.why || same.text}.`; - setStatus(`Fixed, as ${r.version}. Undo brings the lines back.`, 'ok'); + if (!r.accepted) { shows.textContent = ''; setStatus(tr('frame.fix_blocked', { version: r.version }), 'bad'); return; } + shows.textContent = tr('frame.fixed_shows', { message: s.message, why: same.why || same.text }); + setStatus(tr('frame.fixed', { version: r.version }), 'ok'); } catch (e) { setStatus(e.message, 'bad'); } } @@ -783,24 +802,24 @@ function mount(root) { } async function saveJob(job) { const name = `wai-quantum-job-${job.id || (job.result?.receipt?.digest || 'here').slice(0, 12)}.json`; - if (desktop) { const r = await local.saveJob(name, job); setStatus(`Saved ${r.path}.`, 'ok'); } - else { download(name, JSON.stringify(job, null, 2) + '\n', 'application/json'); setStatus(`Saved ${name}. wai-quantum verify checks it.`, 'ok'); } + if (desktop) { const r = await local.saveJob(name, job); setStatus(tr('frame.saved', { path: r.path }), 'ok'); } + else { download(name, JSON.stringify(job, null, 2) + '\n', 'application/json'); setStatus(tr('frame.saved_job', { name }), 'ok'); } } saveBtn.addEventListener('click', async () => { const name = 'program.qasm'; - if (desktop) { const r = await local.saveProgram(name, text()); setStatus(`Saved ${r.path}.`, 'ok'); } + if (desktop) { const r = await local.saveProgram(name, text()); setStatus(tr('frame.saved', { path: r.path }), 'ok'); } else download(name, text(), 'text/plain'); }); openBtn.addEventListener('click', () => fileIn.click()); fileIn.addEventListener('change', async () => { const f = fileIn.files?.[0]; if (!f) return; const body = await f.text(); - if (await from(`the file ${f.name}`, () => setText(body))) shows.textContent = `Opened ${f.name}.`; + if (await from(`the file ${f.name}`, () => setText(body))) shows.textContent = tr('frame.opened', { name: f.name }); fileIn.value = ''; }); shareBtn.addEventListener('click', () => { const u = new URL(location.href); u.hash = 'qasm=' + encodeURIComponent(text()); u.search = ''; - navigator.clipboard?.writeText(u.toString()).then(() => setStatus('Copied a link with the program in its fragment, which no server sees.', 'ok'), () => setStatus(u.toString())); + navigator.clipboard?.writeText(u.toString()).then(() => setStatus(tr('frame.copied'), 'ok'), () => setStatus(u.toString())); }); // ---- history ---- @@ -809,10 +828,10 @@ function mount(root) { historyList.replaceChildren(...(all.length ? all.slice(0, 50).map((h) => el('li', {}, el('button', { type: 'button', class: 'qs-hitem', onclick: async () => { if (!(await from('an earlier run', () => fill(h.job.request || {})))) return; - current = { job: h.job, where: h.where, took: 0 }; show(current); setStatus(`From the history: run on ${h.where}, ${new Date(h.saved).toLocaleString()}.`); + current = { job: h.job, where: h.where, took: 0 }; show(current); setStatus(tr('frame.from_history', { where: whereWord(h.where), when: new Date(h.saved).toLocaleString(locale()) })); } }, el('span', { class: 'qs-htitle', text: h.title || '(a program)' }), - el('span', { class: 'qs-note', text: `${METHOD_NAMES[h.job.method] || h.job.method} · ${h.where} · ${new Date(h.saved).toLocaleTimeString()}` })))) : [el('li', { class: 'qs-note', text: 'Jobs you run appear here, kept on this device.' })])); + el('span', { class: 'qs-note', text: `${METHOD_NAMES[h.job.method] || h.job.method} · ${whereWord(h.where)} · ${new Date(h.saved).toLocaleTimeString(locale())}` })))) : [el('li', { class: 'qs-note', text: tr('frame.history_empty') })])); } // ---- where ---- @@ -828,8 +847,8 @@ function mount(root) { const keep = method.value; try { const m = (await (where === 'service' ? service : local).methods()).methods || []; - method.replaceChildren(el('option', { value: 'auto', text: 'auto', title: 'The strongest method the program admits' }), - ...m.filter((x) => x.available !== false).map((x) => el('option', { value: x.method, text: `${METHOD_NAMES[x.method] || x.method} (to ${x.max_qubits} qubits)` }))); + method.replaceChildren(el('option', { value: 'auto', text: 'auto', title: tr('frame.auto_title') }), + ...m.filter((x) => x.available !== false).map((x) => el('option', { value: x.method, text: tr('frame.method_reach', { method: METHOD_NAMES[x.method] || x.method, n: x.max_qubits }) }))); method.value = [...method.options].some((o) => o.value === keep) ? keep : 'auto'; } catch (e) {} } @@ -838,7 +857,7 @@ function mount(root) { examples.value = ''; if (await from(`the example ${e.title}`, () => fill(e.job))) shows.textContent = e.shows; }); - [shots, seed, method, obs, amps, ...noiseInputs.map((n) => n[0])].forEach((i) => i.addEventListener('input', () => { clearTimeout(drawTimer); drawTimer = setTimeout(plan, 250); })); + [shots, seed, method, obs, amps, mitigate, ...noiseInputs.map((n) => n[0])].forEach((i) => i.addEventListener('input', () => { clearTimeout(drawTimer); drawTimer = setTimeout(plan, 250); })); runBtn.addEventListener('click', run); cancelBtn.addEventListener('click', cancel); @@ -865,65 +884,122 @@ function mount(root) { const prose = (t) => t.split(/```[a-z0-9]*\n?/i).map((part, i) => (i % 2 ? el('pre', { class: 'qs-ai-code', text: part.replace(/\n$/, '') }) : part.trim() ? el('p', { class: 'qs-ai-text' }, ...part.trim().split('\n').flatMap((l, j) => [j ? el('br') : null, ...inline(l)])) : null)); async function ask(q, label = q) { if (aiSend.disabled) return; + if (locale() === 'zh-CN') q += '\n\nAnswer in Simplified Chinese.'; aiSend.disabled = true; say('qs-ai-user', el('p', { text: label })); - const thinking = say('qs-ai-tool', el('span', { class: 'qs-note', text: 'thinking…' })); + const thinking = say('qs-ai-tool', el('span', { class: 'qs-note', text: tr('ai.thinking') })); try { await assistantFor().ask(q, (ev) => { if (ev.type === 'tool') { const res = JSON.stringify(ev.result); - aiLog.insertBefore(el('li', { class: 'qs-ai-tool' }, el('details', {}, el('summary', { text: `used ${ev.name}` }), el('pre', { class: 'qs-ai-code', text: res.length > 2000 ? res.slice(0, 2000) + '…' : res }))), thinking); + aiLog.insertBefore(el('li', { class: 'qs-ai-tool' }, el('details', {}, el('summary', { text: tr('ai.used', { name: ev.name }) }), el('pre', { class: 'qs-ai-code', text: res.length > 2000 ? res.slice(0, 2000) + '…' : res }))), thinking); } else if (ev.type === 'retry') { - aiLog.insertBefore(el('li', { class: 'qs-ai-tool' }, el('span', { class: 'qs-note', text: `The engine could not read the proposed program (${ev.error}); asked for a correction.` })), thinking); + aiLog.insertBefore(el('li', { class: 'qs-ai-tool' }, el('span', { class: 'qs-note', text: tr('ai.retry', { error: ev.error }) })), thinking); } else if (ev.type === 'answer') { thinking.remove(); aiConnected = true; const li = say('qs-ai-answer', ...prose(ev.text)); for (const p of ev.proposals) { const box = el('div', { class: 'qs-ai-proposal' }); + // The correctness record, when the reader keeps one: what the engine made of it. + let rid = null; + telemetry.record(proposalRecord(p, q)).then((i) => { rid = i; }); // Every proposal states the level the engine checked it to, never "valid". if (p.verdict) box.append(levelBadge(p.verdict), ' '); - if (!p.lint.ok) box.append(el('span', { class: 'qs-badge bad', text: 'does not parse' }), el('span', { class: 'qs-note', text: ' ' + p.lint.error })); + if (!p.lint.ok) box.append(el('span', { class: 'qs-badge bad', text: tr('ai.does_not_parse') }), el('span', { class: 'qs-note', text: ' ' + p.lint.error })); else { const r = p.job?.result; const counts = r?.counts ? Object.entries(r.counts).sort((a, b) => b[1] - a[1]).slice(0, 4).map(([k, n]) => `${compact(k)}: ${n}`).join(', ') : ''; - box.append(el('span', { class: 'qs-badge ok', text: `checked: ${p.lint.qubits} qubits, ${p.lint.gates} gates` }), - el('span', { class: 'qs-note', text: p.job ? ` ran here on ${METHOD_NAMES[p.job.method] || p.job.method}${counts ? '; ' + counts : ''}` : p.error ? ` did not run: ${p.error}` : '' }), + box.append(el('span', { class: 'qs-badge ok', text: tr('ai.checked', { qubits: p.lint.qubits, gates: p.lint.gates }) }), + el('span', { class: 'qs-note', text: p.job ? tr('ai.ran_here', { method: METHOD_NAMES[p.job.method] || p.job.method }) + (counts ? '; ' + counts : '') : p.error ? tr('ai.did_not_run', { error: p.error }) : '' }), organic.on() - ? el('span', { class: 'qs-note qs-ai-held', text: ' You are exploring on your own: end that under Versions to take it.' }) - : el('button', { class: 'qs-btn qs-small qs-ai-apply', type: 'button', text: 'Apply', onclick: () => offer(p.qasm, { kind: 'assistant', name: assistant?.model?.name || undefined }, 'The assistant’s program, checked and run before it was offered.') })); + ? el('span', { class: 'qs-note qs-ai-held', text: tr('ai.organic') }) + : el('button', { class: 'qs-btn qs-small qs-ai-apply', type: 'button', text: tr('ai.apply'), onclick: () => { telemetry.decide(rid, 'accepted'); offer(p.qasm, { kind: 'assistant', name: assistant?.model?.name || undefined }, 'The assistant’s program, checked and run before it was offered.'); } })); } + box.append(' ', el('button', { class: 'qs-btn qs-small qs-ai-reject', type: 'button', text: 'Not this', onclick: (e) => { telemetry.decide(rid, 'rejected'); e.target.disabled = true; box.classList.add('qs-set-aside'); } })); li.append(box); } - if (ev.unsourced?.length) li.append(el('p', { class: 'qs-ai-unsourced', text: `Not from a run: ${ev.unsourced.join(', ')}. The model gave these itself; the engine did not compute them.` })); - if (ev.usage?.total_tokens) li.append(el('p', { class: 'qs-note', text: `${ev.usage.prompt_tokens ?? '?'} tokens in, ${ev.usage.completion_tokens ?? '?'} out` })); + if (ev.unsourced?.length) li.append(el('p', { class: 'qs-ai-unsourced', text: tr('ai.unsourced', { list: ev.unsourced.join(', ') }) })); + if (ev.usage?.total_tokens) li.append(el('p', { class: 'qs-note', text: tr('ai.tokens', { into: ev.usage.prompt_tokens ?? '?', out: ev.usage.completion_tokens ?? '?' }) })); } }); } catch (e) { thinking.remove(); - say('qs-ai-error', el('p', { text: `The assistant could not answer: ${e.message}. Check the model server under “Model…”.` })); - aiState.textContent = 'not connected'; aiState.className = 'qs-badge bad'; + say('qs-ai-error', el('p', { text: tr('ai.error', { error: e.message }) })); + aiState.textContent = tr('ai.not_connected'); aiState.className = 'qs-badge bad'; aiConnected = false; } finally { aiSend.disabled = false; } } + // A proposal's correctness record: its codes, its verdict, its repairs and its receipt. + function proposalRecord(p, task) { + const v = p.verdict || {}; + const codes = [p.lint?.code, ...(v.refusal?.codes || []), ...(v.parsed?.diagnostics || []).map((d) => d.code)].filter(Boolean); + return { + model: assistant?.model?.name || null, task, program: p.qasm, intent: p.job?.request?.intent, run: p.job?.request ? { shots: p.job.request.shots } : undefined, + codes: [...new Set(codes)], verdict: p.verdict ? { level: v.level, holds: v.holds } : undefined, repairs: (v.repairs || []).map((r) => r.code), + receipt: p.job?.result?.receipt?.digest, decision: 'pending', + }; + } + + // ---- answers in the answer view layer: drawn from a pasted answer, or as the model streams one ---- + let avContext = { task: null, model: null }; + async function drawAnswer(t) { + answers.reset(); avAfter.hidden = true; avContext = { task: null, model: null }; + avStatus.textContent = 'Drawing as it arrives…'; + for (const line of t.split(/(?<=\n)/)) await answers.feed(line); + await answers.end(); + } + async function askAnswer() { + const q = avQ.value.trim(); + if (!q) return; + answers.reset(); avAfter.hidden = true; + avContext = { task: q, model: aiModel.value.trim() || null }; + avStatus.textContent = 'Asking the model…'; + try { + const { prompt } = await local.answer('prompt', {}); + await model().stream({ model: aiModel.value.trim() || undefined, temperature: 0, messages: [ + { role: 'system', content: `${prompt}\nWrite only the answer, in the layer; prose goes in Text views.` }, + { role: 'user', content: `${q}\n\n(The program in the editor:)\n\`\`\`qasm\n${text()}\`\`\`` }, + ] }, (piece) => answers.feed(piece)); + await answers.end(); + } catch (e) { avStatus.textContent = `The model could not answer: ${e.message}`; } + } + async function answerEnded({ answer, text: t, values }) { + if (!answer || answer.error) return; + const n = answer.errors; + avStatus.textContent = `${answer.statements.length} statements; ${n} error${n === 1 ? '' : 's'}${answer.warnings ? ` and ${answer.warnings} note${answer.warnings === 1 ? '' : 's'}` : ''}.`; + const rid = await telemetry.record({ ...answerRecord({ answer, values }), task: avContext.task, model: avContext.model }); + const seal = answer.statements.find((s) => s.view === 'Receipt' && values.get(s.name)?.ok); + avAfter.replaceChildren( + el('button', { class: 'qs-btn qs-small', type: 'button', id: 'qs-av-accept', text: 'Take it', onclick: () => { telemetry.decide(rid, 'accepted'); avStatus.textContent = 'Taken.'; } }), + el('button', { class: 'qs-btn qs-small', type: 'button', id: 'qs-av-reject', text: 'Not this', onclick: () => { telemetry.decide(rid, 'rejected'); avStatus.textContent = 'Set aside.'; } }), + seal ? el('button', { class: 'qs-btn qs-small', type: 'button', id: 'qs-av-store', text: 'Save with its receipt', onclick: async () => { + const m = await local.answer('marker', { statement: seal.name, sealed: values.get(seal.name).value }); + saveFile('answer.wqa', t.replace(/\n?$/, '\n') + m.marker + '\n', 'text/plain'); + } }) : null); + avAfter.hidden = false; + } + avDraw.addEventListener('click', () => drawAnswer(avInput.value)); + avAsk.addEventListener('click', askAnswer); + aiSend.addEventListener('click', () => { const q = aiInput.value.trim(); if (q) { aiInput.value = ''; ask(q); } }); aiInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); aiSend.click(); } }); aiConnect.addEventListener('click', async () => { - aiState.textContent = 'connecting…'; aiState.className = 'qs-badge run'; + aiState.textContent = tr('ai.connecting'); aiState.className = 'qs-badge run'; try { const ids = await model().models(); const name = ids[0] ? ids[0].split(/[\\/]/).pop().replace(/\.gguf$/i, '') : ''; - aiState.textContent = `connected${name ? ' · ' + name : ''}`; aiState.className = 'qs-badge ok'; aiState.title = ids[0] || ''; + aiState.textContent = tr('ai.connected') + (name ? ' · ' + name : ''); aiState.className = 'qs-badge ok'; aiState.title = ids[0] || ''; aiConnected = true; if (!aiModel.value && ids[0]) aiModel.value = ids[0]; - } catch (e) { aiState.textContent = 'not connected'; aiState.className = 'qs-badge bad'; aiState.title = e.message; aiConnected = false; } + } catch (e) { aiState.textContent = tr('ai.not_connected'); aiState.className = 'qs-badge bad'; aiState.title = e.message; aiConnected = false; } }); // ---- start ---- const handed = new URLSearchParams(location.hash.slice(1)).get('qasm'); let draft = null; try { draft = localStorage.getItem(DRAFT); } catch (e) {} - if (handed) { fill({ qasm: handed }); shows.textContent = 'The program from the link.'; } + if (handed) { fill({ qasm: handed }); shows.textContent = tr('frame.from_link'); } else if (draft) setText(draft); else { fill(EXAMPLES[0].job); shows.textContent = EXAMPLES[0].shows; } // The draft is kept as the page goes, not only once a redraw has waited. @@ -931,16 +1007,16 @@ function mount(root) { // The versions this browser kept go on if the editor holds their program; a // draft typed since is the reader's, and anything else starts where it came from. versions.begin(handed ? { kind: 'origin', name: 'a link' } : draft ? { kind: 'person' } : { kind: 'origin', name: `the example ${EXAMPLES[0].title}` }, - draft && !handed ? { kind: 'origin', name: 'the draft kept here' } : null).catch((e) => setStatus(`Versions: ${e.message}`, 'bad')); + draft && !handed ? { kind: 'origin', name: 'the draft kept here' } : null).catch((e) => setStatus(tr('frame.versions_error', { error: e.message }), 'bad')); loadMethods(); showHistory(); - local.signer().then((d) => { mySigner = d; document.getElementById('qs-signer').textContent = `Signs as ${d.slice(0, 32)}… (${desktop ? 'this computer’s key' : 'a key made for this browser'})`; }).catch(() => {}); - service?.describe().then((d) => { serviceKey = d.receipt_key || null; }).catch(() => { whereService.disabled = true; whereService.title = 'The service did not answer'; }); + local.signer().then((d) => { mySigner = d; document.getElementById('qs-signer').textContent = tr('frame.signs_as', { did: d.slice(0, 32), whose: desktop ? tr('frame.key_computer') : tr('frame.key_made_browser') }); }).catch(() => {}); + service?.describe().then((d) => { serviceKey = d.receipt_key || null; }).catch(() => { whereService.disabled = true; whereService.title = tr('frame.service_silent'); }); edited(); forceLinting(view); // `propose(qasm, by, note)`: a program offered as `by`'s proposal, taken // change by change; it resolves once the reader has chosen. - return { view, run, fill, request, gallery, versions, rules, debug, propose: (qasm, by, note) => offer(qasm, by, note) }; + return { view, run, fill, request, gallery, versions, rules, debug, course, propose: (qasm, by, note) => offer(qasm, by, note) }; } const root = document.getElementById('studio'); diff --git a/wai/quantum-studio/src/md.js b/wai/quantum-studio/src/md.js new file mode 100644 index 000000000..93bbc384c --- /dev/null +++ b/wai/quantum-studio/src/md.js @@ -0,0 +1,54 @@ +// A lesson's Markdown as nodes, never as HTML: paragraphs, headings, lists, +// fenced code (its info string's first word as the language), inline code, +// **bold** and *emphasis*. HTML comments, such as a translation's note, are +// left out. +import { el } from './dom.js'; + +// **bold**, *emphasis* and `code` within a line. +export function inline(text) { + return text.split(/(\*\*[^*]+\*\*|`[^`]+`|\*[^*\s][^*]*\*)/).filter(Boolean).map((p) => { + if (p.startsWith('**') && p.endsWith('**')) return el('strong', { text: p.slice(2, -2) }); + if (p.startsWith('`') && p.endsWith('`')) return el('code', { text: p.slice(1, -1) }); + if (p.length > 2 && p.startsWith('*') && p.endsWith('*')) return el('em', { text: p.slice(1, -1) }); + return document.createTextNode(p); + }); +} + +export function markdown(src) { + const out = []; + const lines = String(src || '').replace(//g, '').split('\n'); + let para = []; + let list = null; + const flush = () => { + if (para.length) out.push(el('p', {}, ...inline(para.join(' ')))); + para = []; + }; + const endList = () => { if (list) out.push(list); list = null; }; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const fence = line.match(/^```(\S*)/); + if (fence) { + flush(); endList(); + const body = []; + for (i++; i < lines.length && !lines[i].startsWith('```'); i++) body.push(lines[i]); + out.push(el('pre', { class: 'qs-code', 'data-lang': fence[1] || 'text' }, el('code', { text: body.join('\n') }))); + continue; + } + const h = line.match(/^(#{1,4})\s+(.*)$/); + if (h) { flush(); endList(); out.push(el(`h${Math.min(6, h[1].length + 2)}`, {}, ...inline(h[2]))); continue; } + const item = line.match(/^\s*(?:[-*]|\d+\.)\s+(.*)$/); + if (item) { + flush(); + const ordered = /^\s*\d+\./.test(line); + if (!list || (list.tagName === 'OL') !== ordered) { endList(); list = el(ordered ? 'ol' : 'ul'); } + list.append(el('li', {}, ...inline(item[1]))); + continue; + } + if (!line.trim()) { flush(); endList(); continue; } + if (list && /^\s{2,}\S/.test(line)) { list.lastChild.append(' ', ...inline(line.trim())); continue; } + endList(); + para.push(line.trim()); + } + flush(); endList(); + return out; +} diff --git a/wai/quantum-studio/src/novelty.js b/wai/quantum-studio/src/novelty.js index f4dddc8a2..f39af3452 100644 --- a/wai/quantum-studio/src/novelty.js +++ b/wai/quantum-studio/src/novelty.js @@ -10,6 +10,7 @@ // everything it was compared with. It counts what was flagged and what the // reader saved, in this browser. import { el } from './dom.js'; +import { t } from './i18n.js'; const FOUND = 'wai.quantum-studio.found'; // The novelty at which a verified entry is worth a second look. @@ -24,28 +25,28 @@ const two = (x) => (Math.round(x * 100) / 100).toFixed(2); export function noveltyLabel(v, { verified = false } = {}) { if (!v || v.ok === false) return null; const r = v.rediscovers ?? []; - const what = (x) => (x.prepares ? 'the same state from |0…0⟩' : 'the same circuit'); + const what = (x) => (x.prepares ? t('novelty.same_state') : t('novelty.same_circuit')); const route = r.find((x) => x.route === 'a different route'); if (route) { - return { text: `a different route to ${route.name}`, cls: 'find', flag: 'route', - why: `The engine finds ${what(route)} as ${route.name} (${route.how}), built unlike it: ${two(route.distance)} apart, where 0 is built alike and 1 shares nothing. Worth a second look.` }; + return { text: t('novelty.route', { name: route.name }), cls: 'find', flag: 'route', + why: t('novelty.route_why', { what: what(route), name: route.name, how: route.how, distance: two(route.distance) }) }; } const same = r.find((x) => x.route === 'the same construction') ?? r[0]; - if (same) return { text: `a known construction: ${same.name}`, cls: '', flag: null, why: `The engine finds ${what(same)} as ${same.name} (${same.how}), built alike.` }; + if (same) return { text: t('novelty.known', { name: same.name }), cls: '', flag: null, why: t('novelty.known_why', { what: what(same), name: same.name, how: same.how }) }; if (!v.nearest) return null; const far = verified && v.novelty >= HIGH; - return { text: `new: ${two(v.novelty)} from the nearest (${v.nearest.name})`, cls: far ? 'find' : '', flag: far ? 'novel' : null, - why: `Its gates and their order are ${two(v.novelty)} from ${v.nearest.name}, the nearest of the ${v.compared} programs compared (0 built alike, 1 nothing shared). A reason to look, not a claim that nobody has built it.` }; + return { text: t('novelty.new', { novelty: two(v.novelty), name: v.nearest.name }), cls: far ? 'find' : '', flag: far ? 'novel' : null, + why: t('novelty.new_why', { novelty: two(v.novelty), name: v.nearest.name, compared: v.compared }) }; } // The measures a candidate beats its reference on that the cost did not weigh. export function unaskedText(v, words = {}) { const u = v?.unasked ?? []; if (!u.length) return null; - return `Better than the reference on what the cost does not weigh: ${u.map((x) => `${words[x.measure] ?? x.measure} ${x.candidate} against ${x.reference}`).join('; ')}.`; + return t('novelty.unasked', { list: u.map((x) => t('novelty.unasked_item', { measure: words[x.measure] ?? x.measure, candidate: x.candidate, reference: x.reference })).join('; ') }); } -const KINDS = { route: 'a different route', aberration: 'an aberration', novel: 'far from the known' }; +const KINDS = { route: t('novelty.kind_route'), aberration: t('novelty.kind_aberration'), novel: t('novelty.kind_novel') }; // The pane. `open(item)` offers an item's program to the editor. export function foundPane({ open }) { @@ -54,26 +55,26 @@ export function foundPane({ open }) { const keep = () => { try { localStorage.setItem(FOUND, JSON.stringify(state)); } catch (e) {} }; const count = el('span', { class: 'qs-badge', id: 'qs-found-count' }); const list = el('ol', { class: 'qs-found-list', id: 'qs-found-list' }); - const empty = el('p', { class: 'qs-empty', id: 'qs-found-empty', text: 'What the engine finds worth a second look comes here: a known result reached by a different route, a Workbench candidate that breaks only one of its rules, a verified entry far from everything it was compared with. Save what you mean to keep; dismiss the rest.' }); + const empty = el('p', { class: 'qs-empty', id: 'qs-found-empty', text: t('novelty.found_empty') }); const node = el('section', { class: 'qs-pane qs-found', 'aria-labelledby': 'qs-found-h' }, - el('div', { class: 'qs-head' }, el('h2', { id: 'qs-found-h', text: 'Found' }), el('span', { class: 'qs-grow' }), count), + el('div', { class: 'qs-head' }, el('h2', { id: 'qs-found-h', text: t('novelty.found_title') }), el('span', { class: 'qs-grow' }), count), empty, list); function render() { - count.textContent = `${state.flagged} flagged · ${state.saved} saved`; + count.textContent = t('novelty.found_count', { flagged: state.flagged, saved: state.saved }); empty.hidden = state.items.length > 0; list.replaceChildren(...state.items.map((it) => { - const note = el('input', { class: 'qs-input qs-small', value: it.note ?? '', placeholder: 'A note, kept with it', 'aria-label': `A note on ${it.title}`, autocomplete: 'off' }); + const note = el('input', { class: 'qs-input qs-small', value: it.note ?? '', placeholder: t('novelty.note_placeholder'), 'aria-label': t('novelty.note_label', { title: it.title }), autocomplete: 'off' }); note.addEventListener('change', () => { it.note = note.value; keep(); }); - const saveBtn = el('button', { class: 'qs-btn qs-small qs-found-save', type: 'button', text: it.saved ? 'Saved' : 'Save', 'aria-pressed': String(!!it.saved) }); + const saveBtn = el('button', { class: 'qs-btn qs-small qs-found-save', type: 'button', text: it.saved ? t('novelty.saved') : t('novelty.save'), 'aria-pressed': String(!!it.saved) }); saveBtn.addEventListener('click', () => { it.saved = !it.saved; state.saved += it.saved ? 1 : -1; keep(); render(); }); - const dismiss = el('button', { class: 'qs-btn qs-small qs-found-dismiss', type: 'button', text: 'Dismiss' }); + const dismiss = el('button', { class: 'qs-btn qs-small qs-found-dismiss', type: 'button', text: t('novelty.dismiss') }); dismiss.addEventListener('click', () => { if (it.saved) state.saved -= 1; state.items = state.items.filter((x) => x !== it); keep(); render(); }); - const openBtn = it.source ? el('button', { class: 'qs-btn qs-small', type: 'button', text: 'Open in editor', onclick: () => open(it) }) : null; + const openBtn = it.source ? el('button', { class: 'qs-btn qs-small', type: 'button', text: t('novelty.open'), onclick: () => open(it) }) : null; return el('li', { class: `qs-found-item${it.saved ? ' saved' : ''}`, 'data-kind': it.kind }, el('div', { class: 'qs-cand-head' }, el('span', { class: 'qs-badge find', text: KINDS[it.kind] ?? it.kind }), diff --git a/wai/quantum-studio/src/predict.js b/wai/quantum-studio/src/predict.js index 3eff79ffa..b1213dc33 100644 --- a/wai/quantum-studio/src/predict.js +++ b/wai/quantum-studio/src/predict.js @@ -3,6 +3,7 @@ // close they come, on this device only. A prediction is the reader's; the // Studio never makes one. import { el } from './dom.js'; +import { t as tr } from './i18n.js'; const ITEM = 'wai.quantum-studio.predictions'; const KEEP = 200; @@ -38,25 +39,25 @@ const pct = (x) => `${(100 * x).toFixed(x < 0.1 ? 1 : 0)}%`; export function form({ width, onRun }) { const bits = el('input', { class: 'qs-input', id: 'qs-predict-bits', placeholder: width ? '0'.repeat(width) : '00', autocomplete: 'off', spellcheck: 'false', inputmode: 'numeric', pattern: '[01]+' }); const share = el('input', { class: 'qs-input', id: 'qs-predict-share', type: 'number', min: '0', max: '100', step: '1', placeholder: '50', inputmode: 'decimal' }); - const note = el('p', { class: 'qs-note', id: 'qs-predict-note', text: `Counts are keyed by the classical bits, highest bit first${width ? `: ${width} of them` : ''}.` }); + const note = el('p', { class: 'qs-note', id: 'qs-predict-note', text: width ? tr('predict.keyed_width', { width }) : tr('predict.keyed') }); const go = () => { const b = bits.value.trim(), s = parseFloat(share.value); - if (!/^[01]+$/.test(b)) { note.textContent = 'An outcome is a string of 0s and 1s, highest bit first.'; bits.focus(); return; } - if (width && b.length !== width) { note.textContent = `This program has ${width} classical bit${width === 1 ? '' : 's'}; the outcome has ${b.length}.`; bits.focus(); return; } - if (!(s >= 0 && s <= 100)) { note.textContent = 'The share is a percentage of the shots, from 0 to 100.'; share.focus(); return; } + if (!/^[01]+$/.test(b)) { note.textContent = tr('predict.not_bits'); bits.focus(); return; } + if (width && b.length !== width) { note.textContent = width === 1 ? tr('predict.wrong_width_one', { width, length: b.length }) : tr('predict.wrong_width_other', { width, length: b.length }); bits.focus(); return; } + if (!(s >= 0 && s <= 100)) { note.textContent = tr('predict.bad_share'); share.focus(); return; } onRun({ bits: b, share: s / 100 }); }; for (const i of [bits, share]) i.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); go(); } }); const node = el('form', { class: 'qs-predict', id: 'qs-predict', onsubmit: (e) => { e.preventDefault(); go(); } }, - el('div', { class: 'qs-k', text: 'Before it runs' }), - el('p', { class: 'qs-why', text: 'Which outcome do you expect most often, and in what share of the shots?' }), + el('div', { class: 'qs-k', text: tr('predict.before') }), + el('p', { class: 'qs-why', text: tr('predict.question') }), el('div', { class: 'qs-row' }, - el('label', { class: 'qs-field', for: 'qs-predict-bits' }, el('span', { text: 'Most likely outcome' }), bits), - el('label', { class: 'qs-field', for: 'qs-predict-share' }, el('span', { text: 'Its share, %' }), share)), + el('label', { class: 'qs-field', for: 'qs-predict-bits' }, el('span', { text: tr('predict.outcome_label') }), bits), + el('label', { class: 'qs-field', for: 'qs-predict-share' }, el('span', { text: tr('predict.share_label') }), share)), note, el('div', { class: 'qs-actions' }, - el('button', { class: 'qs-btn qs-primary', type: 'submit', id: 'qs-predict-run', text: 'Run' }), - el('button', { class: 'qs-btn', type: 'button', id: 'qs-predict-skip', text: 'Run without predicting', onclick: () => onRun(null) }))); + el('button', { class: 'qs-btn qs-primary', type: 'submit', id: 'qs-predict-run', text: tr('predict.run') }), + el('button', { class: 'qs-btn', type: 'button', id: 'qs-predict-skip', text: tr('predict.skip'), onclick: () => onRun(null) }))); setTimeout(() => bits.focus(), 0); return node; } @@ -64,19 +65,19 @@ export function form({ width, onRun }) { // The block that sets a prediction against the run, and records it. export function block(pred, result) { const c = compare(pred, result?.counts, result?.omitted_shots || 0); - const head = el('div', { class: 'qs-k', text: 'Your prediction' }); - if (!c) return el('div', { class: 'qs-block qs-predicted', id: 'qs-predicted' }, head, el('p', { class: 'qs-why', text: 'This run gave no counts to set it against: give the job shots.' })); + const head = el('div', { class: 'qs-k', text: tr('predict.yours') }); + if (!c) return el('div', { class: 'qs-block qs-predicted', id: 'qs-predicted' }, head, el('p', { class: 'qs-why', text: tr('predict.no_counts') })); const all = load(); all.push({ at: Date.now(), right: c.right, off: c.off }); save(all); const t = calibration(all); const verdict = c.right - ? `The outcome you named came out on top; your share was ${(100 * c.off).toFixed(1)} points off.` - : `Another outcome came out on top: ${c.top} in ${pct(c.topShare)} of the shots.`; + ? tr('predict.right', { points: (100 * c.off).toFixed(1) }) + : tr('predict.wrong', { top: c.top, share: pct(c.topShare) }); return el('div', { class: 'qs-block qs-predicted', id: 'qs-predicted' }, head, el('table', { class: 'qs-table' }, el('tbody', {}, - el('tr', {}, el('th', { scope: 'row', text: 'you said' }), el('td', { class: 'mono', text: `${c.bits} in ${pct(c.share)}` })), - el('tr', {}, el('th', { scope: 'row', text: 'the run' }), el('td', { class: 'mono', text: `${c.bits} in ${pct(c.got)}` })))), + el('tr', {}, el('th', { scope: 'row', text: tr('predict.you_said') }), el('td', { class: 'mono', text: tr('predict.bits_in', { bits: c.bits, share: pct(c.share) }) })), + el('tr', {}, el('th', { scope: 'row', text: tr('predict.the_run') }), el('td', { class: 'mono', text: tr('predict.bits_in', { bits: c.bits, share: pct(c.got) }) })))), el('p', { class: 'qs-why', id: 'qs-predict-verdict', text: verdict }), - el('p', { class: 'qs-note', id: 'qs-calibration', text: t.n === 1 ? 'Your first prediction kept on this device; each one after it adds to a tally here.' : `Over ${t.n} predictions kept on this device: you named the top outcome in ${t.right}, and your share was off by ${(100 * t.off).toFixed(1)} points on average.` })); + el('p', { class: 'qs-note', id: 'qs-calibration', text: t.n === 1 ? tr('predict.first') : tr('predict.tally', { n: t.n, right: t.right, points: (100 * t.off).toFixed(1) }) })); } diff --git a/wai/quantum-studio/src/qec-view.js b/wai/quantum-studio/src/qec-view.js new file mode 100644 index 000000000..4e4936869 --- /dev/null +++ b/wai/quantum-studio/src/qec-view.js @@ -0,0 +1,245 @@ +// The error-correction pane (engine::qec): a code drawn on its lattice, an error +// placed by clicking qubits, its syndrome lit on the checks, the decoder's +// correction marked and the residual judged (a stabilizer, or a logical error); +// the code's distance as its certificate states it, with a witness to show; and +// logical failure rates over distances, plotted against the physical rate. Every +// answer comes sealed with this browser's key (or the desktop user's), and the +// seal is checked here. The pane stays closed until it is opened. +import { el } from './dom.js'; + +const NS = 'http://www.w3.org/2000/svg'; +export const CODES = [ + ['surface:3', 'Surface code, d = 3'], + ['surface:5', 'Surface code, d = 5'], + ['surface:7', 'Surface code, d = 7'], + ['toric:4', 'Toric code, L = 4'], + ['steane', 'Steane code [[7,1,3]]'], + ['color:5', 'Colour code, d = 5'], + ['color:7', 'Colour code, d = 7'], + ['bb:72', 'Bivariate bicycle [[72,12,6]]'], + ['gross', 'Gross code [[144,12,12]]'], +]; +const DECODER_NAMES = { matching: 'exact matching', union_find: 'union-find', color: 'colour (restriction)', relay_bp: 'Relay-BP', search: 'exact search' }; + +function node(tag, attrs = {}, ...kids) { + const e = document.createElementNS(NS, tag); + for (const [k, v] of Object.entries(attrs)) if (v != null) e.setAttribute(k, String(v)); + for (const k of kids) if (k != null) e.append(k); + return e; +} + +// A check's outline: its qubits in angle order about their centre. +function outline(points) { + const cx = points.reduce((a, p) => a + p[0], 0) / points.length; + const cy = points.reduce((a, p) => a + p[1], 0) / points.length; + return points.slice().sort((a, b) => Math.atan2(a[1] - cy, a[0] - cx) - Math.atan2(b[1] - cy, b[0] - cx)); +} + +// The code on its lattice. `state` holds the error, the syndrome, the +// correction, the residual and a witness, each a set of indices; `onQubit(q)` +// hears a qubit chosen. +export function latticeSvg(code, state, onQubit) { + const L = code.layout; + // A triangle stands on its base: its rows run upward. + const up = L.kind === 'triangle' ? (p) => [p[0], -p[1]] : (p) => p; + const pts = L.qubits.map(up), checks = L.checks.map(up); + const all = pts.concat(checks); + const xs = all.map((p) => p[0]), ys = all.map((p) => p[1]); + const [x0, x1, y0, y1] = [Math.min(...xs), Math.max(...xs), Math.min(...ys), Math.max(...ys)]; + const pad = 0.8; + const unit = Math.max(x1 - x0, y1 - y0, 1); + const r = Math.max(0.12, Math.min(0.28, 4 / unit)); + const polygons = L.kind === 'grid' || L.kind === 'triangle'; + const svg = node('svg', { viewBox: `${x0 - pad} ${y0 - pad} ${x1 - x0 + 2 * pad} ${y1 - y0 + 2 * pad}`, class: 'qs-qec-svg', role: 'img', 'aria-label': `${code.name}: ${code.n} qubits and ${code.checks.length} checks${state.syndrome.size ? `, ${state.syndrome.size} violated` : ''}` }); + const faces = node('g', { class: 'qs-qec-checks' }); + code.checks.forEach((c, i) => { + const color = code.colors ? ` c${code.colors[i]}` : ''; + const lit = state.syndrome.has(i) ? ' on' : ''; + const label = `check ${i}${code.colors ? `, colour ${code.colors[i]}` : ''}: qubits ${c.join(', ')}${lit ? ', violated' : ''}`; + if (polygons && c.length >= 3) { + faces.append(node('polygon', { class: `qs-qec-face${color}${lit}`, points: outline(c.map((q) => pts[q])).map((p) => p.join(',')).join(' ') }, node('title', {}, label))); + } else if (polygons && c.length === 2) { + const [a, b] = c.map((q) => pts[q]); + faces.append(node('line', { class: `qs-qec-edge${color}${lit}`, x1: a[0], y1: a[1], x2: b[0], y2: b[1] }, node('title', {}, label))); + } else { + const [cx, cy] = checks[i]; + faces.append(node('rect', { class: `qs-qec-face${color}${lit}`, x: cx - r * 0.9, y: cy - r * 0.9, width: r * 1.8, height: r * 1.8, rx: r * 0.3 }, node('title', {}, label))); + } + }); + svg.append(faces); + const qubits = node('g', { class: 'qs-qec-qubits' }); + pts.forEach((p, q) => { + const cls = ['qs-qec-qubit']; + if (state.error.has(q)) cls.push('err'); + if (state.correction.has(q)) cls.push('fix'); + if (state.residual.has(q)) cls.push('res'); + if (state.witness.has(q)) cls.push('wit'); + const what = [state.error.has(q) && 'flipped', state.correction.has(q) && 'corrected', state.residual.has(q) && 'in the residual', state.witness.has(q) && 'in the witness'].filter(Boolean).join(', '); + const g = node('g', { class: cls.join(' '), tabindex: '0', role: 'button', 'data-q': q, 'aria-label': `qubit ${q}${what ? `: ${what}` : ''}` }, + node('circle', { cx: p[0], cy: p[1], r: r * 1.8, class: 'qs-qec-hit' }), + state.witness.has(q) ? node('circle', { cx: p[0], cy: p[1], r: r * 1.45, class: 'qs-qec-wit' }) : null, + node('circle', { cx: p[0], cy: p[1], r, class: 'qs-qec-dot' }), + node('title', {}, `qubit ${q}${what ? `: ${what}` : ''}`)); + g.addEventListener('click', () => onQubit(q)); + g.addEventListener('keydown', (ev) => { if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); onQubit(q); } }); + qubits.append(g); + }); + svg.append(qubits); + return svg; +} + +// Failure rate against the physical rate, one line per distance, rates on a log +// scale (decade gridlines); each point focusable, its numbers in its title. +export function thresholdSvg(points) { + const W = 520, H = 240, Lm = 64, R = 92, T = 14, B = 40; + const pos = points.filter((p) => p.rate > 0); + const ps = [...new Set(points.map((p) => p.p))].sort((a, b) => a - b); + const ds = [...new Set(points.map((p) => p.distance))].sort((a, b) => a - b); + const lo = pos.length ? Math.floor(Math.log10(Math.min(...pos.map((p) => p.rate)))) : -4; + const hi = pos.length ? Math.ceil(Math.log10(Math.max(...pos.map((p) => p.rate)))) : 0; + const [y0, y1] = hi - lo < 1 ? [lo - 1, hi] : [lo, hi]; + const [px0, px1] = ps.length > 1 ? [ps[0], ps[ps.length - 1]] : [ps[0] * 0.5, ps[0] * 1.5]; + const X = (v) => Lm + ((v - px0) / (px1 - px0 || 1)) * (W - Lm - R); + const Y = (rate) => H - B - ((Math.log10(Math.max(rate, 10 ** y0)) - y0) / (y1 - y0)) * (H - T - B); + const svg = node('svg', { viewBox: `0 0 ${W} ${H}`, class: 'qs-frontier qs-qec-plot', role: 'img', 'aria-label': `Logical failure rate against physical rate for distances ${ds.join(', ')}` }); + const grid = node('g', { class: 'qs-frontier-grid' }); + for (let d = y0; d <= y1; d++) { + grid.append(node('line', { x1: Lm, x2: W - R, y1: Y(10 ** d), y2: Y(10 ** d) })); + grid.append(node('text', { x: Lm - 8, y: Y(10 ** d), 'text-anchor': 'end', 'dominant-baseline': 'central', class: 'qs-frontier-tick' }, `1e${d}`)); + } + for (const p of ps) grid.append(node('text', { x: X(p), y: H - B + 16, 'text-anchor': 'middle', class: 'qs-frontier-tick' }, String(p))); + svg.append(grid); + svg.append(node('text', { x: (Lm + W - R) / 2, y: H - 6, 'text-anchor': 'middle', class: 'qs-frontier-axis' }, 'physical error rate')); + svg.append(node('text', { x: 14, y: (T + H - B) / 2, 'text-anchor': 'middle', transform: `rotate(-90 14 ${(T + H - B) / 2})`, class: 'qs-frontier-axis' }, 'logical failure rate')); + ds.forEach((d, k) => { + const line = points.filter((p) => p.distance === d).sort((a, b) => a.p - b.p); + const cls = `qs-qec-series s${k % 4}`; + if (line.length > 1) svg.append(node('polyline', { class: `${cls} qs-qec-line`, points: line.map((p) => `${X(p.p)},${Y(p.rate)}`).join(' ') })); + for (const p of line) { + svg.append(node('g', { class: `${cls} qs-frontier-point`, tabindex: '0', 'aria-label': `d = ${d}, p = ${p.p}: ${p.failures} failures in ${p.shots} shots, ${p.rate.toExponential(2)}` }, + node('circle', { cx: X(p.p), cy: Y(p.rate), r: 4, class: 'qs-qec-dot' }), + node('title', {}, `d = ${d}, p = ${p.p}: ${p.failures}/${p.shots} = ${p.rate.toExponential(2)} ± ${p.stderr.toExponential(1)}`))); + } + const last = line[line.length - 1]; + if (last) svg.append(node('text', { x: W - R + 8, y: Y(last.rate), 'dominant-baseline': 'central', class: `${cls} qs-qec-legend` }, `d = ${d}`)); + }); + return svg; +} + +// `engine` answers qec(op, args) and verify(body). +export function qecPane({ engine }) { + const code = el('select', { class: 'qs-input qs-small-input', id: 'qs-qec-code', 'aria-label': 'Code' }, ...CODES.map(([v, t]) => el('option', { value: v, text: t }))); + const decoder = el('select', { class: 'qs-input qs-small-input', id: 'qs-qec-decoder', 'aria-label': 'Decoder' }); + const decode = el('button', { class: 'qs-btn qs-small', id: 'qs-qec-decode', type: 'button', text: 'Decode', title: 'Decode the syndrome of the qubits you flipped, and judge the residual' }); + const clear = el('button', { class: 'qs-btn qs-small', id: 'qs-qec-clear', type: 'button', text: 'Clear' }); + const witness = el('button', { class: 'qs-btn qs-small', id: 'qs-qec-witness', type: 'button', text: 'Show witness', title: 'The certificate’s lightest logical operator, on the lattice' }); + const lattice = el('div', { class: 'qs-diagram qs-qec-lattice', id: 'qs-qec-lattice' }); + const summary = el('p', { class: 'qs-why', id: 'qs-qec-summary' }); + const cert = el('p', { class: 'qs-note', id: 'qs-qec-cert' }); + const result = el('p', { class: 'qs-note', id: 'qs-qec-result', text: 'Click qubits to flip them, then Decode.' }); + const family = el('select', { class: 'qs-input qs-small-input', id: 'qs-qec-family', 'aria-label': 'Threshold family' }, + el('option', { value: 'color', text: 'colour code, code capacity' }), el('option', { value: 'surface_capacity', text: 'surface code, code capacity' }), el('option', { value: 'surface', text: 'surface code, circuit level' })); + const shots = el('input', { class: 'qs-input qs-small-input', id: 'qs-qec-shots', type: 'text', value: '2000', inputmode: 'numeric', 'aria-label': 'Shots per point' }); + const measure = el('button', { class: 'qs-btn qs-small', id: 'qs-qec-measure', type: 'button', text: 'Measure', title: 'Logical failure rates at distances 3, 5 and 7 over three physical rates' }); + const plot = el('div', { class: 'qs-diagram qs-frontier-figure', id: 'qs-qec-plot' }); + const reading = el('p', { class: 'qs-note', id: 'qs-qec-reading' }); + // Closed until opened: the first opening reads the code and certifies it. + const details = el('details', { class: 'qs-qec-details', id: 'qs-qec' }, + el('summary', { class: 'qs-head' }, el('h2', { id: 'qs-qec-h', text: 'Error correction' }), el('span', { class: 'qs-note', text: 'a code and its distance, an error decoded, a threshold' })), + el('div', { class: 'qs-shortcuts' }, + el('div', { class: 'qs-shortcuts-head' }, code, decoder, decode, clear, witness), + summary, lattice, cert, result), + el('div', { class: 'qs-shortcuts' }, + el('div', { class: 'qs-shortcuts-head' }, el('span', { class: 'qs-k', text: 'Threshold' }), el('span', { class: 'qs-grow' }), family, el('label', { class: 'qs-inline', for: 'qs-qec-shots' }, el('span', { text: 'shots' }), shots), measure), + plot, reading)); + const pane = el('section', { class: 'qs-pane qs-qec', id: 'qs-qec-pane', 'aria-labelledby': 'qs-qec-h' }, details); + details.addEventListener('toggle', () => { if (details.open && !current && !busy) show(); }); + let current = null; + const state = { error: new Set(), syndrome: new Set(), correction: new Set(), residual: new Set(), witness: new Set() }; + let busy = false; + + const draw = () => { if (current) lattice.replaceChildren(latticeSvg(current.code, state, flip)); }; + // Flipping a qubit changes the error and its syndrome; an earlier decode no longer applies. + function flip(q) { + if (state.error.has(q)) state.error.delete(q); else state.error.add(q); + state.syndrome = syndromeOf(current.code, state.error); + state.correction.clear(); state.residual.clear(); + result.textContent = state.error.size ? `${state.error.size} qubit${state.error.size === 1 ? '' : 's'} flipped; ${state.syndrome.size} check${state.syndrome.size === 1 ? '' : 's'} violated. Decode to correct it.` : 'Click qubits to flip them, then Decode.'; + draw(); + } + async function verified(body) { + try { const v = await engine.verify(body); return v.valid === true; } catch (e) { return false; } + } + async function show() { + if (busy) return; + busy = true; + summary.textContent = `Reading ${code.value} and certifying its distance…`; + try { + const body = await engine.qec('code', { code: code.value }); + const c = body.result.code, k = body.result.certificate; + current = { code: c, certificate: k }; + for (const s of Object.values(state)) s.clear(); + decoder.replaceChildren(...c.decoders.map((d) => el('option', { value: d, text: DECODER_NAMES[d] ?? d }))); + summary.textContent = `${c.name}: [[${c.n}, ${c.k}, ${k?.distance ?? '?'}]], ${c.checks.length} Z checks (shown) and ${c.stabs.length} X stabilizers.`; + const ok = await verified(body); + cert.textContent = k ? `${k.reading} ${k.x.nodes + k.z.nodes} search nodes; ${ok ? 'the certificate is sealed and its seal verifies here' : 'its seal DOES NOT VERIFY here'}.` : 'Not certified.'; + result.textContent = 'Click qubits to flip them, then Decode.'; + draw(); + } catch (e) { summary.textContent = e.message; } + finally { busy = false; } + } + async function run() { + if (busy || !current) return; + busy = true; decode.disabled = true; + try { + const body = await engine.qec('decode', { code: current.code.name, decoder: decoder.value, error: [...state.error] }); + const r = body.result; + state.correction = new Set(r.correction); + // The residual is marked only when it is a logical error: a stabilizer residual is the decode working. + state.residual = new Set(r.residual?.logical ? r.residual.qubits : []); + const ok = await verified(body); + const judged = r.residual ? (r.residual.logical ? `The residual ${r.residual.qubits.length ? `on qubits ${r.residual.qubits.join(', ')} ` : ''}is a logical operator: a logical error.` : 'The residual is a product of stabilizers: the logical state is unharmed.') : ''; + result.textContent = `${DECODER_NAMES[r.decoder] ?? r.decoder}: correction on ${r.correction.length ? `qubits ${r.correction.join(', ')}` : 'no qubits'} (weight ${r.weight}), which ${r.explains ? 'explains' : 'DOES NOT explain'} the syndrome. ${judged} ${ok ? 'Sealed; the seal verifies here.' : 'The seal DOES NOT VERIFY here.'}`; + result.dataset.logical = r.residual?.logical ? 'yes' : 'no'; + draw(); + } catch (e) { result.textContent = e.message; } + finally { busy = false; decode.disabled = false; } + } + async function threshold() { + if (busy) return; + const n = parseInt(shots.value, 10); + if (!(n > 0 && n <= 50000)) { reading.textContent = 'Shots per point: a whole number from 1 to 50000 here.'; return; } + busy = true; measure.disabled = true; + const circuit = family.value === 'surface'; + reading.textContent = 'Sampling and decoding…'; + try { + const body = await engine.qec('threshold', { family: family.value, distances: circuit ? [3, 5] : [3, 5, 7], p: circuit ? [0.003, 0.006, 0.009] : [0.02, 0.05, 0.08], shots: n }); + const r = body.result; + plot.replaceChildren(thresholdSvg(r.points)); + const below = r.below_threshold.filter((b) => b.distance_helps).map((b) => b.p); + const fit = r.fit ? ` The code model fitted to these points: a = ${r.fit.prefactor.toFixed(4)}, p* = ${r.fit.threshold.toFixed(5)}.` : ''; + const ok = await verified(body); + reading.textContent = `${r.noise}; decoder ${r.decoder}. ${below.length ? `At p = ${below.join(', ')} every larger distance fails less: below this decoder’s threshold.` : 'At no rate here does every larger distance fail less.'}${fit} Each point has its standard error in its title; few shots make a crossing a guess. ${ok ? 'Sealed; the seal verifies here.' : 'The seal DOES NOT VERIFY here.'}`; + } catch (e) { reading.textContent = e.message; } + finally { busy = false; measure.disabled = false; } + } + code.addEventListener('change', show); + decode.addEventListener('click', run); + clear.addEventListener('click', () => { for (const s of Object.values(state)) s.clear(); result.textContent = 'Click qubits to flip them, then Decode.'; draw(); }); + witness.addEventListener('click', () => { + if (!current?.certificate) return; + const on = state.witness.size === 0; + state.witness = new Set(on ? current.certificate.x.witness : []); + witness.textContent = on ? 'Hide witness' : 'Show witness'; + draw(); + }); + measure.addEventListener('click', threshold); + return { node: pane, show }; +} + +// The checks an error flips. +export function syndromeOf(code, error) { + const s = new Set(); + code.checks.forEach((c, i) => { if (c.filter((q) => error.has(q)).length % 2 === 1) s.add(i); }); + return s; +} diff --git a/wai/quantum-studio/src/result-parts.js b/wai/quantum-studio/src/result-parts.js new file mode 100644 index 000000000..5f202badb --- /dev/null +++ b/wai/quantum-studio/src/result-parts.js @@ -0,0 +1,81 @@ +// The parts of a result as the Studio shows it, shared by the Studio's result +// pane and the views an agent host draws (src/views/): the method's name, its +// accuracy class in words, the counts as bars, the expectation values and the +// amplitudes as tables, and the energy figure with what it is. Every number is +// the result's own; these only place them. +import { el } from './dom.js'; +import { t as tr } from './i18n.js'; + +export const METHOD_NAMES = Object.fromEntries(['exact', 'statevector', 'statevector_single', 'stabilizer', 'pauli', 'mps', 'tensor_network', 'cutrank', 'cutrank_single', 'density_matrix', 'trajectories'].map((m) => [m, tr(`method.${m}`)])); +export const ACCURACY = Object.fromEntries(['bit-exact', 'exact', 'single', 'bounded', 'estimate', 'heuristic'].map((a) => [a, tr(`accuracy.${a}`)])); + +// A bit string short enough to read. +export const compact = (b) => (b.length <= 24 ? b : `${b.slice(0, 8)}…${b.slice(-8)} (${tr('frame.bits', { n: b.length })})`); + +// What the accuracy class leaves out, in words: a fidelity estimate, a +// discarded weight, error histories, the device, the widest cut, the global +// phase, and how a program that measures in the middle split its shots. +export function accuracyExtras(r) { + const acc = r.accuracy || {}; + const extra = []; + if (acc.fidelity_estimate !== undefined) extra.push(`fidelity estimate ${Number(acc.fidelity_estimate).toFixed(4)}`); + if (acc.discarded_weight !== undefined) extra.push(`discarded weight ${Number(acc.discarded_weight).toExponential(2)}`); + if (acc.histories !== undefined) extra.push(`${acc.histories} error histories`); + if (acc.device) extra.push(`on ${acc.device}`); + if (acc.max_cut !== undefined) extra.push(`the widest cut ${acc.max_cut} bits`); + if (r.global_phase) extra.push(`global phase ${r.global_phase}`); + const dy = r.dynamic; + if (dy) { + const parts = [`${dy.measurements_and_resets} measurements and resets`]; + if (dy.branches_run !== undefined) parts.push(`the shots split into ${dy.branches_run} branches`); + if (dy.branches_weighed !== undefined) parts.push(`the expectation values weigh ${dy.branches_weighed} branches exactly`); + extra.push(parts.join('; ')); + } + return extra; +} + +// The counts as bars, the commonest first, at most `most` of them. +export function countsBlock(r, most = 32) { + const entries = Object.entries(r.counts).sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1)); + const total = entries.reduce((s, [, n]) => s + n, 0) + (r.omitted_shots || 0); + const max = entries.length ? entries[0][1] : 1; + return el('div', { class: 'qs-block', id: 'qs-counts' }, el('div', { class: 'qs-k', text: tr('frame.counts', { total }) }), + el('div', { class: 'qs-bars' }, ...entries.slice(0, most).map(([k, n]) => el('div', { class: 'qs-bar', 'data-outcome': k, 'data-count': n }, + el('span', { class: 'qs-key', title: k, text: compact(k) }), + el('span', { class: 'qs-track' }, el('span', { class: 'qs-fill', style: `width:${(100 * n) / max}%` })), + el('span', { class: 'qs-n', text: `${n} · ${((100 * n) / total).toFixed(1)}%` })))), + entries.length > most ? el('p', { class: 'qs-note', text: tr('frame.more_outcomes', { n: entries.length - most }) }) : null); +} + +// The expectation values, each with its standard error or its bound. +export function expectationsBlock(r) { + return el('div', { class: 'qs-block', id: 'qs-expectations' }, el('div', { class: 'qs-k', text: tr('frame.expectations') }), + el('table', { class: 'qs-table' }, el('tbody', {}, ...r.expectations.map((e) => el('tr', {}, + el('th', { scope: 'row', text: `⟨${e.observable}⟩` }), el('td', { class: 'num', text: Number(e.value).toFixed(6) }), + el('td', { class: 'qs-note', text: e.stderr !== undefined ? `± ${Number(e.stderr).toExponential(1)}` : e.bound !== undefined ? `error ≤ ${Number(e.bound).toExponential(1)}` : '' })))))); +} + +// The amplitudes, each with its probability and phase. +export function amplitudesBlock(r) { + return el('div', { class: 'qs-block', id: 'qs-amplitudes' }, el('div', { class: 'qs-k', text: tr('frame.amplitudes') }), + el('table', { class: 'qs-table' }, el('thead', {}, el('tr', {}, el('th', { text: tr('frame.th_state') }), el('th', { text: tr('frame.th_amplitude') }), el('th', { text: tr('frame.th_probability') }), el('th', { text: tr('frame.th_phase') }))), + el('tbody', {}, ...r.amplitudes.map((a) => { + const p = a.re * a.re + a.im * a.im; + const ph = Math.atan2(a.im, a.re) / Math.PI; + return el('tr', {}, el('th', { scope: 'row', text: `|${compact(a.bits)}⟩` }), + el('td', { class: 'num', text: `${Number(a.re).toFixed(6)} ${a.im < 0 ? '−' : '+'} ${Math.abs(a.im).toFixed(6)}i` }), + el('td', { class: 'num', text: p.toFixed(6) }), el('td', { class: 'num', text: p < 1e-24 ? '—' : `${ph.toFixed(4)}π` })); + })))); +} + +// A figure is shown with its class, its coverage and its declared uncertainty, +// as the host states them, so a reading is never shown without what it is +// (energy-measurement §2); no figure is shown as unmetered, never as 0 J. +export function energyText(en = {}) { + const amount = (j) => j < 1e3 ? j + ' µJ' : j < 1e6 ? (j / 1e3).toFixed(1) + ' mJ' : (j / 1e6).toFixed(2) + ' J'; + const ppm = en.energy_uncertainty && en.energy_uncertainty.relative_ppm; + return en.joules_micro && en.energy_provenance + ? `${amount(en.joules_micro)} (${en.energy_provenance}${en.coverage ? ', ' + en.coverage : ''}${ppm ? ', ±' + (ppm / 1e4).toFixed(ppm < 1e4 ? 2 : 1) + ' %' : ''})` + : en.joules_micro ? `${amount(en.joules_micro)} (no class stated: not a reading)` + : en.unmetered ? `unmetered: ${en.unmetered}` : 'unmetered'; +} diff --git a/wai/quantum-studio/src/rewrite-view.js b/wai/quantum-studio/src/rewrite-view.js index 786743661..b85cedaa5 100644 --- a/wai/quantum-studio/src/rewrite-view.js +++ b/wai/quantum-studio/src/rewrite-view.js @@ -7,28 +7,29 @@ // reviewed like any other, so the reader makes the optimisation one checked // step at a time. import { el } from './dom.js'; +import { t } from './i18n.js'; const ORDER = ['cancel', 'merge', 'through', 'conjugate', 'drop', 'move']; -const WORDS = { cancel: 'cancel', merge: 'merge', through: 'carry', conjugate: 'conjugate', drop: 'drop', move: 'move' }; +const WORDS = { cancel: t('rewrite_view.rule_cancel'), merge: t('rewrite_view.rule_merge'), through: t('rewrite_view.rule_through'), conjugate: t('rewrite_view.rule_conjugate'), drop: t('rewrite_view.rule_drop'), move: t('rewrite_view.rule_move') }; // `engine` answers rewrite(name, args); `program()` is the editor's text; // `onMark(ops)` marks gates on the diagram; `offer(qasm, note)` proposes a program. export function rewritePanel({ engine, program, onMark, offer }) { - const ask = el('button', { class: 'qs-btn qs-small', id: 'qs-rewrites', type: 'button', text: 'Rewrites', title: 'Every rewrite that keeps the program’s meaning, each checked by the engine' }); + const ask = el('button', { class: 'qs-btn qs-small', id: 'qs-rewrites', type: 'button', text: t('rewrite_view.rewrites'), title: t('rewrite_view.rewrites_title') }); const body = el('div', { class: 'qs-shortcuts-body', id: 'qs-rewrites-body' }); const node = el('div', { class: 'qs-shortcuts qs-rewrites', id: 'qs-rewrites-panel' }, - el('div', { class: 'qs-shortcuts-head' }, el('span', { class: 'qs-k', text: 'Rewrites' }), el('span', { class: 'qs-grow' }), ask), + el('div', { class: 'qs-shortcuts-head' }, el('span', { class: 'qs-k', text: t('rewrite_view.heading') }), el('span', { class: 'qs-grow' }), ask), body); let checkedFor = null; let busy = false; function row(o) { - const take = el('button', { class: 'qs-btn qs-small qs-rewrite-take', type: 'button', text: 'Take', onclick: () => offer(o.result, `A rewrite, checked by the engine: ${o.why}.`) }); + const take = el('button', { class: 'qs-btn qs-small qs-rewrite-take', type: 'button', text: t('rewrite_view.take'), onclick: () => offer(o.result, t('rewrite_view.offer_note', { why: o.why })) }); const lines = [...new Set(o.lines)]; const li = el('li', { 'data-id': o.id, 'data-rule': o.rule, tabindex: '0' }, el('span', { class: 'qs-chip', text: WORDS[o.rule] ?? o.rule }), el('span', { class: 'qs-rewrite-why', text: o.why }), - el('span', { class: 'qs-note', text: `line${lines.length === 1 ? '' : 's'} ${lines.join(', ')}` }), + el('span', { class: 'qs-note', text: lines.length === 1 ? t('rewrite_view.lines_one', { lines: lines.join(', ') }) : t('rewrite_view.lines_other', { lines: lines.join(', ') }) }), take); for (const ev of ['mouseenter', 'focusin']) li.addEventListener(ev, () => onMark(o.gates)); for (const ev of ['mouseleave', 'focusout']) li.addEventListener(ev, () => onMark([])); @@ -40,18 +41,18 @@ export function rewritePanel({ engine, program, onMark, offer }) { busy = true; ask.disabled = true; const src = program(); node.classList.remove('stale'); - if (checkedFor == null) body.replaceChildren(el('p', { class: 'qs-note', text: 'The engine is checking each rewrite…' })); + if (checkedFor == null) body.replaceChildren(el('p', { class: 'qs-note', text: t('rewrite_view.checking') })); try { const v = await engine.rewrite('options', { qasm: src }); checkedFor = src; onMark([]); - if (v.unread) { body.replaceChildren(el('p', { class: 'qs-note', text: `The engine cannot read the program: ${v.unread}` })); return; } + if (v.unread) { body.replaceChildren(el('p', { class: 'qs-note', text: t('rewrite_view.unread', { error: v.unread }) })); return; } const opts = [...(v.options ?? [])].sort((a, b) => ORDER.indexOf(a.rule) - ORDER.indexOf(b.rule)); body.replaceChildren(...[ v.note ? el('p', { class: 'qs-note', text: `${v.note[0].toUpperCase()}${v.note.slice(1)}.` }) : null, opts.length ? el('ol', { class: 'qs-rewrite-list', id: 'qs-rewrite-list' }, ...opts.map(row)) : null, - !opts.length && !v.note ? el('p', { class: 'qs-note', id: 'qs-rewrite-none', text: 'No rewrite applies: no gates cancel, merge or conjugate, and no two that commute sit side by side.' }) : null, - opts.length ? el('p', { class: 'qs-note', text: 'Each keeps the circuit’s meaning: the engine checks the gates a rewrite takes against the gates it puts, on the qubits they touch. Take one at a time; the review shows the change.' }) : null].filter(Boolean)); + !opts.length && !v.note ? el('p', { class: 'qs-note', id: 'qs-rewrite-none', text: t('rewrite_view.none') }) : null, + opts.length ? el('p', { class: 'qs-note', text: t('rewrite_view.note') }) : null].filter(Boolean)); if (program() !== src) stale(program()); } catch (e) { body.replaceChildren(el('p', { class: 'qs-note', text: e.message })); } finally { busy = false; ask.disabled = false; } diff --git a/wai/quantum-studio/src/rules.js b/wai/quantum-studio/src/rules.js index 4c749eafb..2f804542b 100644 --- a/wai/quantum-studio/src/rules.js +++ b/wai/quantum-studio/src/rules.js @@ -8,6 +8,7 @@ import { Decoration, ViewPlugin } from '@codemirror/view'; import { RangeSetBuilder } from '@codemirror/state'; import { el } from './dom.js'; +import { t as tr } from './i18n.js'; const SAVED = 'wai.quantum-studio.rules'; export const LOCK = '// @lock'; @@ -53,21 +54,21 @@ export function rulesPane({ selection, hold, changed }) { const input = el('input', { class: 'qs-input', id, value, placeholder, spellcheck: 'false', autocomplete: 'off' }); return [input, el('label', { class: 'qs-field', for: id }, el('span', { text: label }), input)]; }; - const [gates, gatesF] = field('qs-rule-gates', 'Gates a proposal may add', rules.gates, 'any'); - const [avoidG, avoidGF] = field('qs-rule-avoid', 'Gates to avoid', rules.avoid_gates, 'none'); - const [avoidT, avoidTF] = field('qs-rule-text', 'Text to avoid', rules.avoid_text, 'none, or text, one per comma'); + const [gates, gatesF] = field('qs-rule-gates', tr('rules.gates'), rules.gates, tr('rules.gates_any')); + const [avoidG, avoidGF] = field('qs-rule-avoid', tr('rules.avoid_gates'), rules.avoid_gates, tr('rules.none')); + const [avoidT, avoidTF] = field('qs-rule-text', tr('rules.avoid_text'), rules.avoid_text, tr('rules.avoid_text_placeholder')); const qubits = el('input', { type: 'checkbox', id: 'qs-rule-qubits', checked: !!rules.qubits }); - const holdBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-rule-hold', type: 'button', text: 'Hold the selected lines', title: `Mark them with ${LOCK} and ${UNLOCK}: no proposal may change them` }); - const regionBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-rule-region', type: 'button', text: 'Keep the next proposal to the selected lines' }); + const holdBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-rule-hold', type: 'button', text: tr('rules.hold'), title: tr('rules.hold_title', { lock: LOCK, unlock: UNLOCK }) }); + const regionBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-rule-region', type: 'button', text: tr('rules.region') }); const regionChip = el('span', { class: 'qs-chip', id: 'qs-rule-region-chip', hidden: true }); const count = el('span', { class: 'qs-badge', id: 'qs-rules-count' }); const node = el('details', { class: 'qs-rules', id: 'qs-rules' }, - el('summary', {}, el('span', { text: 'Rules for proposals' }), count), + el('summary', {}, el('span', { text: tr('rules.title') }), count), el('div', { class: 'qs-rules-body' }, el('div', { class: 'qs-actions' }, holdBtn, regionBtn, regionChip), el('div', { class: 'qs-row' }, gatesF, avoidGF), - el('div', { class: 'qs-row' }, avoidTF, el('label', { class: 'qs-check', for: 'qs-rule-qubits' }, qubits, el('span', { text: 'Hold the qubit declarations' }))), - el('p', { class: 'qs-note', text: `The engine checks every proposal against these, change by change: the assistant's, an alternative, a fix, an agent's. A change that breaks one cannot be taken. Lines between ${LOCK} and ${UNLOCK} are held. What you type is yours to change.` }))); + el('div', { class: 'qs-row' }, avoidTF, el('label', { class: 'qs-check', for: 'qs-rule-qubits' }, qubits, el('span', { text: tr('rules.qubits') }))), + el('p', { class: 'qs-note', text: tr('rules.note', { lock: LOCK, unlock: UNLOCK }) }))); // The rules as the engine reads them. function locks() { @@ -86,8 +87,8 @@ export function rulesPane({ selection, hold, changed }) { count.className = n ? 'qs-badge ok' : 'qs-badge'; regionChip.hidden = !rules.region; if (rules.region) { - regionChip.replaceChildren(el('span', { text: `Only lines ${rules.region[0]} to ${rules.region[1]}, for the next proposal ` }), - el('button', { type: 'button', class: 'qs-chip-x', 'aria-label': 'Keep to no lines', text: '✕', onclick: () => setRegion(null) })); + regionChip.replaceChildren(el('span', { text: tr('rules.only_lines', { first: rules.region[0], last: rules.region[1] }) }), + el('button', { type: 'button', class: 'qs-chip-x', 'aria-label': tr('rules.clear_region'), text: '✕', onclick: () => setRegion(null) })); } } function set(k, v) { rules[k] = v; keep(); show(); changed?.(); } diff --git a/wai/quantum-studio/src/shortcuts-view.js b/wai/quantum-studio/src/shortcuts-view.js index 6cd9a583c..9afd3e415 100644 --- a/wai/quantum-studio/src/shortcuts-view.js +++ b/wai/quantum-studio/src/shortcuts-view.js @@ -4,55 +4,56 @@ // thorough one, with its contraction search, can take minutes. A cheap route // proves the design classically easy; none being cheap proves nothing. import { el } from './dom.js'; +import { t } from './i18n.js'; -const NAMES = { stabilizer: 'Stabilizer tableau', cutrank: 'Cut-rank sum', mps: 'Exact matrix product state', light_cone: 'Light cone', tensor_network: 'Tensor network', statevector: 'State vector' }; +const NAMES = { stabilizer: t('shortcuts_view.route_stabilizer'), cutrank: t('shortcuts_view.route_cutrank'), mps: t('shortcuts_view.route_mps'), light_cone: t('shortcuts_view.route_light_cone'), tensor_network: t('shortcuts_view.route_tensor_network'), statevector: t('shortcuts_view.route_statevector') }; const bytes = (b) => { if (b >= 2 ** 70) return `2^${Math.log2(b).toFixed(1)} B`; for (const [u, s] of [['EiB', 60], ['PiB', 50], ['TiB', 40], ['GiB', 30], ['MiB', 20], ['KiB', 10]]) if (b >= 2 ** s) return `${(b / 2 ** s).toFixed(b / 2 ** s < 10 ? 1 : 0)} ${u}`; return `${Math.round(b)} B`; }; -const time = (s) => (s < 1e-3 ? 'under 1 ms' : s < 1 ? `${Math.round(s * 1e3)} ms` : s < 120 ? `${Math.round(s)} s` : s < 7200 ? `${Math.round(s / 60)} min` : s < 172800 ? `${Math.round(s / 3600)} h` : s < 6.3e7 ? `${Math.round(s / 86400)} days` : `${(s / 3.15e7).toExponential(1)} years`); +const time = (s) => (s < 1e-3 ? t('shortcuts_view.under_ms') : s < 1 ? `${Math.round(s * 1e3)} ms` : s < 120 ? `${Math.round(s)} s` : s < 7200 ? `${Math.round(s / 60)} min` : s < 172800 ? `${Math.round(s / 3600)} h` : s < 6.3e7 ? t('shortcuts_view.days', { n: Math.round(s / 86400) }) : t('shortcuts_view.years', { n: (s / 3.15e7).toExponential(1) })); const size = (r) => { const z = r.size || {}; - if (z.widest_cut != null) return `widest cut ${z.widest_cut} bits`; - if (z.log2_bond != null) return z.log2_bond === 0 ? 'a product state' : `bond ≤ 2^${z.log2_bond}`; - if (z.widest != null) return `cone of ${z.widest} qubits`; - if (z.log2_largest_tensor != null) return `largest tensor 2^${z.log2_largest_tensor}`; - if (z.log2_amplitudes != null) return `2^${z.log2_amplitudes} amplitudes`; - if (z.tableau_bits != null) return 'a tableau'; + if (z.widest_cut != null) return t('shortcuts_view.size_widest_cut', { n: z.widest_cut }); + if (z.log2_bond != null) return z.log2_bond === 0 ? t('shortcuts_view.size_product_state') : t('shortcuts_view.size_bond', { n: z.log2_bond }); + if (z.widest != null) return t('shortcuts_view.size_cone', { n: z.widest }); + if (z.log2_largest_tensor != null) return t('shortcuts_view.size_largest_tensor', { n: z.log2_largest_tensor }); + if (z.log2_amplitudes != null) return t('shortcuts_view.size_amplitudes', { n: z.log2_amplitudes }); + if (z.tableau_bits != null) return t('shortcuts_view.size_tableau'); return ''; }; // `engine` answers shortcuts(qasm, thorough); `program()` is the editor's text. export function shortcutsPanel({ engine, program }) { - const quick = el('button', { class: 'qs-btn qs-small', id: 'qs-shortcuts', type: 'button', text: 'Classical cost', title: 'Every exact classical route the engine knows for this program, with its cost' }); - const deep = el('button', { class: 'qs-btn qs-small', id: 'qs-shortcuts-thorough', type: 'button', text: 'Thorough', title: 'Also search a contraction tree and a longer cut-rank order: seconds to minutes on a large program' }); + const quick = el('button', { class: 'qs-btn qs-small', id: 'qs-shortcuts', type: 'button', text: t('shortcuts_view.classical_cost'), title: t('shortcuts_view.classical_cost_title') }); + const deep = el('button', { class: 'qs-btn qs-small', id: 'qs-shortcuts-thorough', type: 'button', text: t('shortcuts_view.thorough'), title: t('shortcuts_view.thorough_title') }); const body = el('div', { class: 'qs-shortcuts-body', id: 'qs-shortcuts-body' }); const node = el('div', { class: 'qs-shortcuts', id: 'qs-shortcuts-panel' }, - el('div', { class: 'qs-shortcuts-head' }, el('span', { class: 'qs-k', text: 'Classically' }), el('span', { class: 'qs-grow' }), quick, deep), + el('div', { class: 'qs-shortcuts-head' }, el('span', { class: 'qs-k', text: t('shortcuts_view.heading') }), el('span', { class: 'qs-grow' }), quick, deep), body); let checkedFor = null; let busy = false; function render(v) { - if (!v.ok) { body.replaceChildren(el('p', { class: 'qs-note', text: `The engine cannot read the program: ${v.error}` })); return; } + if (!v.ok) { body.replaceChildren(el('p', { class: 'qs-note', text: t('shortcuts_view.unread', { error: v.error }) })); return; } const best = v.cheapest?.counts?.route; const rows = v.routes.map((r) => { const costed = r.applies && r.log2_work != null; return el('tr', { class: [r.applies ? '' : 'off', r.route === best ? 'best' : ''].filter(Boolean).join(' '), 'data-route': r.route, title: r.why }, el('th', { scope: 'row', text: NAMES[r.route] || r.route }), - el('td', { text: r.applies ? size(r) : 'does not apply' }), + el('td', { text: r.applies ? size(r) : t('shortcuts_view.does_not_apply') }), el('td', { class: 'num', text: costed ? bytes(r.memory_bytes) : '' }), - el('td', { class: 'num', text: costed ? `2^${Number(r.log2_work).toFixed(1)}` : r.applies ? 'not searched' : '' }), + el('td', { class: 'num', text: costed ? `2^${Number(r.log2_work).toFixed(1)}` : r.applies ? t('shortcuts_view.not_searched') : '' }), el('td', { class: 'num', text: costed ? time(r.seconds_at_reference) : '' })); }); body.replaceChildren( el('p', { class: 'qs-why', id: 'qs-shortcuts-summary', text: v.summary }), el('div', { class: 'qs-scroll' }, el('table', { class: 'qs-table qs-routes', id: 'qs-routes' }, - el('thead', {}, el('tr', {}, el('th', { text: 'route' }), el('th', { text: 'size' }), el('th', { class: 'num', text: 'memory' }), el('th', { class: 'num', text: 'operations' }), el('th', { class: 'num', text: 'at 10¹⁰/s' }))), + el('thead', {}, el('tr', {}, el('th', { text: t('shortcuts_view.col_route') }), el('th', { text: t('shortcuts_view.col_size') }), el('th', { class: 'num', text: t('shortcuts_view.col_memory') }), el('th', { class: 'num', text: t('shortcuts_view.col_operations') }), el('th', { class: 'num', text: t('shortcuts_view.col_rate') }))), el('tbody', {}, ...rows))), - el('p', { class: 'qs-note', text: `Each route is exact; operations are to a first sample, amplitude or expectation value. The times are at a reference rate, not a measurement. ${v.note[0].toUpperCase()}${v.note.slice(1)}.` })); + el('p', { class: 'qs-note', text: t('shortcuts_view.note', { note: `${v.note[0].toUpperCase()}${v.note.slice(1)}` }) })); } async function check(thorough) { @@ -60,7 +61,7 @@ export function shortcutsPanel({ engine, program }) { busy = true; quick.disabled = deep.disabled = true; const src = program(); node.classList.remove('stale'); - body.replaceChildren(el('p', { class: 'qs-note', text: thorough ? 'Searching each route, a contraction tree among them: this can take minutes on a large program…' : 'Pricing each route…' })); + body.replaceChildren(el('p', { class: 'qs-note', text: thorough ? t('shortcuts_view.searching') : t('shortcuts_view.pricing') })); try { const v = await engine.shortcuts(src, thorough); checkedFor = src; @@ -75,7 +76,7 @@ export function shortcutsPanel({ engine, program }) { const old = src !== checkedFor; node.classList.toggle('stale', old); body.querySelector('.qs-stale-note')?.remove(); - if (old) body.prepend(el('p', { class: 'qs-note qs-stale-note', text: 'For an earlier version of the program: check again.' })); + if (old) body.prepend(el('p', { class: 'qs-note qs-stale-note', text: t('shortcuts_view.stale') })); } quick.addEventListener('click', () => check(false)); deep.addEventListener('click', () => check(true)); diff --git a/wai/quantum-studio/src/state-views.js b/wai/quantum-studio/src/state-views.js index e84827e60..8ba461a3f 100644 --- a/wai/quantum-studio/src/state-views.js +++ b/wai/quantum-studio/src/state-views.js @@ -11,6 +11,7 @@ // overlay under noise, the noisy probabilities against the noiseless // ones on the same path, with the fidelity between them. import { el } from './dom.js'; +import { t as tr } from './i18n.js'; const NS = 'http://www.w3.org/2000/svg'; const svgEl = (tag, attrs = {}, ...kids) => { @@ -46,22 +47,22 @@ function disk({ size = 36, fill = 0, angle = null, hand = 1, ring = null, label export function amplitudeTable(state) { const rows = (state.amplitudes ?? []).map((a) => el('tr', {}, el('th', { scope: 'row', class: 'mono', text: `|${a.bits}⟩` }), - el('td', {}, disk({ size: 26, fill: a.p, angle: Math.PI * a.phase, hand: Math.sqrt(a.p), label: `probability ${f(a.p)}, phase ${f(a.phase)}π` })), + el('td', {}, disk({ size: 26, fill: a.p, angle: Math.PI * a.phase, hand: Math.sqrt(a.p), label: tr('state_views.amp_label', { p: f(a.p), phase: f(a.phase) }) })), el('td', { class: 'num', text: `${signed(a.re, 4)} ${a.im < 0 ? '−' : '+'} ${Math.abs(a.im).toFixed(4)}i` }), el('td', { class: 'num', text: f(a.p, 4) }), el('td', { class: 'num', text: `${f(a.phase, 4)}π` }))); const probs = (state.probabilities ?? []).map((a) => el('tr', {}, el('th', { scope: 'row', class: 'mono', text: `|${a.bits}⟩` }), - el('td', {}, disk({ size: 26, fill: a.p, label: `probability ${f(a.p)}` })), + el('td', {}, disk({ size: 26, fill: a.p, label: tr('state_views.prob_label', { p: f(a.p) }) })), el('td', { class: 'num', text: '—' }), el('td', { class: 'num', text: f(a.p, 4) }), el('td', { class: 'num', text: '—' }))); const listed = rows.length || probs.length; return el('div', { class: 'qs-dbg-amps' }, - el('table', { class: 'qs-table', id: 'qs-debug-amps', 'aria-label': 'Amplitudes at this step' }, - el('thead', {}, el('tr', {}, el('th', { text: 'state' }), el('th', { text: '' }), el('th', { text: 'amplitude' }), el('th', { text: 'probability' }), el('th', { text: 'phase' }))), + el('table', { class: 'qs-table', id: 'qs-debug-amps', 'aria-label': tr('state_views.amps_label') }, + el('thead', {}, el('tr', {}, el('th', { text: tr('state_views.col_state') }), el('th', { text: '' }), el('th', { text: tr('state_views.col_amplitude') }), el('th', { text: tr('state_views.col_probability') }), el('th', { text: tr('state_views.col_phase') }))), el('tbody', {}, ...rows, ...probs)), - el('p', { class: 'qs-note', text: `${listed} of ${state.nonzero} non-zero basis states listed, of ${state.of}; purity ${f(state.purity, 6)}${probs.length ? ' (a mixed state: probabilities, no amplitudes)' : ''}; digest ${String(state.digest || '').slice(0, 16)}…` })); + el('p', { class: 'qs-note', text: tr('state_views.amps_note', { listed, nonzero: state.nonzero, of: state.of, purity: f(state.purity, 6), mixed: probs.length ? tr('state_views.mixed') : '', digest: String(state.digest || '').slice(0, 16) }) })); } // A point on or in the unit sphere as the reader sees it, from azimuth −35° and elevation 20°: (right, up). @@ -75,7 +76,7 @@ function project([x, y, z]) { export function blochSphere(b, size = 110) { const r = size * 0.36, cx = size / 2, cy = size / 2 + 2; const pt = (v) => { const [px, py] = project(v); return [cx + r * px, cy - r * py]; }; - const s = svgEl('svg', { width: size, height: size, viewBox: `0 0 ${size} ${size}`, class: 'qs-bloch', role: 'img', 'aria-label': `Qubit ${b.qubit}: Bloch vector (${f(b.x)}, ${f(b.y)}, ${f(b.z)}), length ${f(b.length)}` }); + const s = svgEl('svg', { width: size, height: size, viewBox: `0 0 ${size} ${size}`, class: 'qs-bloch', role: 'img', 'aria-label': tr('state_views.bloch_label', { qubit: b.qubit, x: f(b.x), y: f(b.y), z: f(b.z), length: f(b.length) }) }); s.append(svgEl('circle', { cx, cy, r: r.toFixed(2), class: 'qs-bloch-edge' })); const eq = []; for (let k = 0; k <= 48; k++) { const t = (2 * Math.PI * k) / 48; const [x, y] = pt([Math.cos(t), Math.sin(t), 0]); eq.push(`${x.toFixed(2)},${y.toFixed(2)}`); } @@ -98,33 +99,33 @@ export function blochSphere(b, size = 110) { export function qubitCards(bloch, ideal) { return el('div', { class: 'qs-dbg-qubits', id: 'qs-debug-qubits' }, ...bloch.map((b) => { const transverse = Math.hypot(b.x, b.y); - const phase = disk({ size: 44, fill: b.p1, angle: transverse > 1e-9 ? Math.atan2(b.y, b.x) : null, hand: transverse, ring: (b.purity - 0.5) / 0.5, label: `Qubit ${b.qubit}: probability of 1 ${f(b.p1)}, purity ${f(b.purity)}` }); + const phase = disk({ size: 44, fill: b.p1, angle: transverse > 1e-9 ? Math.atan2(b.y, b.x) : null, hand: transverse, ring: (b.purity - 0.5) / 0.5, label: tr('state_views.qubit_label', { qubit: b.qubit, prob: f(b.p1), purity: f(b.purity) }) }); const twin = ideal?.find((x) => x.qubit === b.qubit); return el('figure', { class: 'qs-dbg-qubit', 'data-q': b.qubit }, el('div', { class: 'qs-dbg-pics' }, phase, blochSphere(b)), el('figcaption', {}, el('b', { text: `q${b.qubit}` }), el('span', { class: 'mono', text: ` x ${signed(b.x)} · y ${signed(b.y)} · z ${signed(b.z)}` }), - el('span', { class: 'qs-note', text: ` p(1) ${f(b.p1)} · purity ${f(b.purity)}${twin ? ` · without noise z ${signed(twin.z)}` : ''}` }))); + el('span', { class: 'qs-note', text: tr('state_views.caption', { prob: f(b.p1), purity: f(b.purity), twin: twin ? tr('state_views.without_noise_z', { z: signed(twin.z) }) : '' }) }))); })); } // The reduced density matrix the probe asked for. export function densityView(reduced) { - if (!reduced) return el('p', { class: 'qs-note', text: 'Choose up to six qubits for their reduced state.' }); + if (!reduced) return el('p', { class: 'qs-note', text: tr('state_views.choose_qubits') }); const m = reduced.matrix; const d = m.length; const k = Math.log2(d); const label = (i) => `|${i.toString(2).padStart(k, '0')}⟩`; const max = Math.max(1e-12, ...m.flat().map(([re, im]) => Math.hypot(re, im))); - const grid = el('table', { class: 'qs-dbg-rho', id: 'qs-debug-rho', 'aria-label': `The reduced density matrix of qubits ${reduced.qubits.join(', ')}` }, + const grid = el('table', { class: 'qs-dbg-rho', id: 'qs-debug-rho', 'aria-label': tr('state_views.rho_label', { qubits: reduced.qubits.join(', ') }) }, el('thead', {}, el('tr', {}, el('th', { text: '' }), ...m.map((_, j) => el('th', { class: 'mono', text: label(j) })))), el('tbody', {}, ...m.map((row, i) => el('tr', {}, el('th', { class: 'mono', scope: 'row', text: label(i) }), ...row.map(([re, im]) => { const mag = Math.hypot(re, im); return el('td', { class: 'num', style: `--qs-mag:${(mag / max).toFixed(3)}`, title: `${signed(re, 4)} ${im < 0 ? '−' : '+'} ${Math.abs(im).toFixed(4)}i`, text: mag < 5e-5 ? '·' : f(mag, 3) }); }))))); return el('div', { class: 'qs-dbg-density' }, grid, - el('p', { class: 'qs-note', id: 'qs-debug-rho-note', text: `qubits ${reduced.qubits.join(', ')} (the first the most significant): purity ${f(reduced.purity, 6)}, entropy ${f(reduced.entropy_bits, 6)} bits. Each entry's size is shown; hover for its value.` })); + el('p', { class: 'qs-note', id: 'qs-debug-rho-note', text: tr('state_views.rho_note', { qubits: reduced.qubits.join(', '), purity: f(reduced.purity, 6), entropy: f(reduced.entropy_bits, 6) }) })); } // Under noise: the noisy probabilities, each with the noiseless one on the same path. @@ -139,8 +140,8 @@ export function overlay(state, ideal) { const a = noisy.get(k) ?? 0, b = clean.get(k) ?? 0; return el('div', { class: 'qs-bar' }, el('span', { class: 'qs-key', text: k }), - el('span', { class: 'qs-track' }, el('span', { class: 'qs-fill', style: `width:${(100 * a).toFixed(2)}%` }), el('span', { class: 'qs-ideal', style: `left:${(100 * b).toFixed(2)}%`, title: `without noise ${f(b, 4)}` })), - el('span', { class: 'qs-n', text: `${f(a, 4)} · without noise ${f(b, 4)}` })); + el('span', { class: 'qs-track' }, el('span', { class: 'qs-fill', style: `width:${(100 * a).toFixed(2)}%` }), el('span', { class: 'qs-ideal', style: `left:${(100 * b).toFixed(2)}%`, title: tr('state_views.without_noise', { p: f(b, 4) }) })), + el('span', { class: 'qs-n', text: `${f(a, 4)} · ${tr('state_views.without_noise', { p: f(b, 4) })}` })); })), - el('p', { class: 'qs-note', text: `The bars are the noisy state's probabilities; each mark is the noiseless state's on the same path. Fidelity to it ${f(ideal.fidelity, 6)}.` })); + el('p', { class: 'qs-note', text: tr('state_views.overlay_note', { fidelity: f(ideal.fidelity, 6) }) })); } diff --git a/wai/quantum-studio/src/studio.css b/wai/quantum-studio/src/studio.css index 2af6f04e2..6acb0754a 100644 --- a/wai/quantum-studio/src/studio.css +++ b/wai/quantum-studio/src/studio.css @@ -535,3 +535,88 @@ .qs-dev-ink0, .qs-dev-ink1 { fill: var(--qs-dev-ink-lo); } .qs-dev-ink2, .qs-dev-ink3, .qs-dev-ink4 { fill: var(--qs-dev-ink-hi); } .qs-dev-inkn { fill: var(--text-secondary); } + +/* The error-correction pane: the code's checks as faces (a colour code's in its + three colours), violated checks lit, qubits flipped, corrected, in the + residual or in the certificate's witness; the threshold plot's series. */ +.qs-qec .qs-shortcuts-head { flex-wrap: wrap; } +.qs-qec .qs-small-input { width: auto; min-height: 26px; padding: 2px 6px; } +.qs-qec-lattice { max-width: 560px; max-height: none; overflow: visible; } +.qs-qec-svg { width: 100%; height: auto; display: block; } +.qs-qec-face { fill: color-mix(in srgb, var(--text-secondary) 10%, transparent); stroke: var(--border-subtle); stroke-width: 0.04; } +.qs-qec-face.c0 { fill: color-mix(in srgb, var(--bad-text) 16%, transparent); } +.qs-qec-face.c1 { fill: color-mix(in srgb, var(--std-text) 16%, transparent); } +.qs-qec-face.c2 { fill: color-mix(in srgb, var(--info-text) 16%, transparent); } +.qs-qec-face.on { fill: color-mix(in srgb, var(--warn-text) 55%, transparent); stroke: var(--warn-text); } +.qs-qec-edge { stroke: color-mix(in srgb, var(--text-secondary) 35%, transparent); stroke-width: 0.16; stroke-linecap: round; } +.qs-qec-edge.on { stroke: var(--warn-text); } +.qs-qec-qubit { cursor: pointer; outline: none; } +.qs-qec-hit { fill: transparent; } +.qs-qec-dot { fill: var(--bg-surface); stroke: var(--text-secondary); stroke-width: 0.05; } +.qs-qec-qubit.err .qs-qec-dot { fill: var(--bad-text); stroke: var(--bad-text); } +.qs-qec-qubit.fix .qs-qec-dot { stroke: var(--ok-text); stroke-width: 0.12; } +.qs-qec-qubit.res .qs-qec-dot { fill: var(--warn-text); } +.qs-qec-wit { fill: none; stroke: var(--accent); stroke-width: 0.07; stroke-dasharray: 0.1 0.06; } +.qs-qec-qubit:focus-visible .qs-qec-dot, .qs-qec-qubit:hover .qs-qec-dot { stroke: var(--focus-ring, var(--accent)); } +.qs-qec-line { fill: none; stroke-width: 2; stroke-linejoin: round; } +.qs-qec-plot .qs-qec-dot { stroke-width: 1.5; } +.qs-qec-plot .qs-qec-series .qs-qec-dot { fill: inherit; stroke: var(--bg-surface); } +.qs-qec-series.s0 { stroke: var(--accent); fill: var(--accent); } +.qs-qec-series.s1 { stroke: var(--info-text); fill: var(--info-text); } +.qs-qec-series.s2 { stroke: var(--std-text); fill: var(--std-text); } +.qs-qec-series.s3 { stroke: var(--warn-text); fill: var(--warn-text); } +.qs-qec-line.qs-qec-series { fill: none; } +.qs-qec-legend { stroke: none; font-size: 11px; } +.qs-qec-details > summary { cursor: pointer; list-style: none; } +.qs-qec-details > summary::-webkit-details-marker { display: none; } +.qs-qec-details > summary::before { content: '▸'; color: var(--text-muted); margin-right: 6px; } +.qs-qec-details[open] > summary::before { content: '▾'; } +/* Answer views: an answer in the answer view layer, each view drawn as its statement arrives. */ +.qs-answers > summary { cursor: pointer; font-weight: 600; } +.qs-answers .qs-row { align-items: flex-end; } +.qs-av-input { width: 100%; box-sizing: border-box; font: 12px var(--font-mono, ui-monospace, monospace); } +.qs-av { display: flex; flex-direction: column; gap: 10px; margin-top: 8px; } +.qs-av-shell { display: flex; flex-direction: column; gap: 10px; } +.qs-av-card { border: 1px solid var(--border); border-radius: 8px; padding: 8px 10px; display: flex; flex-direction: column; gap: 6px; min-width: 0; } +.qs-av-card[data-state="dropped"], .qs-av-card[data-state="refused"] { border-color: color-mix(in srgb, var(--bad-text) 40%, var(--border)); } +.qs-av-what { color: var(--text-secondary); } +.qs-av-body { display: flex; flex-direction: column; gap: 6px; min-width: 0; } +.qs-av-circuit, .qs-av-live { overflow-x: auto; max-width: 100%; } +.qs-av-bloch { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; } +.qs-av-diags { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--text-secondary); } +.qs-av-diag.warning { color: var(--text-muted); } +.qs-ai-proposal.qs-set-aside { opacity: 0.6; } +.qs-tel { font-size: 12px; margin: 4px 0; } +.qs-tel > summary { cursor: pointer; color: var(--text-secondary); } +/* The course: the lessons beside the lesson, its exercise checked by the engine. */ +.qs-course[hidden] { display: none; } +.qs-course-body { display: grid; grid-template-columns: minmax(180px, 240px) minmax(0, 1fr); gap: 0; min-height: 0; } +.qs-course-list { padding: 8px 10px; border-right: 1px solid var(--border); max-height: 640px; overflow: auto; } +.qs-course-unit { margin-bottom: 8px; } +.qs-course-unit ol { margin: 4px 0 0; padding: 0; list-style: none; } +.qs-course-item { display: block; width: 100%; text-align: left; padding: 4px 6px; border: 0; border-radius: 5px; background: none; color: var(--text-primary); font: 12px var(--font-sans, system-ui, sans-serif); cursor: pointer; overflow-wrap: anywhere; } +.qs-course-item:hover { background: color-mix(in srgb, var(--accent) 10%, transparent); } +.qs-course-item[aria-current="true"] { background: color-mix(in srgb, var(--accent) 18%, transparent); } +.qs-course-mark { display: inline-block; width: 1.1em; color: var(--text-muted); } +.qs-course-item.done .qs-course-mark { color: var(--ok-text, var(--std-text, #10b981)); } +.qs-course-lesson { padding: 10px 14px 12px; min-width: 0; line-height: 1.55; } +.qs-course-title { margin: 2px 0 6px; font-size: 16px; } +.qs-course-text p, .qs-course-text li { margin: 0 0 8px; } +.qs-course-text h3, .qs-course-text h4, .qs-course-text h5 { margin: 12px 0 6px; font-size: 13px; } +.qs-course-text code, .qs-course-exercise code { font-family: var(--font-mono, ui-monospace, monospace); font-size: 12px; } +.qs-code { margin: 0 0 10px; padding: 8px 10px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg-base); overflow-x: auto; font-size: 12px; } +.qs-course-exercise { margin-top: 10px; padding: 10px 12px; border: 1px solid var(--border-subtle); border-radius: 8px; background: var(--bg-base); } +.qs-course-task { margin: 4px 0 6px; font-weight: 600; } +.qs-course-must, .qs-course-cases, .qs-course-rules, .qs-course-wrong ul { margin: 4px 0 8px; padding-left: 18px; font-size: 12px; } +.qs-course-cases, .qs-course-rules { list-style: none; padding-left: 0; } +.qs-course-rules li.bad, .qs-course-cases li.bad { color: var(--bad-text, #f87171); } +.qs-course-result p { margin: 8px 0 4px; } +.qs-course-hints { margin: 8px 0 4px; padding-left: 18px; font-size: 12px; } +.qs-course-wrong, .qs-course-asked { margin-top: 8px; padding: 8px 10px; border-left: 3px solid var(--accent); background: color-mix(in srgb, var(--accent) 6%, transparent); border-radius: 4px; font-size: 12px; overflow-wrap: anywhere; } +.qs-course-engine { display: block; color: var(--text-secondary); font-family: var(--font-mono, ui-monospace, monospace); font-size: 11px; } +.qs-course-nav { justify-content: space-between; margin-top: 10px; } +#qs-course-companion { padding: 6px 14px 10px; margin: 0; border-top: 1px solid var(--border); } +@media (max-width: 900px) { + .qs-course-body { grid-template-columns: minmax(0, 1fr); } + .qs-course-list { border-right: 0; border-bottom: 1px solid var(--border); max-height: 220px; } +} diff --git a/wai/quantum-studio/src/study.js b/wai/quantum-studio/src/study.js index 1d96bc4c9..16d5f6ea2 100644 --- a/wai/quantum-studio/src/study.js +++ b/wai/quantum-studio/src/study.js @@ -11,6 +11,7 @@ // again from what they made. "Export my record" seals it with this browser's // key into a file they give the researcher; nothing is sent anywhere. import { el } from './dom.js'; +import { t } from './i18n.js'; import * as Predict from './predict.js'; const SAVED = 'wai.quantum-studio.study'; @@ -52,18 +53,19 @@ export function engineBest(measures) { return best; } +// Each measure's name as the reader is told it; the keys are the engine's. const NAMES = { - quality: 'your edits the engine proves equal that made it cheaper', - quality_trend: 'that share, later against earlier', - selection: 'how often you took the engine’s best of those shown', - calibration_off: 'how far your predicted share was from the run’s', - calibration_slope: 'the run’s share against your prediction (1 is calibrated)', - unassisted: 'your probe score without the assistant (1 matches the reference)', - ownership: 'your answers on ownership, 1 to 7', - authorship: 'your share of the program’s gates', - novelty: 'how far your designs are from the known constructions', - diversity: 'how many distinct designs you made (Vendi)', - convergence: 'a model’s share of your program’s gates', + quality: t('study.measure_quality'), + quality_trend: t('study.measure_quality_trend'), + selection: t('study.measure_selection'), + calibration_off: t('study.measure_calibration_off'), + calibration_slope: t('study.measure_calibration_slope'), + unassisted: t('study.measure_unassisted'), + ownership: t('study.measure_ownership'), + authorship: t('study.measure_authorship'), + novelty: t('study.measure_novelty'), + diversity: t('study.measure_diversity'), + convergence: t('study.measure_convergence'), }; // The pane. `history()` and `inputs()` read the program's; `signer()` is @@ -73,14 +75,14 @@ const NAMES = { export function studyPane({ engine, log, flags, history, inputs, signer, save, organic, featuresView }) { const status = el('p', { class: 'qs-note', id: 'qs-study-status' }); const fileIn = el('input', { type: 'file', accept: '.json,application/json', id: 'qs-study-file', hidden: true, 'aria-hidden': 'true' }); - const loadBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-study-load', type: 'button', text: 'Load a protocol', onclick: () => fileIn.click() }); + const loadBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-study-load', type: 'button', text: t('study.load'), onclick: () => fileIn.click() }); const offer = el('div', { class: 'qs-study-offer', id: 'qs-study-offer' }); const keepBox = el('input', { type: 'checkbox', id: 'qs-study-keep', checked: log.keeping() }); - const keepLabel = el('label', { class: 'qs-check', for: 'qs-study-keep' }, keepBox, el('span', { text: 'Keep a record of how I work, in this browser' })); + const keepLabel = el('label', { class: 'qs-check', for: 'qs-study-keep' }, keepBox, el('span', { text: t('study.keep') })); const tools = el('div', { class: 'qs-study-tools', id: 'qs-study-tools' }); const out = el('div', { class: 'qs-study-out', id: 'qs-study-out' }); const node = el('section', { class: 'qs-pane qs-study', 'aria-labelledby': 'qs-study-h' }, - el('div', { class: 'qs-head' }, el('h2', { id: 'qs-study-h', text: 'Study' }), el('span', { class: 'qs-grow' }), loadBtn, fileIn), + el('div', { class: 'qs-head' }, el('h2', { id: 'qs-study-h', text: t('study.title') }), el('span', { class: 'qs-grow' }), loadBtn, fileIn), el('div', { class: 'qs-study-body' }, status, offer, keepLabel, tools, out, featuresView)); const record = () => { @@ -93,21 +95,26 @@ export function studyPane({ engine, log, flags, history, inputs, signer, save, o keepBox.checked = log.keeping(); keepLabel.hidden = log.enrolled(); if (log.enrolled()) { - status.textContent = `You take part in “${s.study}”, condition ${s.condition}, set by your key. Your record stays in this browser until you export it.`; + status.textContent = t('study.enrolled', { condition: s.condition, study: s.study }); } else { - status.textContent = 'Take part in a study of how people design with the engine: a researcher gives you a protocol, sealed before any data, and you see what it studies before you choose. Or keep a record for yourself.'; + status.textContent = t('study.intro'); } const n = s.choices.length + s.probes.length + s.ownership.length; + const [c, p, o] = [s.choices.length, s.probes.length, s.ownership.length]; tools.hidden = !log.keeping(); tools.replaceChildren( - el('p', { class: 'qs-note', text: `Kept here: ${s.choices.length} choice${s.choices.length === 1 ? '' : 's'} among candidates, ${s.probes.length} probe${s.probes.length === 1 ? '' : 's'}, ${s.ownership.length} answer${s.ownership.length === 1 ? '' : 's'} on ownership, and your predictions and history.` }), + el('p', { class: 'qs-note', text: t('study.kept', { + choices: c === 1 ? t('study.choice_one', { n: c }) : t('study.choices', { n: c }), + probes: p === 1 ? t('study.probe_one', { n: p }) : t('study.probes', { n: p }), + answers: o === 1 ? t('study.answer_one', { n: o }) : t('study.answers', { n: o }), + }) }), el('div', { class: 'qs-actions' }, - el('button', { class: 'qs-btn qs-small', id: 'qs-study-own', type: 'button', text: 'How much is this yours?', onclick: ownershipForm }), - el('button', { class: 'qs-btn qs-small', id: 'qs-study-probe', type: 'button', text: 'A probe task', onclick: probeForm }), - el('button', { class: 'qs-btn qs-small', id: 'qs-study-record', type: 'button', text: 'My record', onclick: showRecord }), - log.enrolled() ? el('button', { class: 'qs-btn qs-small', id: 'qs-study-export', type: 'button', text: 'Export my record', onclick: exportRecord }) : null, - log.enrolled() ? el('button', { class: 'qs-btn qs-small', id: 'qs-study-leave', type: 'button', text: 'Leave the study', onclick: leave }) : null, - n ? el('button', { class: 'qs-btn qs-small', id: 'qs-study-forget', type: 'button', text: 'Forget what is kept', onclick: () => { log.forget(); out.replaceChildren(); show(); } }) : null)); + el('button', { class: 'qs-btn qs-small', id: 'qs-study-own', type: 'button', text: t('study.own'), onclick: ownershipForm }), + el('button', { class: 'qs-btn qs-small', id: 'qs-study-probe', type: 'button', text: t('study.probe'), onclick: probeForm }), + el('button', { class: 'qs-btn qs-small', id: 'qs-study-record', type: 'button', text: t('study.my_record'), onclick: showRecord }), + log.enrolled() ? el('button', { class: 'qs-btn qs-small', id: 'qs-study-export', type: 'button', text: t('study.export'), onclick: exportRecord }) : null, + log.enrolled() ? el('button', { class: 'qs-btn qs-small', id: 'qs-study-leave', type: 'button', text: t('study.leave'), onclick: leave }) : null, + n ? el('button', { class: 'qs-btn qs-small', id: 'qs-study-forget', type: 'button', text: t('study.forget'), onclick: () => { log.forget(); out.replaceChildren(); show(); } }) : null)); } fileIn.addEventListener('change', async () => { @@ -121,19 +128,19 @@ export function studyPane({ engine, log, flags, history, inputs, signer, save, o const a = await engine.study('assign', { sealed, participant: me }); offer.replaceChildren( el('h3', { class: 'qs-study-name', text: p.protocol.study }), - el('p', { class: 'qs-note', text: 'What it studies, written before any data:' }), + el('p', { class: 'qs-note', text: t('study.hypotheses') }), el('ul', { class: 'qs-study-hyp' }, ...p.protocol.hypotheses.map((h) => el('li', { text: h }))), - el('p', { class: 'qs-note', text: `It compares ${p.protocol.conditions.length} conditions, each a setting of the Studio’s features; your key assigns you one. Its primary measure: ${NAMES[p.protocol.primary] ?? p.protocol.primary}. The protocol is sealed (digest ${p.digest.slice(0, 16)}…), so it cannot change after you join.` }), + el('p', { class: 'qs-note', text: t('study.compares', { n: p.protocol.conditions.length, digest: p.digest.slice(0, 16), primary: NAMES[p.protocol.primary] ?? p.protocol.primary }) }), el('div', { class: 'qs-actions' }, - el('button', { class: 'qs-btn qs-small qs-primary', id: 'qs-study-join', type: 'button', text: 'Take part', onclick: () => { log.join(sealed, p.digest, p.protocol.study, a); flags.lock(a.flags || {}); offer.replaceChildren(); show(); } }), - el('button', { class: 'qs-btn qs-small', type: 'button', text: 'Not now', onclick: () => offer.replaceChildren() }))); - } catch (e) { offer.replaceChildren(el('p', { class: 'qs-empty', text: `That is not a protocol the engine opens: ${e.message}` })); } + el('button', { class: 'qs-btn qs-small qs-primary', id: 'qs-study-join', type: 'button', text: t('study.join'), onclick: () => { log.join(sealed, p.digest, p.protocol.study, a); flags.lock(a.flags || {}); offer.replaceChildren(); show(); } }), + el('button', { class: 'qs-btn qs-small', type: 'button', text: t('study.not_now'), onclick: () => offer.replaceChildren() }))); + } catch (e) { offer.replaceChildren(el('p', { class: 'qs-empty', text: t('study.not_protocol', { error: e.message }) })); } }); function leave() { log.leave(); flags.unlock(); - out.replaceChildren(el('p', { class: 'qs-note', text: 'You left the study. What was kept stays in this browser until you forget it.' })); + out.replaceChildren(el('p', { class: 'qs-note', text: t('study.left') })); show(); } @@ -141,42 +148,42 @@ export function studyPane({ engine, log, flags, history, inputs, signer, save, o const items = await engine.study('ownership_items', {}); const selects = items.map((text, i) => el('select', { class: 'qs-input qs-small', id: `qs-own-${i}`, 'aria-label': text }, el('option', { value: '', text: '–' }), ...[1, 2, 3, 4, 5, 6, 7].map((v) => el('option', { value: String(v), text: String(v) })))); - const saveBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-own-save', type: 'button', text: 'Save my answers' }); + const saveBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-own-save', type: 'button', text: t('study.save_answers') }); saveBtn.addEventListener('click', () => { const vals = selects.map((s) => Number(s.value)); - if (vals.some((v) => !v)) { saveBtn.textContent = 'Answer every one'; return; } + if (vals.some((v) => !v)) { saveBtn.textContent = t('study.answer_all'); return; } log.ownership({ items: vals, program: history()?.versions?.at(-1)?.id ?? null }); - out.replaceChildren(el('p', { class: 'qs-note', id: 'qs-own-saved', text: 'Saved, with the version of the program you answered about.' })); + out.replaceChildren(el('p', { class: 'qs-note', id: 'qs-own-saved', text: t('study.saved_answers') })); show(); }); - out.replaceChildren(el('p', { class: 'qs-note', text: 'About the program in the editor, from 1 (not at all) to 7 (completely):' }), + out.replaceChildren(el('p', { class: 'qs-note', text: t('study.own_scale') }), el('ol', { class: 'qs-own' }, ...items.map((text, i) => el('li', {}, el('span', { text }), selects[i]))), saveBtn); } async function probeForm() { const probes = await engine.study('probes', {}); const done = new Set(log.state().probes.map((p) => p.probe)); - const pick = el('select', { class: 'qs-input qs-small', id: 'qs-probe-pick', 'aria-label': 'Which probe' }, ...probes.map((p) => el('option', { value: p.probe, text: `${p.probe}${done.has(p.probe) ? ' (done before)' : ''}` }))); + const pick = el('select', { class: 'qs-input qs-small', id: 'qs-probe-pick', 'aria-label': t('study.which_probe') }, ...probes.map((p) => el('option', { value: p.probe, text: `${p.probe}${done.has(p.probe) ? t('study.done_before') : ''}` }))); const task = el('p', { class: 'qs-study-task', id: 'qs-probe-task' }); - const answer = el('textarea', { class: 'qs-input qs-probe-answer', id: 'qs-probe-answer', rows: '7', spellcheck: 'false', 'aria-label': 'Your answer, in OpenQASM', placeholder: 'OPENQASM 3.0;\nqubit[2] q;\n…' }); + const answer = el('textarea', { class: 'qs-input qs-probe-answer', id: 'qs-probe-answer', rows: '7', spellcheck: 'false', 'aria-label': t('study.answer_label'), placeholder: 'OPENQASM 3.0;\nqubit[2] q;\n…' }); const result = el('p', { class: 'qs-note', id: 'qs-probe-result' }); const was = organic.on(); organic.set(true); const close = () => { organic.set(was); out.replaceChildren(); }; const showTask = () => { task.textContent = probes.find((p) => p.probe === pick.value)?.task ?? ''; result.textContent = ''; }; pick.addEventListener('change', showTask); - const score = el('button', { class: 'qs-btn qs-small', id: 'qs-probe-score', type: 'button', text: 'Score it' }); + const score = el('button', { class: 'qs-btn qs-small', id: 'qs-probe-score', type: 'button', text: t('study.score') }); score.addEventListener('click', async () => { try { const s = await engine.study('score_probe', { probe: pick.value, qasm: answer.value }); log.probe({ probe: pick.value, source: answer.value }); - result.textContent = s.verified ? `Verified. Score ${s.score} (the reference scores 1; cost ${s.cost} against ${s.reference_cost}).` : `Not verified: ${s.failure}. Score 0.`; + result.textContent = s.verified ? t('study.verified', { score: s.score, cost: s.cost, reference: s.reference_cost }) : t('study.not_verified', { failure: s.failure }); show(); } catch (e) { result.textContent = e.message; } }); - out.replaceChildren(el('p', { class: 'qs-note', text: 'Done without the assistant: it is paused until you close this. The engine scores your answer.' }), + out.replaceChildren(el('p', { class: 'qs-note', text: t('study.probe_note') }), el('div', { class: 'qs-actions' }, pick), task, answer, - el('div', { class: 'qs-actions' }, score, el('button', { class: 'qs-btn qs-small', id: 'qs-probe-close', type: 'button', text: 'Close', onclick: close })), result); + el('div', { class: 'qs-actions' }, score, el('button', { class: 'qs-btn qs-small', id: 'qs-probe-close', type: 'button', text: t('study.close'), onclick: close })), result); showTask(); } @@ -184,16 +191,16 @@ export function studyPane({ engine, log, flags, history, inputs, signer, save, o try { const m = await engine.study('measure', { record: record() }); const rows = [ - ['quality', m.quality?.share, m.quality ? `${m.quality.improved} of ${m.quality.edits} edits` : ''], + ['quality', m.quality?.share, m.quality ? t('study.of_edits', { n: m.quality.improved, of: m.quality.edits }) : ''], ['quality_trend', m.quality?.trend, ''], - ['selection', m.selection?.rate, m.selection?.choices ? `${m.selection.best_taken} of ${m.selection.choices}; by chance ${m.selection.chance}` : ''], - ['calibration_off', m.calibration?.mean_off, m.calibration?.predictions ? `${m.calibration.predictions} predictions` : ''], + ['selection', m.selection?.rate, m.selection?.choices ? t('study.of_choices', { n: m.selection.best_taken, of: m.selection.choices, chance: m.selection.chance }) : ''], + ['calibration_off', m.calibration?.mean_off, m.calibration?.predictions ? t('study.predictions', { n: m.calibration.predictions }) : ''], ['calibration_slope', m.calibration?.slope, ''], - ['unassisted', m.unassisted?.mean, m.unassisted?.probes ? `${m.unassisted.probes} probes` : ''], - ['ownership', m.ownership?.mean, m.ownership?.answers ? `${m.ownership.answers} answers` : ''], + ['unassisted', m.unassisted?.mean, m.unassisted?.probes ? t('study.probes', { n: m.unassisted.probes }) : ''], + ['ownership', m.ownership?.mean, m.ownership?.answers ? t('study.answers', { n: m.ownership.answers }) : ''], ['authorship', m.authorship, ''], ['novelty', m.novelty?.mean, ''], - ['diversity', m.diversity?.vendi, m.diversity?.designs ? `${m.diversity.designs} designs` : ''], + ['diversity', m.diversity?.vendi, m.diversity?.designs ? t('study.designs', { n: m.diversity.designs }) : ''], ['convergence', m.convergence?.share, ''], ]; out.replaceChildren(el('table', { class: 'qs-table qs-study-measures', id: 'qs-study-measures' }, @@ -201,7 +208,7 @@ export function studyPane({ engine, log, flags, history, inputs, signer, save, o el('th', { scope: 'row', text: NAMES[k] }), el('td', { class: 'num', text: v == null ? '—' : String(v) }), el('td', { class: 'qs-note', text: more }))))), - el('p', { class: 'qs-note', text: 'Each is the engine’s, computed again from what you made and did; none is a model’s judgement.' })); + el('p', { class: 'qs-note', text: t('study.measures_note') })); } catch (e) { out.replaceChildren(el('p', { class: 'qs-empty', text: e.message })); } } @@ -210,7 +217,7 @@ export function studyPane({ engine, log, flags, history, inputs, signer, save, o const sealed = await engine.study('seal_record', { record: record() }); const me = sealed.record.participant; const where = await save(`study-record-${me.slice(-12)}.json`, JSON.stringify(sealed, null, 2) + '\n', 'application/json'); - out.replaceChildren(el('p', { class: 'qs-note', id: 'qs-study-exported', text: `Saved ${where}, sealed with your key. Give it to the researcher; nothing was sent.` })); + out.replaceChildren(el('p', { class: 'qs-note', id: 'qs-study-exported', text: t('study.exported', { where }) })); } catch (e) { out.replaceChildren(el('p', { class: 'qs-empty', text: e.message })); } } diff --git a/wai/quantum-studio/src/taste.js b/wai/quantum-studio/src/taste.js index 0c3ce463a..721249713 100644 --- a/wai/quantum-studio/src/taste.js +++ b/wai/quantum-studio/src/taste.js @@ -11,15 +11,25 @@ // built), which the profile never learns and only the novelty dial moves, and // says whether it narrows what it puts first. import { el } from './dom.js'; +import { t } from './i18n.js'; const SAVED = 'wai.quantum-studio.taste'; export const PARTS = [ - ['economy', 'what it spends: gates, depth, T gates'], - ['clarity', 'how readily it is read'], - ['insight', 'what it shows about the problem'], - ['precision', 'how exactly it does its job, under noise too'], - ['utility', 'how readily it is used elsewhere'], + ['economy', t('taste.part_economy_what')], + ['clarity', t('taste.part_clarity_what')], + ['insight', t('taste.part_insight_what')], + ['precision', t('taste.part_precision_what')], + ['utility', t('taste.part_utility_what')], ]; +// A part of the rubric as the reader reads it; the engine and the profile keep its name. +const PART_WORDS = { + economy: t('taste.part_economy'), + clarity: t('taste.part_clarity'), + insight: t('taste.part_insight'), + precision: t('taste.part_precision'), + utility: t('taste.part_utility'), +}; +const partWord = (k) => PART_WORDS[k] ?? k; const blank = (name) => ({ taste: 1, name, choices: [] }); const num = (x) => String(Math.round(x * 1000) / 1000); @@ -52,24 +62,25 @@ export function tasteStore(engine) { // A profile's summary in words. export function summaryText(s) { - if (!s?.choices) return 'No choices yet. Compare two kept generators, or apply or refuse an alternative that is the same circuit as the draft, and your taste is learned from what you choose.'; - const parts = Object.entries(s.parts ?? {}).map(([k, n]) => `${k} ${n}`); - const out = [`${s.choices} choice${s.choices === 1 ? '' : 's'}${parts.length ? ` (${parts.join(', ')})` : ''}`]; - if (s.as_made?.of) out.push(`it predicts ${s.as_made.right} of the ${s.as_made.of} you made after your first, each from the ones before`); - if (s.leanings?.length) out.push(`it leans toward ${s.leanings.map((l) => l.words).join(', ')}`); - if (s.unmeasured) out.push(`${s.unmeasured} differ in nothing the engine measures`); - return out.join(' · ') + '.'; + if (!s?.choices) return t('taste.no_choices'); + const parts = Object.entries(s.parts ?? {}).map(([k, n]) => `${partWord(k)} ${n}`); + const choices = s.choices === 1 ? t('taste.choices_one', { n: s.choices }) : t('taste.choices_many', { n: s.choices }); + const out = [`${choices}${parts.length ? t('taste.choices_parts', { parts: parts.join(', ') }) : ''}`]; + if (s.as_made?.of) out.push(t('taste.predicts', { right: s.as_made.right, of: s.as_made.of })); + if (s.leanings?.length) out.push(t('taste.leans', { list: s.leanings.map((l) => l.words).join(', ') })); + if (s.unmeasured) out.push(t('taste.unmeasured', { n: s.unmeasured })); + return t('taste.summary', { parts: out.join(' · ') }); } // Where a ranking's parts and the cost put another first, and whether it narrows. export function disagreeText(r) { - const d = (r.disagree ?? []).map((x) => `${x.part === 'cost' ? 'the cost' : x.part} puts ${x.first} first`); - return d.length ? `Disagreeing: ${d.join('; ')}.` : null; + const d = (r.disagree ?? []).map((x) => t('taste.puts_first', { part: x.part === 'cost' ? t('taste.the_cost') : partWord(x.part), first: x.first })); + return d.length ? t('taste.disagreeing', { list: d.join('; ') }) : null; } export function narrowText(r) { const n = r.narrowing; if (n?.ranked == null || n?.baseline == null) return null; - return `The first ${n.first} are ${n.ranked.toFixed(2)} apart in how they are built; by ${n.by}, ${n.baseline.toFixed(2)}${n.narrows ? ': this ranking narrows what comes first' : ''}.`; + return t('taste.narrow', { first: n.first, ranked: n.ranked.toFixed(2), by: n.by, baseline: n.baseline.toFixed(2), narrows: n.narrows ? t('taste.narrows') : '' }); } // The Workbench's taste: `candidates()` the kept generators ({name, qasm, @@ -78,20 +89,20 @@ export function narrowText(r) { export function tastePanel({ store, candidates, inputs, task, onPick }) { const count = el('span', { class: 'qs-badge', id: 'qs-taste-count' }); const summaryLine = el('p', { class: 'qs-note', id: 'qs-taste-summary' }); - const profileIn = el('input', { class: 'qs-input qs-small', id: 'qs-taste-profile', value: store.name(), autocomplete: 'off', spellcheck: 'false', list: 'qs-taste-profiles', 'aria-label': 'The taste profile’s name' }); + const profileIn = el('input', { class: 'qs-input qs-small', id: 'qs-taste-profile', value: store.name(), autocomplete: 'off', spellcheck: 'false', list: 'qs-taste-profiles', 'aria-label': t('taste.profile_label') }); const names = el('datalist', { id: 'qs-taste-profiles' }); - const compareBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-taste-compare', type: 'button', text: 'Compare two', title: 'The two kept generators your profile is least sure of: choose the one you prefer' }); - const rankBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-taste-rank', type: 'button', text: 'Rank by my taste' }); - const dial = el('input', { type: 'range', min: '-1', max: '1', step: '0.25', value: '0', id: 'qs-taste-dial', 'aria-label': 'The novelty dial, from the typical to the novel' }); + const compareBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-taste-compare', type: 'button', text: t('taste.compare'), title: t('taste.compare_title') }); + const rankBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-taste-rank', type: 'button', text: t('taste.rank') }); + const dial = el('input', { type: 'range', min: '-1', max: '1', step: '0.25', value: '0', id: 'qs-taste-dial', 'aria-label': t('taste.dial_label') }); const pairBox = el('div', { class: 'qs-taste-pair', id: 'qs-taste-pair' }); const rankBox = el('div', { class: 'qs-taste-ranked', id: 'qs-taste-ranked' }); const node = el('details', { class: 'qs-taste', id: 'qs-taste' }, - el('summary', {}, el('span', { text: 'Your taste' }), count), + el('summary', {}, el('span', { text: t('taste.title') }), count), el('div', { class: 'qs-taste-body' }, - el('div', { class: 'qs-actions' }, el('label', { class: 'qs-taste-name' }, el('span', { text: 'Profile' }), profileIn), names, compareBtn, rankBtn), - el('label', { class: 'qs-taste-dial' }, el('span', { text: 'typical' }), dial, el('span', { text: 'novel' })), + el('div', { class: 'qs-actions' }, el('label', { class: 'qs-taste-name' }, el('span', { text: t('taste.profile') }), profileIn), names, compareBtn, rankBtn), + el('label', { class: 'qs-taste-dial' }, el('span', { text: t('taste.typical') }), dial, el('span', { text: t('taste.novel') })), summaryLine, pairBox, rankBox, - el('p', { class: 'qs-note', text: 'You choose only between programs the engine proves do the same thing, so a choice is about taste, never correctness. The profile is yours, kept in this browser under its name. A ranking shows how typical each one is, which the profile never learns and only the dial moves.' }))); + el('p', { class: 'qs-note', text: t('taste.note') }))); async function show() { names.replaceChildren(...store.names().map((n) => el('option', { value: n }))); @@ -99,36 +110,36 @@ export function tastePanel({ store, candidates, inputs, task, onPick }) { summaryLine.textContent = summaryText(s); count.textContent = s?.choices ? String(s.choices) : ''; } - const none = () => { pairBox.replaceChildren(el('p', { class: 'qs-empty', text: 'Start an archive and keep two generators or more to compare and rank them.' })); }; + const none = () => { pairBox.replaceChildren(el('p', { class: 'qs-empty', text: t('taste.none') })); }; async function compare() { const cs = candidates(); if (!cs || cs.length < 2) { none(); return; } - pairBox.replaceChildren(el('p', { class: 'qs-note', text: 'The engine is finding the pair your profile is least sure of…' })); + pairBox.replaceChildren(el('p', { class: 'qs-note', text: t('taste.finding_pair') })); try { const n = await store.next(cs, inputs(), task()); - if (!n.a) { pairBox.replaceChildren(el('p', { class: 'qs-empty', text: `Nothing to compare: ${n.why}.` })); return; } + if (!n.a) { pairBox.replaceChildren(el('p', { class: 'qs-empty', text: t('taste.nothing_to_compare', { why: n.why }) })); return; } const [a, b] = [cs.find((c) => c.name === n.a), cs.find((c) => c.name === n.b)]; - const part = el('select', { class: 'qs-input qs-small', id: 'qs-taste-part', 'aria-label': 'What the choice is about' }, - el('option', { value: '', text: 'the whole' }), ...PARTS.map(([k, w]) => el('option', { value: k, text: k, title: w }))); + const part = el('select', { class: 'qs-input qs-small', id: 'qs-taste-part', 'aria-label': t('taste.part_label') }, + el('option', { value: '', text: t('taste.the_whole') }), ...PARTS.map(([k, w]) => el('option', { value: k, text: partWord(k), title: w }))); const side = (c, o) => el('div', { class: 'qs-taste-side', 'data-name': c.name }, el('div', { class: 'qs-cand-head' }, - el('span', { class: 'qs-cand-title', text: `${c.name}${c.cost != null ? ` · cost ${num(c.cost)}` : ''}` }), - el('button', { class: 'qs-btn qs-small qs-taste-pick', type: 'button', text: 'This one', onclick: () => pick(c, o, part.value || null) })), + el('span', { class: 'qs-cand-title', text: `${c.name}${c.cost != null ? t('taste.cost_suffix', { cost: num(c.cost) }) : ''}` }), + el('button', { class: 'qs-btn qs-small qs-taste-pick', type: 'button', text: t('taste.this_one'), onclick: () => pick(c, o, part.value || null) })), el('pre', { class: 'qs-ai-code', text: c.qasm })); pairBox.replaceChildren( - el('p', { class: 'qs-note', text: `Which do you prefer? The engine proves they do the same thing (${n.how}).` }), + el('p', { class: 'qs-note', text: t('taste.which', { how: n.how }) }), el('div', { class: 'qs-taste-sides' }, side(a, b), side(b, a)), - el('div', { class: 'qs-actions' }, el('label', { class: 'qs-taste-name' }, el('span', { text: 'About' }), part), - el('button', { class: 'qs-btn qs-small', id: 'qs-taste-skip', type: 'button', text: 'Skip', onclick: () => pairBox.replaceChildren() }))); + el('div', { class: 'qs-actions' }, el('label', { class: 'qs-taste-name' }, el('span', { text: t('taste.about') }), part), + el('button', { class: 'qs-btn qs-small', id: 'qs-taste-skip', type: 'button', text: t('taste.skip'), onclick: () => pairBox.replaceChildren() }))); } catch (e) { pairBox.replaceChildren(el('p', { class: 'qs-empty', text: e.message })); } } async function pick(c, o, part) { try { await store.choose(c.qasm, o.qasm, { part, task: task(), inputs: inputs() }); - pairBox.replaceChildren(el('p', { class: 'qs-note', id: 'qs-taste-chosen', text: `Kept: ${c.name} over ${o.name}${part ? `, for its ${part}` : ''}. ` }, - el('button', { class: 'qs-btn qs-small', id: 'qs-taste-another', type: 'button', text: 'Another', onclick: compare }))); + pairBox.replaceChildren(el('p', { class: 'qs-note', id: 'qs-taste-chosen', text: t('taste.kept', { chosen: c.name, other: o.name, part: part ? t('taste.for_its', { part: partWord(part) }) : '' }) }, + el('button', { class: 'qs-btn qs-small', id: 'qs-taste-another', type: 'button', text: t('taste.another'), onclick: compare }))); if (rankBox.childElementCount) await rank(); } catch (e) { pairBox.replaceChildren(el('p', { class: 'qs-empty', text: e.message })); } } @@ -140,12 +151,12 @@ export function tastePanel({ store, candidates, inputs, task, onPick }) { const r = await store.rank(cs, inputs(), Number(dial.value)); const list = el('ol', { class: 'qs-taste-list', id: 'qs-taste-list' }, ...r.ranked.map((x) => el('li', { 'data-name': x.name }, el('button', { class: 'qs-chip', type: 'button', text: x.name, onclick: () => onPick?.(x.name) }), - el('span', { class: 'qs-taste-typ', title: 'Typicality: how near the known constructions it is built, from 1 (built as one is) to 0 (nothing shared)' }, - el('span', { class: 'qs-taste-bar', style: `--t: ${x.typicality ?? 0}` }), el('span', { text: x.typicality == null ? 'typicality —' : `typicality ${x.typicality.toFixed(2)}` })), - el('span', { class: 'qs-note', text: `score ${x.score}${x.cost != null ? ` · cost ${num(x.cost)}` : ''}${Object.keys(x.parts ?? {}).length ? ` · ${Object.entries(x.parts).map(([k, v]) => `${k} ${v}`).join(', ')}` : ''}` })))); + el('span', { class: 'qs-taste-typ', title: t('taste.typicality_title') }, + el('span', { class: 'qs-taste-bar', style: `--t: ${x.typicality ?? 0}` }), el('span', { text: x.typicality == null ? t('taste.typicality_none') : t('taste.typicality', { value: x.typicality.toFixed(2) }) })), + el('span', { class: 'qs-note', text: `${t('taste.score', { score: x.score })}${x.cost != null ? t('taste.cost_suffix', { cost: num(x.cost) }) : ''}${Object.keys(x.parts ?? {}).length ? ` · ${Object.entries(x.parts).map(([k, v]) => `${partWord(k)} ${v}`).join(', ')}` : ''}` })))); const d = disagreeText(r), nw = narrowText(r); rankBox.replaceChildren(...[ - r.choices ? null : el('p', { class: 'qs-note', text: 'No choices yet: ordered by the dial alone.' }), + r.choices ? null : el('p', { class: 'qs-note', text: t('taste.no_choices_dial') }), list, d ? el('p', { class: 'qs-note', id: 'qs-taste-disagree', text: d }) : null, nw ? el('p', { class: 'qs-note', id: 'qs-taste-narrow', text: nw }) : null].filter(Boolean)); diff --git a/wai/quantum-studio/src/telemetry.js b/wai/quantum-studio/src/telemetry.js new file mode 100644 index 000000000..d302e6983 --- /dev/null +++ b/wai/quantum-studio/src/telemetry.js @@ -0,0 +1,70 @@ +// Correctness telemetry, on this device only: one record per answer a model gives +// (wai_quantum::engine::telemetry): the diagnostic codes it drew, the level the +// engine checked it to and whether its intent held, the repairs made, the reader's +// accept or reject, and the receipt that seals its run. +// +// It is off until the reader turns it on. While it is off nothing is recorded and +// nothing is written: not a record, not a key in storage. Nothing is ever sent +// anywhere; the reader exports the record to a file when they ask, or the failures +// among it as an evaluation corpus in the conformance format. +import { el } from './dom.js'; + +export const KEY = 'wai.quantum-studio.telemetry'; + +// The store. `engine` answers telemetry('record' | 'corpus', …); `storage` is +// localStorage or a stand-in. +export function telemetryStore({ engine, storage = globalThis.localStorage, now = () => Date.now() }) { + let state = null; + try { state = JSON.parse(storage?.getItem(KEY) || 'null'); } catch (e) {} + if (!state || typeof state !== 'object' || !Array.isArray(state.records)) state = { on: false, records: [] }; + const save = () => { try { storage?.setItem(KEY, JSON.stringify(state)); } catch (e) {} }; + const listeners = new Set(); + const changed = () => listeners.forEach((f) => f()); + return { + on: () => !!state.on, + set(on) { state.on = !!on; save(); changed(); }, + records: () => state.records.map((r) => ({ ...r })), + // One answer's record; its index, or null when telemetry is off (then the + // engine is not even asked). + async record(fields) { + if (!state.on) return null; + const r = await engine.telemetry('record', { fields: { at: now(), surface: 'studio', ...fields } }); + if (!r || r.error || !state.on) return null; + state.records.push(r); + save(); changed(); + return state.records.length - 1; + }, + // The reader's accept or reject of the answer recorded at `id`. + decide(id, decision) { + if (!state.on || id == null || !state.records[id]) return; + state.records[id].decision = decision; + save(); changed(); + }, + clear() { state.records = []; save(); changed(); }, + corpus() { return engine.telemetry('corpus', { records: state.records }); }, + onChange(f) { listeners.add(f); }, + }; +} + +// The control: the switch (off by default), how many records, the exports and +// a delete. `save(name, text, type)` saves a file the reader asked for. +export function telemetryPanel({ store, save }) { + const box = el('input', { type: 'checkbox', id: 'qs-tel-on' }); + box.checked = store.on(); + box.addEventListener('change', () => store.set(box.checked)); + const count = el('span', { class: 'qs-note', id: 'qs-tel-count' }); + const exportAll = el('button', { class: 'qs-btn qs-small', type: 'button', id: 'qs-tel-export', text: 'Export the record', onclick: () => save('correctness-record.json', JSON.stringify({ format: 'wai.quantum.telemetry/1', records: store.records() }, null, 2) + '\n', 'application/json') }); + const exportCorpus = el('button', { class: 'qs-btn qs-small', type: 'button', id: 'qs-tel-corpus', text: 'Export the failures as an eval corpus', onclick: async () => save('correctness-failures.json', JSON.stringify(await store.corpus(), null, 2) + '\n', 'application/json') }); + const del = el('button', { class: 'qs-btn qs-small', type: 'button', id: 'qs-tel-delete', text: 'Delete the record', onclick: () => store.clear() }); + const show = () => { + const n = store.records().length; + count.textContent = `${n} answer${n === 1 ? '' : 's'} recorded${store.on() ? '' : '; recording is off'}`; + exportAll.disabled = exportCorpus.disabled = del.disabled = n === 0; + }; + store.onChange(show); + show(); + return el('details', { class: 'qs-tel', id: 'qs-tel' }, el('summary', { text: 'Correctness record' }), + el('label', { class: 'qs-check', for: 'qs-tel-on' }, box, ' Keep a record of how far the engine checked each of the model’s answers, on this device'), + el('p', { class: 'qs-note', text: 'Off unless you turn it on. Each record holds the codes an answer drew, the level the engine checked it to, the repairs, whether you took it, and its receipt. Nothing is sent anywhere; export it to a file when you choose.' }), + el('div', { class: 'qs-actions' }, count, exportAll, exportCorpus, del)); +} diff --git a/wai/quantum-studio/src/verdict-view.js b/wai/quantum-studio/src/verdict-view.js index edf508111..64ef20aba 100644 --- a/wai/quantum-studio/src/verdict-view.js +++ b/wai/quantum-studio/src/verdict-view.js @@ -10,20 +10,21 @@ // A verdict the job's receipt seals says so; one the Studio worked out here // (for a result that carries none, or a refusal) says that instead. import { el } from './dom.js'; +import { t } from './i18n.js'; const WORDS = { - refused: 'refused', - parsed: 'parsed, not run', - executed: 'executed, not checked', - evidence: 'checked as evidence', - proof: 'checked as proof', + refused: t('verdict_view.refused'), + parsed: t('verdict_view.parsed'), + executed: t('verdict_view.executed'), + evidence: t('verdict_view.evidence'), + proof: t('verdict_view.proof'), }; const CLASS = { refused: 'bad', parsed: '', executed: '', evidence: 'run', proof: 'ok' }; // The badge: its level as a word, the statement as its title. export function levelBadge(v, id) { const level = v?.level ?? 'parsed'; - const holds = v?.holds === false && (level === 'proof' || level === 'evidence') ? ': fails' : ''; + const holds = v?.holds === false && (level === 'proof' || level === 'evidence') ? t('verdict_view.fails') : ''; const attrs = { class: `qs-badge qs-level ${CLASS[level] ?? ''}${holds ? ' bad' : ''}`.trim(), 'data-level': level, title: v?.statement ?? '', text: `${WORDS[level] ?? level}${holds}` }; if (id) attrs.id = id; return el('span', attrs); @@ -31,7 +32,7 @@ export function levelBadge(v, id) { // The badge with the statement, and whether the receipt seals it. export function verdictLine(v, { sealed, id } = {}) { - if (!v) return el('span', { class: 'qs-note', text: 'no verdict' }); - const where = sealed ? 'sealed by the receipt' : 'worked out here, not sealed by a receipt'; - return el('span', { class: 'qs-verdict' }, levelBadge(v, id), el('span', { class: 'qs-why', text: ` ${v.statement ?? ''} (${where})` })); + if (!v) return el('span', { class: 'qs-note', text: t('verdict_view.no_verdict') }); + const where = sealed ? t('verdict_view.sealed') : t('verdict_view.not_sealed'); + return el('span', { class: 'qs-verdict' }, levelBadge(v, id), el('span', { class: 'qs-why', text: t('verdict_view.why', { statement: v.statement ?? '', where }) })); } diff --git a/wai/quantum-studio/src/versions.js b/wai/quantum-studio/src/versions.js index 859db2d24..575450d49 100644 --- a/wai/quantum-studio/src/versions.js +++ b/wai/quantum-studio/src/versions.js @@ -7,6 +7,7 @@ // history stays in this browser; a run's request carries its account, which // the receipt binds. import { el } from './dom.js'; +import { t } from './i18n.js'; const SAVED = 'wai.quantum-studio.versions'; const IDLE_MS = 4000; @@ -15,16 +16,20 @@ const SVG = 'http://www.w3.org/2000/svg'; // Whose hand, as the reader is told. export const HANDS = { - person: 'you', assistant: 'the assistant', agent: 'an agent', engine: 'the engine', origin: 'where it came from', - assistant_then_person: 'the assistant, then you', agent_then_person: 'an agent, then you', + person: t('versions.hand_person'), assistant: t('versions.hand_assistant'), agent: t('versions.hand_agent'), engine: t('versions.hand_engine'), origin: t('versions.hand_origin'), + assistant_then_person: t('versions.hand_assistant_then_person'), agent_then_person: t('versions.hand_agent_then_person'), }; -const KINDS = { person: 'you', assistant: 'the assistant', agent: 'an agent', engine: 'the engine' }; -const HOW = { start: 'started', edit: 'edited', proposal: 'proposed', merge: 'merged' }; +const KINDS = { person: HANDS.person, assistant: HANDS.assistant, agent: HANDS.agent, engine: HANDS.engine }; +const HOW = { start: t('versions.how_start'), edit: t('versions.how_edit'), proposal: t('versions.how_proposal'), merge: t('versions.how_merge') }; // Who, as the reader is told: a program from elsewhere is named for where it came from. -export const whoName = (by) => (by?.kind === 'origin' ? by.name || 'where it came from' : `${KINDS[by?.kind] || by?.kind || '?'}${by?.name ? ` (${by.name})` : ''}`); +export const whoName = (by) => (by?.kind === 'origin' ? by.name || HANDS.origin : `${KINDS[by?.kind] || by?.kind || '?'}${by?.name ? ` (${by.name})` : ''}`); // How a version was made, and by whom: “edited by you”, “started from the example Bell pair”. -const made = (v) => (v.by?.kind === 'origin' ? `${v.how === 'start' ? 'started from' : 'opened'} ${whoName(v.by)}` : `${HOW[v.how] || v.how} by ${whoName(v.by)}`); -const plural = (n, w) => `${n} ${w}${n === 1 ? '' : 's'}`; +const made = (v) => (v.by?.kind === 'origin' + ? (v.how === 'start' ? t('versions.started_from', { who: whoName(v.by) }) : t('versions.opened_from', { who: whoName(v.by) })) + : t('versions.made_by', { how: HOW[v.how] || v.how, who: whoName(v.by) })); +const nChanges = (n) => (n === 1 ? t('versions.change_one', { n }) : t('versions.changes', { n })); +const nGates = (n) => (n === 1 ? t('versions.gate_one', { n }) : t('versions.gates', { n })); +const nLines = (n) => (n === 1 ? t('versions.line_one', { n }) : t('versions.lines', { n })); const svg = (tag, attrs = {}) => { const e = document.createElementNS(SVG, tag); @@ -70,15 +75,15 @@ export function versionsPane({ engine, text, setText, inputs, lock, reviewHost, const serial = (f) => (queue = queue.then(f, f)); const handsBox = el('input', { type: 'checkbox', id: 'qs-vs-hands' }); - const handsLabel = el('label', { class: 'qs-check', for: 'qs-vs-hands', title: 'Mark each line and gate with whose hand it is in' }, handsBox, el('span', { text: 'Who wrote it' })); - const download = el('button', { class: 'qs-btn qs-small', id: 'qs-vs-download', type: 'button', text: 'Download', title: 'The program and its history, as wai-quantum history verify reads them' }); + const handsLabel = el('label', { class: 'qs-check', for: 'qs-vs-hands', title: t('versions.hands_title') }, handsBox, el('span', { text: t('versions.hands') })); + const download = el('button', { class: 'qs-btn qs-small', id: 'qs-vs-download', type: 'button', text: t('versions.download'), title: t('versions.download_title') }); const share = el('div', { class: 'qs-vs-share', id: 'qs-vs-share' }); const graph = el('div', { class: 'qs-vs-graph' }); const list = el('ol', { class: 'qs-vs-list', id: 'qs-vs-list' }); const detail = el('div', { class: 'qs-vs-detail', id: 'qs-vs-detail' }); const node = el('section', { class: 'qs-pane qs-vs', 'aria-labelledby': 'qs-vs-h' }, - el('div', { class: 'qs-head' }, el('h2', { id: 'qs-vs-h', text: 'Versions' }), el('span', { class: 'qs-grow' }), handsLabel, download), - el('p', { class: 'qs-note qs-vs-intro', text: 'Each change, who made it and how. What you type is yours; a proposal you apply is its proposer’s, and only the changes you take reach the program.' }), + el('div', { class: 'qs-head' }, el('h2', { id: 'qs-vs-h', text: t('versions.title') }), el('span', { class: 'qs-grow' }), handsLabel, download), + el('p', { class: 'qs-note qs-vs-intro', text: t('versions.intro') }), share, el('div', { class: 'qs-vs-lines' }, graph, list), detail); // ---- recording ---- @@ -138,7 +143,7 @@ export function versionsPane({ engine, text, setText, inputs, lock, reviewHost, await record(); const base = source(current); const { changes } = await call('changes', { base, proposed }); - if (!changes.length) { status?.(merge ? `${merge} holds nothing the program does not.` : 'The proposal is the program as it is.'); return null; } + if (!changes.length) { status?.(merge ? t('versions.merge_nothing', { id: merge }) : t('versions.proposal_same')); return null; } // The reader's rules bind a proposal, not their own merge. const breaches = merge ? changes.map(() => []) : (await engine.locks('check', { base, proposed, locks: rules(), inputs: inputs() })).changes.map((c) => c.breaches); const blocked = breaches.flatMap((b, i) => (b.length ? [i] : [])); @@ -167,19 +172,19 @@ export function versionsPane({ engine, text, setText, inputs, lock, reviewHost, const boxes = changes.map((c, i) => el('input', { type: 'checkbox', id: `qs-review-${i}`, checked: !blocked.includes(i), disabled: blocked.includes(i) })); const take = el('button', { class: 'qs-btn qs-primary qs-small', id: 'qs-review-apply', type: 'button' }); const open = changes.length - blocked.length; - const count = () => { const n = boxes.filter((b) => b.checked).length; take.textContent = n === changes.length ? `Take all ${changes.length}` : `Take ${n} of ${open}`; take.disabled = n === 0 && open === 0; }; + const count = () => { const n = boxes.filter((b) => b.checked).length; take.textContent = n === changes.length ? t('versions.take_all', { n: changes.length }) : t('versions.take_some', { n, of: open }); take.disabled = n === 0 && open === 0; }; boxes.forEach((b) => b.addEventListener('change', count)); count(); const who = merge ? `${merge} (${whoName(history.versions.find((v) => v.id === merge)?.by)})` : whoName(by); reviewHost.replaceChildren( - el('p', { class: 'qs-review-head' }, el('b', { text: merge ? `Bring in ${merge}` : `A proposal from ${who}` }), - el('span', { class: 'qs-note', text: ` · ${plural(changes.length, 'change')}${blocked.length ? `, ${blocked.length} blocked by your rules` : ''}. Take the ones you want; the program waits until you choose.` })), + el('p', { class: 'qs-review-head' }, el('b', { text: merge ? t('versions.bring_in', { id: merge }) : t('versions.proposal_from', { who }) }), + el('span', { class: 'qs-note', text: t('versions.review_sum', { changes: nChanges(changes.length), blocked: blocked.length ? t('versions.blocked_by_your_rules', { n: blocked.length }) : '' }) })), note ? el('p', { class: 'qs-note qs-review-note', text: note }) : null, el('ol', { class: 'qs-review-list' }, ...changes.map((c, i) => el('li', { class: blocked.includes(i) ? 'qs-blocked' : '' }, - el('label', { class: 'qs-check', for: `qs-review-${i}` }, boxes[i], el('span', { text: `at line ${c.line}` })), - breaches[i].length ? el('p', { class: 'qs-blocked-why', text: `Blocked: ${breaches[i].map((b) => b.why).join('; ')}.` }) : null, + el('label', { class: 'qs-check', for: `qs-review-${i}` }, boxes[i], el('span', { text: t('versions.at_line', { line: c.line }) })), + breaches[i].length ? el('p', { class: 'qs-blocked-why', text: t('versions.blocked_why', { why: breaches[i].map((b) => b.why).join('; ') }) }) : null, el('pre', { class: 'qs-diff' }, ...c.removed.map((l) => el('span', { class: 'qs-del', text: `− ${l}\n` })), ...c.added.map((l) => el('span', { class: 'qs-add', text: `+ ${l}\n` })))))), el('div', { class: 'qs-actions' }, take, - el('button', { class: 'qs-btn qs-small', id: 'qs-review-reject', type: 'button', text: merge ? 'Not now' : 'Reject all', onclick: async () => { + el('button', { class: 'qs-btn qs-small', id: 'qs-review-reject', type: 'button', text: merge ? t('versions.not_now') : t('versions.reject_all'), onclick: async () => { if (merge) { reviewing = false; lock(false); reviewHost.replaceChildren(); reviewHost.hidden = true; resolve(null); return; } resolve(await finish([])); } }))); @@ -204,16 +209,17 @@ export function versionsPane({ engine, text, setText, inputs, lock, reviewHost, } function shareOf(a) { - if (!a) return [el('p', { class: 'qs-note', text: 'The program does not read here, so its gates are not counted; its lines are marked.' })]; + if (!a) return [el('p', { class: 'qs-note', text: t('versions.unread') })]; const total = a.of.gates; - const bar = el('div', { class: 'qs-vs-bar', role: 'img', 'aria-label': a.contributors.map((c) => `${c.gates} of ${total} gates by ${HANDS[c.hand]}`).join(', ') }, - ...a.contributors.filter((c) => c.gates).map((c) => el('span', { class: `qs-vs-seg qs-hand-${c.hand}`, style: `flex-grow:${c.gates}`, title: `${HANDS[c.hand]}${c.name ? ` (${c.name})` : ''}: ${plural(c.gates, 'gate')}` }))); + const hand = (c) => `${HANDS[c.hand]}${c.name ? ` (${c.name})` : ''}`; + const bar = el('div', { class: 'qs-vs-bar', role: 'img', 'aria-label': a.contributors.map((c) => t('versions.bar_part', { n: c.gates, of: total, who: HANDS[c.hand] })).join(', ') }, + ...a.contributors.filter((c) => c.gates).map((c) => el('span', { class: `qs-vs-seg qs-hand-${c.hand}`, style: `flex-grow:${c.gates}`, title: t('versions.hand_gates', { gates: nGates(c.gates), who: hand(c) }) }))); return [ - el('p', { class: 'qs-vs-sum', id: 'qs-vs-sum', text: `${current}: ${plural(total, 'gate')}, ${plural(a.of.lines, 'line')}` }), + el('p', { class: 'qs-vs-sum', id: 'qs-vs-sum', text: t('versions.sum', { version: current, gates: nGates(total), lines: nLines(a.of.lines) }) }), total ? bar : null, el('ul', { class: 'qs-vs-who' }, ...a.contributors.map((c) => el('li', {}, el('span', { class: `qs-vs-key qs-hand-${c.hand}`, 'aria-hidden': 'true' }), - el('span', { text: `${HANDS[c.hand]}${c.name ? ` (${c.name})` : ''}: ${plural(c.gates, 'gate')}, ${plural(c.lines, 'line')}` })))), + el('span', { text: t('versions.hand_share', { gates: nGates(c.gates), lines: nLines(c.lines), who: hand(c) }) })))), ]; } @@ -239,15 +245,15 @@ export function versionsPane({ engine, text, setText, inputs, lock, reviewHost, const parent = v.parents?.[0]; const heads = log.filter((x) => x.head).map((x) => x.id); const actions = []; - if (v.id !== current) actions.push(el('button', { class: 'qs-btn qs-small', id: 'qs-vs-open', type: 'button', text: `Open ${v.id}`, title: 'Put this version in the editor; what you change from it is a line of work of its own', onclick: () => openVersion(v.id) })); - if (heads.includes(v.id) && v.id !== current) actions.push(el('button', { class: 'qs-btn qs-small', id: 'qs-vs-merge', type: 'button', text: `Bring ${v.id} in`, title: 'Review its changes against the program and take the ones you want; recorded as a merge', onclick: () => review(v.source, { kind: 'person' }, null, { merge: v.id }) })); + if (v.id !== current) actions.push(el('button', { class: 'qs-btn qs-small', id: 'qs-vs-open', type: 'button', text: t('versions.open', { id: v.id }), title: t('versions.open_title'), onclick: () => openVersion(v.id) })); + if (heads.includes(v.id) && v.id !== current) actions.push(el('button', { class: 'qs-btn qs-small', id: 'qs-vs-merge', type: 'button', text: t('versions.bring', { id: v.id }), title: t('versions.bring_title'), onclick: () => review(v.source, { kind: 'person' }, null, { merge: v.id }) })); const r = v.review; detail.replaceChildren( - el('p', { class: 'qs-vs-what' }, el('b', { text: v.id }), el('span', { text: ` ${made(v)}${v.parents?.length ? `, from ${v.parents.join(' and ')}` : ''}, ${new Date(v.at).toLocaleString()}` })), + el('p', { class: 'qs-vs-what' }, el('b', { text: v.id }), el('span', { text: t('versions.what', { when: new Date(v.at).toLocaleString(), from: v.parents?.length ? t('versions.from', { parents: v.parents.join(t('versions.and')) }) : '', made: made(v) }) })), v.note ? el('p', { class: 'qs-note', text: v.note }) : null, - r ? el('p', { class: 'qs-note', id: 'qs-vs-review', text: `${r.accepted.length} of ${plural(r.offered, 'change')} taken${r.blocked?.length ? `, ${r.blocked.length} blocked by the rules` : ''}${r.accepted.length < r.offered ? '; the rest were not taken, and the whole proposal is kept below' : ''}.` }) : null, - r && r.accepted.length < r.offered ? el('details', {}, el('summary', { text: 'The whole proposal' }), el('pre', { class: 'qs-ai-code', text: r.proposed })) : null, - parent ? el('p', { class: 'qs-note', text: `+${e.added ?? 0} −${e.removed ?? 0} against ${parent}${e.sealed ? ' · sealed' : ''}` }) : null, + r ? el('p', { class: 'qs-note', id: 'qs-vs-review', text: t('versions.review_taken', { n: r.accepted.length, changes: nChanges(r.offered), blocked: r.blocked?.length ? t('versions.blocked_by_the_rules', { n: r.blocked.length }) : '', rest: r.accepted.length < r.offered ? t('versions.rest_kept') : '' }) }) : null, + r && r.accepted.length < r.offered ? el('details', {}, el('summary', { text: t('versions.whole_proposal') }), el('pre', { class: 'qs-ai-code', text: r.proposed })) : null, + parent ? el('p', { class: 'qs-note', text: t('versions.against', { added: e.added ?? 0, removed: e.removed ?? 0, parent, sealed: e.sealed ? t('versions.sealed') : '' }) }) : null, actions.length ? el('div', { class: 'qs-actions' }, ...actions) : null); } @@ -255,7 +261,7 @@ export function versionsPane({ engine, text, setText, inputs, lock, reviewHost, await record(); current = id; keep(); setText(source(id)); - status?.(`Opened ${id}. What you change from here is a line of work of its own.`); + status?.(t('versions.opened', { id })); await render(); } @@ -266,12 +272,12 @@ export function versionsPane({ engine, text, setText, inputs, lock, reviewHost, graph.replaceChildren(drawGraph(history.versions, ls)); list.replaceChildren(...[...history.versions].reverse().map((v) => { const e = log.find((x) => x.id === v.id) || {}; - const tags = [v.id === current ? 'in the editor' : null, e.head && v.id !== current ? 'a head' : null].filter(Boolean); + const tags = [v.id === current ? t('versions.in_editor') : null, e.head && v.id !== current ? t('versions.a_head') : null].filter(Boolean); return el('li', { 'data-id': v.id, class: v.id === current ? 'here' : '' }, el('button', { type: 'button', class: 'qs-vs-item', onclick: () => showDetail(v, log) }, el('span', { class: 'qs-vs-id', text: v.id }), el('span', { class: 'qs-vs-by', text: made(v) }), - el('span', { class: 'qs-note', text: `${e.review ? `${e.review.accepted} of ${e.review.offered} taken${e.review.blocked ? `, ${e.review.blocked} blocked` : ''} · ` : ''}+${e.added ?? 0} −${e.removed ?? 0}${tags.length ? ` · ${tags.join(', ')}` : ''}` }))); + el('span', { class: 'qs-note', text: `${e.review ? t('versions.item_review', { n: e.review.accepted, of: e.review.offered, blocked: e.review.blocked ? t('versions.item_blocked', { n: e.review.blocked }) : '' }) : ''}+${e.added ?? 0} −${e.removed ?? 0}${tags.length ? ` · ${tags.join(', ')}` : ''}` }))); })); const b = await blame().catch(() => null); share.replaceChildren(...shareOf(b?.authorship)); @@ -289,7 +295,7 @@ export function versionsPane({ engine, text, setText, inputs, lock, reviewHost, await record(); await save('program.qasm', source(current), 'text/plain'); await save('program.history.json', JSON.stringify(history, null, 2) + '\n', 'application/json'); - status?.('Saved program.qasm and program.history.json; wai-quantum history verify program.qasm checks every version.', 'ok'); + status?.(t('versions.saved'), 'ok'); }); return { diff --git a/wai/quantum-studio/src/views/check.js b/wai/quantum-studio/src/views/check.js new file mode 100644 index 000000000..457eac798 --- /dev/null +++ b/wai/quantum-studio/src/views/check.js @@ -0,0 +1,49 @@ +// The engine inside the view (wai/wai-quantum-views): a job's receipt checked +// on the machine that shows it, by the engine's own code. +// +// The page carries the engine twice, put in by the build: as WebAssembly +// (globalThis.WQ_ENGINE_WASM, base64) and as the same build translated to +// JavaScript (globalThis.wqEngineJs(), a factory). A host's content-security +// policy may refuse to compile WebAssembly (the extension's default policy +// has no 'wasm-unsafe-eval'); the page's own script runs wherever the view +// does, so the second form checks the receipt there. Both give the same +// answer to the same request; the view says which ran. + +let ready = null; + +// The three exports driven alike: a request written into the engine's memory, +// the answer read back. +function driver(ex) { + return (request) => { + const bytes = new TextEncoder().encode(JSON.stringify(request)); + const p = ex.wq_alloc(bytes.length); + new Uint8Array(ex.memory.buffer, p, bytes.length).set(bytes); + const n = ex.wq_call(p, bytes.length); + return JSON.parse(new TextDecoder().decode(new Uint8Array(ex.memory.buffer, ex.wq_out(), n))); + }; +} + +export function engine() { + ready ??= (async () => { + let refused = null; + try { + const b64 = globalThis.WQ_ENGINE_WASM; + if (!b64) throw new Error('this page carries no WebAssembly engine'); + const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0)); + const { instance } = await WebAssembly.instantiate(bytes, {}); + return { form: 'WebAssembly', call: driver(instance.exports) }; + } catch (e) { + refused = String(e?.message || e); + } + if (typeof globalThis.wqEngineJs !== 'function') throw new Error(`the engine did not load here: ${refused}`); + return { form: 'JavaScript', refused, call: driver(globalThis.wqEngineJs()) }; + })(); + return ready; +} + +// Whether `job` ({request, result}) verifies here: {valid, key, signer, form}. +export async function verify(job) { + const e = await engine(); + const v = e.call({ op: 'verify', job: { request: job.request, result: job.result } }); + return { ...v, form: e.form, refused: e.refused }; +} diff --git a/wai/quantum-studio/src/views/host.js b/wai/quantum-studio/src/views/host.js new file mode 100644 index 000000000..1744aad54 --- /dev/null +++ b/wai/quantum-studio/src/views/host.js @@ -0,0 +1,69 @@ +// The view's side of the host protocol for interactive views (the Model +// Context Protocol's extension io.modelcontextprotocol/ui, version +// 2026-01-26): JSON-RPC 2.0 over postMessage with the window that framed the +// view. +// +// The view asks ui/initialize, applies the host's context, says +// ui/notifications/initialized, and then hears the tool's input +// (ui/notifications/tool-input, once, before the result), its result +// (ui/notifications/tool-result), a cancellation, a change of context, and +// teardown. It may call the server's tools (tools/call: only the ones whose +// visibility includes the view), tell the host its size, and set what the +// model reads of it (ui/update-model-context). +export const PROTOCOL = '2026-01-26'; + +export function connect({ name, version, onInput, onResult, onCancelled, onContext, onTeardown }) { + let next = 1; + const waiting = new Map(); + const send = (msg) => window.parent.postMessage(msg, '*'); + const respond = (id, result) => send({ jsonrpc: '2.0', id, result }); + const refuse = (id, code, message) => send({ jsonrpc: '2.0', id, error: { code, message } }); + const request = (method, params = {}) => new Promise((resolve, reject) => { + const id = next++; + waiting.set(id, { resolve, reject }); + send({ jsonrpc: '2.0', id, method, params }); + }); + const notify = (method, params = {}) => send({ jsonrpc: '2.0', method, params }); + + window.addEventListener('message', (ev) => { + if (ev.source !== window.parent) return; + const m = ev.data; + if (!m || m.jsonrpc !== '2.0') return; + if (m.method == null && m.id != null) { + const w = waiting.get(m.id); + if (!w) return; + waiting.delete(m.id); + if (m.error) w.reject(Object.assign(new Error(m.error.message || 'the host refused'), { code: m.error.code })); + else w.resolve(m.result); + return; + } + switch (m.method) { + case 'ui/notifications/tool-input': onInput?.(m.params?.arguments ?? {}); break; + case 'ui/notifications/tool-input-partial': break; + case 'ui/notifications/tool-result': onResult?.(m.params ?? {}); break; + case 'ui/notifications/tool-cancelled': onCancelled?.(m.params?.reason); break; + case 'ui/notifications/host-context-changed': onContext?.(m.params ?? {}); break; + case 'ui/resource-teardown': onTeardown?.(m.params?.reason); if (m.id != null) respond(m.id, {}); break; + case 'ping': if (m.id != null) respond(m.id, {}); break; + default: if (m.id != null) refuse(m.id, -32601, `the view takes no ${m.method}`); + } + }); + + return { + // The handshake: the host's answer, its context applied, then initialized. + async start() { + const init = await request('ui/initialize', { + protocolVersion: PROTOCOL, + appInfo: { name, version }, + appCapabilities: { availableDisplayModes: ['inline'] }, + }); + onContext?.(init?.hostContext ?? {}); + notify('ui/notifications/initialized'); + return init; + }, + callTool: (tool, args) => request('tools/call', { name: tool, arguments: args }), + sizeChanged: (width, height) => notify('ui/notifications/size-changed', { width, height }), + // What the model reads of the view; each call replaces the last. + tellModel: (text, structuredContent) => request('ui/update-model-context', { content: [{ type: 'text', text }], ...(structuredContent ? { structuredContent } : {}) }), + }; +} diff --git a/wai/quantum-studio/src/views/main.js b/wai/quantum-studio/src/views/main.js new file mode 100644 index 000000000..1cc55f602 --- /dev/null +++ b/wai/quantum-studio/src/views/main.js @@ -0,0 +1,99 @@ +// A view of WAI Quantum's engine inside an agent host: one page, the view it +// draws named by the server in , the tool's answer +// given by the host (src/views/host.js). The page loads nothing and connects +// nowhere; a job the service is still running is followed through the +// server's own tool for that, which the host keeps from the model. +import { el } from '../dom.js'; +import { connect } from './host.js'; +import { render, META } from './render.js'; + +const VIEW = document.querySelector('meta[name="wai-view"]')?.content || 'result'; +const POLL = 'poll_quantum_job'; +const root = document.getElementById('wq'); +const body = document.body; +body.dataset.view = VIEW; +body.dataset.state = 'waiting'; + +const TITLES = { result: 'Result', receipt: 'Receipt', circuit: 'Circuit', critique: 'Critique', estimate: 'Resource estimate', target: 'Device target' }; +let args = {}; + +function head(sub) { + return el('header', { class: 'wq-head' }, el('span', { class: 'wq-name', text: 'WAI Quantum' }), el('span', { class: 'wq-what', text: TITLES[VIEW] || VIEW }), sub ? el('span', { class: 'qs-note', text: sub }) : null); +} + +function waiting(text) { + root.replaceChildren(head(), el('p', { class: 'qs-note', id: 'wq-waiting', text })); +} + +const host = connect({ + name: 'wai-quantum-views', + version: '1', + onInput: (a) => { + args = a || {}; + waiting('The tool is running; its answer appears here.'); + }, + onResult: (res) => show(res), + onCancelled: (why) => { + body.dataset.state = 'cancelled'; + waiting(`The call was cancelled${why ? `: ${why}` : ''}.`); + }, + onContext: (ctx) => { + if (ctx?.theme === 'light' || ctx?.theme === 'dark') document.documentElement.dataset.theme = ctx.theme; + }, + onTeardown: () => { stopped = true; }, +}); + +let stopped = false; +let polling = null; + +async function show(res) { + const data = res?._meta?.[META] || null; + const out = render(VIEW, { args, res: res || {}, data }); + root.replaceChildren(head(), ...out.blocks); + body.dataset.state = out.state; + if (out.poll) follow(out.poll); + if (out.checked) { + const v = await out.checked; + if (!v.error) { + const what = VIEW === 'estimate' ? 'the estimate’s seal' : VIEW === 'circuit' ? 'the compile’s certificate' : 'the receipt'; + host.tellModel(`The ${TITLES[VIEW].toLowerCase()} view checked ${what} on the person’s machine with the engine: ${v.valid ? 'it verifies' : 'it does NOT verify'}${v.signer ? `, signed by ${v.signer}` : ''}.`).catch(() => {}); + } + } +} + +// A job the service is still running: asked after, more slowly each time, until it ends. +function follow(id) { + if (polling) return; + let wait = 1000; + polling = (async () => { + while (!stopped) { + await new Promise((r) => setTimeout(r, wait)); + wait = Math.min(5000, wait * 1.5); + let res; + try { res = await host.callTool(POLL, { id }); } catch (e) { + waiting(`This host would not let the view follow the job (${e.message}); get_quantum_job reads it.`); + break; + } + const s = res?.structuredContent?.status; + if (s !== 'queued' && s !== 'running') { polling = null; show(res); return; } + } + polling = null; + })(); +} + +// The host sizes the frame to the page. +let sized = 0; +new ResizeObserver(() => { + cancelAnimationFrame(sized); + sized = requestAnimationFrame(() => { + // The content's own size, not the frame's: the host fits the frame to it. + const r = root.getBoundingClientRect(); + host.sizeChanged(Math.ceil(r.width), Math.ceil(r.bottom)); + }); +}).observe(root); + +waiting('Waiting for the host.'); +host.start().catch((e) => { + body.dataset.state = 'no-host'; + waiting(`No host answered (${e.message}). This page is a view for an agent host; it draws what a tool answers.`); +}); diff --git a/wai/quantum-studio/src/views/render.js b/wai/quantum-studio/src/views/render.js new file mode 100644 index 000000000..6803728d9 --- /dev/null +++ b/wai/quantum-studio/src/views/render.js @@ -0,0 +1,271 @@ +// The six views an agent host draws, from the Studio's own components: a +// circuit, a result with its verdict, a receipt checked here, a critique, a +// resource estimate's frontier, and a device target's map. +// +// Each draws a tool's answer: the call's arguments, its result (the +// structured content), and what the server's engine computed for the view to +// draw (the result's `_meta["science.transaction.wai/quantum-view"]`: the +// program's layout and its description in words, a target's map). A view +// computes no physics and states no number the answer does not hold. The one +// thing it computes is the receipt check, with the engine's own code, here. +import { el } from '../dom.js'; +import { circuitSvg, mark } from '../circuit-svg.js'; +import { verdictLine } from '../verdict-view.js'; +import { chips, fidelityChip } from '../critique-view.js'; +import { frontierSvg, compact as count, duration } from '../estimate-view.js'; +import { couplingSvg, deviceBlock, legend, table } from '../device.js'; +import { METHOD_NAMES, ACCURACY, accuracyExtras, countsBlock, expectationsBlock, amplitudesBlock, energyText } from '../result-parts.js'; +import { verify } from './check.js'; + +export const META = 'science.transaction.wai/quantum-view'; + +const block = (k, ...kids) => el('div', { class: 'qs-block' }, el('div', { class: 'qs-k', text: k }), ...kids); +const row = (k, v, attrs = {}) => el('tr', {}, el('th', { scope: 'row', text: k }), el('td', attrs, v)); +const note = (text, attrs = {}) => el('p', { class: 'qs-note', ...attrs, text }); + +// What a receipt shows, and what it does not, as the engine's guide says it. +const RECEIPT_NOTE = 'A receipt shows that this key signed this request and this result; anyone can run the request again to compare. It does not say who holds the key, nor that the physics is right.'; + +// The circuit, drawn from the engine's layout, with its description in words. +export function circuitBlock(data, title = 'Circuit') { + if (!data) return null; + if (!data.layout) return data.why ? block(title, note(data.why, { id: 'wq-not-drawn' })) : null; + const { svg, drawn, total } = circuitSvg(data.layout); + const fig = el('div', { class: 'wq-circuit', id: 'wq-circuit' }, svg); + return { + svg, + node: block(title, fig, + drawn < total ? note(`The first ${drawn} of ${total} columns are drawn.`) : null, + data.description ? el('details', { class: 'wq-words' }, el('summary', { text: 'The circuit in words' }), el('p', { class: 'qs-why', id: 'wq-description', text: data.description })) : null), + }; +} + +// The program's text, folded. +function programBlock(src, title = 'Program') { + if (!src) return null; + return el('details', { class: 'wq-program' }, el('summary', { text: title }), el('pre', { class: 'wq-src', text: src })); +} + +// The receipt of `job` ({request, result}) checked in this view, by the +// engine's own code. `checked` resolves to the check's answer. +export function receiptBlock(job, { title = 'Receipt', what = 'this request and this result' } = {}) { + const rc = job?.result?.receipt || {}; + const badge = el('span', { class: 'qs-badge', id: 'wq-receipt', 'data-receipt': 'checking', text: 'checking in this view…' }); + const signer = el('td', { class: 'mono', id: 'wq-signer', text: rc.key ? `${rc.key.slice(0, 16)}…` : 'no key named' }); + const by = el('td', { id: 'wq-checked-by', text: '…' }); + const node = block(title, el('table', { class: 'qs-table' }, el('tbody', {}, row('check', badge), el('tr', {}, el('th', { scope: 'row', text: 'signed by' }), signer), el('tr', {}, el('th', { scope: 'row', text: 'checked by' }), by))), + note(RECEIPT_NOTE)); + const checked = (async () => { + if (!job?.result?.receipt) { + badge.dataset.receipt = 'unsealed'; + badge.textContent = 'no receipt'; + by.textContent = 'nothing to check'; + return { valid: false, error: 'no receipt' }; + } + try { + const v = await verify(job); + if (v.error) throw new Error(v.error); + badge.dataset.receipt = v.valid ? 'valid' : 'invalid'; + badge.className = `qs-badge ${v.valid ? 'ok' : 'bad'}`; + badge.textContent = v.valid ? `✓ seals ${what}` : `✗ does not seal ${what}`; + if (v.signer) signer.textContent = v.signer; + by.textContent = v.form === 'WebAssembly' ? 'the engine, in this view, as WebAssembly' : 'the engine, in this view, translated to JavaScript: this host’s policy refuses WebAssembly'; + if (v.refused) by.title = v.refused; + document.body.dataset.engine = v.form; + return v; + } catch (e) { + badge.dataset.receipt = 'unchecked'; + badge.className = 'qs-badge bad'; + badge.textContent = 'not checked here'; + by.textContent = String(e?.message || e); + return { valid: false, error: String(e?.message || e) }; + } + })(); + return { node, checked }; +} + +// A refusal: what the engine said, its code and meaning, and its verdict. +function refusalBlocks(sc, res) { + const msg = sc?.message || sc?.error || res?.content?.[0]?.text || 'refused'; + return [ + block('Refused', el('p', { class: 'qs-warn', id: 'wq-refusal', text: msg }), + sc?.code ? note(`${sc.code} · ${sc.outcome || 'refused'}: ${sc.meaning || ''}`, { id: 'wq-code' }) : null), + sc?.verdict ? block('Verdict', verdictLine(sc.verdict, { sealed: false, id: 'wq-verdict' })) : null, + ]; +} + +// ---- the views ---- + +// A result: the method and why, its accuracy, the verdict the receipt seals, +// the circuit, the numbers, a device target's account, the energy, and the +// receipt checked here. +function result({ res, data }) { + const sc = res.structuredContent || {}; + if (res.isError || sc.error || sc.code) return { blocks: refusalBlocks(sc, res), state: 'refused' }; + if (sc.status === 'queued' || sc.status === 'running') return { blocks: [block('Running', note(`The job ${sc.id || ''} is ${sc.status} on the service; this view follows it.`, { id: 'wq-running' }))], state: 'running', poll: sc.id }; + const r = sc.result || {}; + const acc = r.accuracy || {}; + const extra = accuracyExtras(r); + const circuit = circuitBlock(data); + const rc = receiptBlock({ request: sc.request, result: r }); + const blocks = [ + block('Verdict', r.verdict ? verdictLine(r.verdict, { sealed: true, id: 'wq-verdict' }) : note('This result carries no verdict.')), + block('Method', el('div', {}, el('b', { class: 'qs-method', id: 'wq-method', text: METHOD_NAMES[sc.method] || sc.method || '' }), el('span', { class: 'qs-why', text: ` ${sc.why || ''}` }))), + block('Accuracy', el('div', {}, el('span', { class: 'qs-badge ok', id: 'wq-accuracy', text: acc.class || '?' }), el('span', { class: 'qs-why', text: ` ${ACCURACY[acc.class] || ''}${extra.length ? ` ${extra.join(', ')}.` : ''}` })), + r.warning ? el('p', { class: 'qs-warn', text: r.warning }) : null), + circuit?.node, + r.counts ? countsBlock(r) : null, + r.expectations ? expectationsBlock(r) : null, + r.amplitudes ? amplitudesBlock(r) : null, + r.target ? deviceBlock(r.target, r.ideal) : null, + block('Energy', el('span', { class: 'qs-why', id: 'wq-energy', text: energyText(r.energy || {}) })), + rc.node, + programBlock(sc.request?.qasm), + ]; + return { blocks, checked: rc.checked, state: 'shown' }; +} + +// A receipt: the job given checked here, beside the server's own answer. +function receipt({ args, res }) { + const sc = res.structuredContent || {}; + const job = args?.job ? args.job : { request: args?.request, result: args?.result }; + if (res.isError && !job?.result) return { blocks: refusalBlocks(sc, res), state: 'refused' }; + const r = job?.result || {}; + const rc = receiptBlock(job, { title: 'Checked in this view' }); + const server = sc.valid === true ? 'it verifies' : sc.valid === false ? 'it does not verify' : 'no answer'; + const agree = el('span', { class: 'qs-badge', id: 'wq-agree', text: 'comparing…' }); + rc.checked.then((v) => { + if (v.error || typeof sc.valid !== 'boolean') { agree.textContent = 'not compared'; agree.dataset.agree = 'unknown'; return; } + const same = v.valid === sc.valid; + agree.dataset.agree = same ? 'yes' : 'no'; + agree.className = `qs-badge ${same ? 'ok' : 'bad'}`; + agree.textContent = same ? 'the server and this view agree' : 'the server and this view disagree: trust neither until it is checked again'; + }); + const top = r.counts ? Object.entries(r.counts).sort((a, b) => b[1] - a[1]).slice(0, 4).map(([k, n]) => `${k}: ${n}`).join(', ') : ''; + const blocks = [ + rc.node, + block('The server’s check', el('table', { class: 'qs-table' }, el('tbody', {}, row('answer', server, { id: 'wq-server-valid' }), row('agreement', agree)))), + block('What it seals', el('table', { class: 'qs-table' }, el('tbody', {}, + job?.request?.qasm ? row('program', `${job.request.qasm.split('\n').filter((l) => l.trim()).length} lines`) : null, + r.method ? row('method', METHOD_NAMES[r.method] || r.method) : null, + r.accuracy?.class ? row('accuracy', r.accuracy.class) : null, + top ? row('counts', top) : null, + r.verdict ? row('verdict', verdictLine(r.verdict, { sealed: true, id: 'wq-verdict' })) : null, + r.format ? row('format', r.format) : null))), + programBlock(job?.request?.qasm), + ]; + return { blocks, checked: rc.checked, state: 'shown' }; +} + +// A circuit: a program drawn, linted, repaired or compiled. +function circuit({ res, data }) { + const sc = res.structuredContent || {}; + const tool = data?.tool || ''; + if (res.isError && !sc.repairs) return { blocks: [...refusalBlocks(sc, res), programBlock(data?.program)], state: 'refused' }; + const c = circuitBlock(data); + const blocks = []; + let checked; + if (sc.verdict) blocks.push(block('Verdict', verdictLine(sc.verdict, { sealed: false, id: 'wq-verdict' }))); + if (sc.result?.certificate) { + // A compile: the passes, each with its check, and the certificate's seal. + const cert = sc.result.certificate; + const e = cert.epsilon || {}; + const counts = (x) => `${x?.gates ?? '—'} gates, ${x?.cnots ?? '—'} CNOTs, ${x?.t_count ?? '—'} T, ${x?.rotations ?? '—'} rotations`; + blocks.push(block('Compiled', el('p', { class: 'qs-why', id: 'wq-compile', text: `To ${cert.basis} at level ${cert.level}: ${cert.verdict}. Within ${e.bound} of the program in the operator norm (budget ${e.budget}).` }), + el('table', { class: 'qs-table', id: 'wq-passes' }, el('thead', {}, el('tr', {}, ...['pass', 'before', 'after', 'check'].map((t) => el('th', { text: t })))), + el('tbody', {}, ...(cert.passes || []).map((p) => el('tr', {}, el('th', { scope: 'row', text: p.pass }), el('td', { text: counts(p.before) }), el('td', { text: counts(p.after) }), el('td', { text: `${p.check?.verdict || ''}${p.check?.distance ? ` (${Number(p.check.distance).toExponential(2)})` : ''}` }))))), + cert.reading ? note(cert.reading) : null)); + const rc = receiptBlock({ request: sc.request, result: sc.result }, { title: 'Certificate', what: 'this compile' }); + checked = rc.checked; + if (c) blocks.push(c.node); + blocks.push(rc.node); + } else if (Array.isArray(sc.repairs)) { + // A repair: each change with its code and lines, and what needs a person. + blocks.push(block('Repairs', sc.repairs.length ? el('ul', { class: 'qs-smells', id: 'wq-repairs' }, ...sc.repairs.map((r) => el('li', { class: 'qs-smell', 'data-code': r.code }, + el('span', { class: 'qs-smell-text', text: `${r.code}: ${r.rule}` }), + el('span', { class: 'qs-smell-where qs-note', text: (r.edits || []).map((x) => `line ${x.line}: ${x.before} → ${x.after}`).join('; ') })))) : note('Nothing to repair.'))); + if (sc.needs_person?.length) blocks.push(block('Needs a person', el('ul', { class: 'qs-smells' }, ...sc.needs_person.map((p) => el('li', { class: 'qs-smell' }, el('span', { class: 'qs-smell-text', text: `${p.code}: ${p.message || p.advice || ''}` })))))); + if (c) blocks.push(c.node); + } else if (sc.ok === false) { + blocks.push(block('Refused', el('p', { class: 'qs-warn', id: 'wq-refusal', text: sc.error }), note(`${sc.code} on line ${sc.line}`, { id: 'wq-code' }))); + } else { + if (sc.qubits != null) blocks.push(block('Shape', el('p', { class: 'qs-why', id: 'wq-shape', text: `${sc.qubits} qubits, ${sc.gates} gates, ${sc.bits} classical bits, ${sc.measured} measured` }))); + if (c) blocks.push(c.node); + } + if (!c && tool !== 'compile_qasm' && data?.why) blocks.push(block('Circuit', note(data.why, { id: 'wq-not-drawn' }))); + blocks.push(programBlock(data?.program, tool === 'compile_qasm' ? 'Compiled program' : 'Program')); + return { blocks, checked, state: sc.ok === false ? 'refused' : 'shown' }; +} + +// A critique: the measures, the smells with their lines, marked on the circuit. +function critique({ res, data }) { + const sc = res.structuredContent || {}; + if (res.isError || sc.ok === false) return { blocks: refusalBlocks(sc, res), state: 'refused' }; + const c = circuitBlock(data); + const smells = el('ul', { class: 'qs-smells', id: 'wq-smells' }, ...(sc.smells || []).map((s) => { + const li = el('li', { class: 'qs-smell', 'data-kind': s.kind, tabindex: '0' }, + el('span', { class: 'qs-smell-text', text: s.message }), + el('span', { class: 'qs-smell-where qs-note', text: s.lines?.length ? `line${s.lines.length > 1 ? 's' : ''} ${s.lines.join(', ')}` : (s.qubits || []).map((q) => `q${q}`).join(' ') })); + if (c) for (const [ev, on] of [['mouseenter', true], ['focusin', true], ['mouseleave', false], ['focusout', false]]) li.addEventListener(ev, () => mark(c.svg, 'qc-smell', on ? s.ops || [] : [], on ? s.qubits || [] : [])); + return li; + })); + const blocks = [ + block('Measures', el('div', { class: 'qs-chips', id: 'wq-measures' }, ...chips(sc), sc.clifford ? el('span', { class: 'qs-badge ok', text: 'Clifford' }) : null, fidelityChip(sc))), + block(`Smells · ${(sc.smells || []).length}`, (sc.smells || []).length ? smells : note('None found.')), + c?.node, + sc.verdict ? block('Verdict', verdictLine(sc.verdict, { sealed: false, id: 'wq-verdict' })) : null, + ]; + return { blocks, state: 'shown' }; +} + +// An estimate: the headline, the counts, the frontier, and every seal checked here. +function estimate({ res }) { + const sc = res.structuredContent || {}; + if (res.isError || !sc.result) return { blocks: refusalBlocks(sc, res), state: 'refused' }; + const r = sc.result; + const e = r.estimate || {}; + const c = r.counts || {}; + const points = r.frontier || []; + const head = Math.max(0, points.findIndex((p) => p.headline)); + const pointed = note('Point at an estimate to read it.', { id: 'wq-point' }); + const svg = points.length ? frontierSvg(points, head, (i) => { const p = points[i]; pointed.textContent = `slowdown ${p.slowdown}: ${count(p.physical_qubits)} physical qubits, ${duration(p.runtime_s)}, distance ${p.distance}, ${p.factories} factories`; }) : null; + const rc = receiptBlock({ request: sc.request, result: r }, { title: 'Seal', what: 'this estimate' }); + const blocks = [ + block('Estimate', el('p', { class: 'qs-why', id: 'wq-headline', text: `${count(e.physical_qubits)} physical qubits, ${e.factories} factories (${Math.round(100 * (e.factory_fraction || 0))}% of the qubits), ${duration(e.runtime_s)} at code distance ${e.distance}, on ${e.qubit} with the ${e.code}.` })), + block('Logical counts', el('div', { class: 'qs-chips', id: 'wq-counts' }, ...[['logical_qubits', 'logical qubits'], ['t_gates', 'T'], ['toffolis', 'Toffolis'], ['rotations', 'rotations'], ['rotation_t', 'T in the rotations'], ['t_depth', 'T depth']].filter(([k]) => c[k] != null).map(([k, label]) => el('span', { class: 'qs-chip' }, el('b', { text: count(c[k]) }), ` ${label}`)))), + svg ? block(`Frontier · ${points.length} estimate${points.length === 1 ? '' : 's'}`, el('div', { class: 'qs-frontier-figure', id: 'wq-frontier' }, svg), pointed) : null, + rc.node, + ...(r.notes || []).map((n) => note(n)), + ]; + return { blocks, checked: rc.checked, state: 'shown' }; +} + +// A device target: its coupling map coloured by its errors, and its summary. +function target({ res, data }) { + const sc = res.structuredContent || {}; + if (res.isError || sc.error) return { blocks: refusalBlocks(sc, res), state: 'refused' }; + const view = sc.view || data?.target_view; + const map = view ? couplingSvg(view, { by: 'readout', width: 420 }) : null; + const gates = (sc.gates || []).filter((g) => g.error).map((g) => `${g.gate} ${g.error.median}`).join(', '); + const blocks = [ + block('Device target', el('table', { class: 'qs-table' }, el('tbody', {}, + row('target', sc.name, { id: 'wq-target-name' }), + row('hash', sc.hash, { class: 'mono', id: 'wq-target-hash' }), + row('qubits, pairs', `${sc.qubits}, ${sc.pairs}`), + sc.native?.basis ? row('compiles to', sc.native.basis) : null, + gates ? row('median errors', gates) : null, + sc.calibration?.id ? row('calibration', sc.calibration.id) : null)), + sc.source?.note ? note(sc.source.note) : null), + map ? block('Coupling map', el('div', { class: 'qs-dev-map', id: 'wq-map' }, map.svg), legend(map.pairBounds, map.qubitBounds, map.colour), table(view)) : block('Coupling map', note('The answer holds no map to draw.')), + ]; + return { blocks, state: 'shown' }; +} + +export const VIEWS = { result, receipt, circuit, critique, estimate, target }; + +export function render(view, input) { + const f = VIEWS[view]; + if (!f) return { blocks: [note(`No view ${view}.`)], state: 'refused' }; + const out = f(input); + return { ...out, blocks: out.blocks.filter(Boolean) }; +} diff --git a/wai/quantum-studio/src/views/views.css b/wai/quantum-studio/src/views/views.css new file mode 100644 index 000000000..0cdeab341 --- /dev/null +++ b/wai/quantum-studio/src/views/views.css @@ -0,0 +1,14 @@ +/* A view inside an agent host: the Studio's components (studio.css, on the + site's tokens) in a narrow column that sizes itself to its content. */ +html, body { margin: 0; padding: 0; background: var(--bg-base); color: var(--text-primary); } +body { font: 13px/1.5 var(--font-sans); } +#wq { display: flex; flex-direction: column; gap: 14px; padding: 12px 14px 16px; max-width: 760px; box-sizing: border-box; } +.wq-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; border-bottom: 1px solid var(--border); padding-bottom: 6px; } +.wq-name { font: 600 11px var(--font-mono, ui-monospace, monospace); letter-spacing: 0.08em; color: var(--accent-text); text-transform: uppercase; } +.wq-what { font-weight: 600; color: var(--text-primary); } +.wq-circuit { overflow-x: auto; max-width: 100%; } +.wq-circuit svg { max-width: none; } +.wq-words summary, .wq-program summary { cursor: pointer; font: 11px var(--font-mono, ui-monospace, monospace); color: var(--text-secondary); } +.wq-src { margin: 6px 0 0; padding: 8px; background: var(--bg-surface); border: 1px solid var(--border); border-radius: 6px; font: 12px var(--font-mono, ui-monospace, monospace); overflow-x: auto; white-space: pre; } +#wq .qs-table th[scope="row"] { white-space: nowrap; width: 1%; overflow-wrap: normal; } +@media (max-width: 420px) { #wq { padding: 10px 10px 14px; } } diff --git a/wai/quantum-studio/src/visual-edit.js b/wai/quantum-studio/src/visual-edit.js index 0b6fae6bc..1696dbd49 100644 --- a/wai/quantum-studio/src/visual-edit.js +++ b/wai/quantum-studio/src/visual-edit.js @@ -10,6 +10,7 @@ // wires above and below, ← and → before the gate before it and after the gate // after it, and Delete removes it. import { el } from './dom.js'; +import { t } from './i18n.js'; // The gates the palette offers: each one's name, its label, its angles, and how many wires. export const PALETTE = [ @@ -29,17 +30,17 @@ export function circuitEditor({ engine, program, apply, status }) { let armed = null; // a palette gate chosen by click or keyboard, waiting for a wire const chips = PALETTE.map((g) => { - const b = el('button', { class: 'qs-chip qs-gate-chip', type: 'button', draggable: 'true', 'data-gate': g.name, text: g.label, title: `Drag ${g.label} onto a wire, or choose it and then a place on the diagram`, 'aria-pressed': 'false' }); + const b = el('button', { class: 'qs-chip qs-gate-chip', type: 'button', draggable: 'true', 'data-gate': g.name, text: g.label, title: t('visual_edit.chip_title', { gate: g.label }), 'aria-pressed': 'false' }); b.addEventListener('dragstart', (ev) => { ev.dataTransfer.setData(TYPE, JSON.stringify(g)); ev.dataTransfer.effectAllowed = 'copy'; }); b.addEventListener('click', () => { armed = armed === g ? null : g; for (const c of node.querySelectorAll('.qs-gate-chip')) c.setAttribute('aria-pressed', String(c === b && armed === g)); - if (armed) status(`Now choose a place on a wire for ${g.label}: click the diagram.`); + if (armed) status(t('visual_edit.armed', { gate: g.label })); }); return b; }); - const del = el('button', { class: 'qs-btn qs-small', id: 'qs-gate-delete', type: 'button', text: 'Delete gate', disabled: true, title: 'Delete the selected gate (Delete)' }); - const node = el('div', { class: 'qs-palette', id: 'qs-palette', role: 'toolbar', 'aria-label': 'Gates to put on the circuit' }, el('span', { class: 'qs-k', text: 'Edit by drawing' }), ...chips, del); + const del = el('button', { class: 'qs-btn qs-small', id: 'qs-gate-delete', type: 'button', text: t('visual_edit.delete'), disabled: true, title: t('visual_edit.delete_title') }); + const node = el('div', { class: 'qs-palette', id: 'qs-palette', role: 'toolbar', 'aria-label': t('visual_edit.palette_label') }, el('span', { class: 'qs-k', text: t('visual_edit.edit_by_drawing') }), ...chips, del); const point = (ev) => { const p = svg.createSVGPoint(); @@ -56,7 +57,7 @@ export function circuitEditor({ engine, program, apply, status }) { const v = await engine.visualEdit(program(), req); if (apply(v) === false) return; select = v.gate ?? null; - status(`${v.summary[0].toUpperCase()}${v.summary.slice(1)}: an edit to the program's text, recorded as yours.`, 'ok'); + status(t('visual_edit.edited', { summary: `${v.summary[0].toUpperCase()}${v.summary.slice(1)}` }), 'ok'); } catch (e) { status(e.message, 'bad'); } @@ -65,7 +66,7 @@ export function circuitEditor({ engine, program, apply, status }) { // A palette gate put at a point on the diagram. function insertAt(g, p) { const arity = g.arity ?? 1; - if (geom.n < arity) { status(`${g.label} takes ${arity} wires; the program has ${geom.n}.`, 'bad'); return; } + if (geom.n < arity) { status(t('visual_edit.too_few_wires', { gate: g.label, arity, wires: geom.n }), 'bad'); return; } const q = wire(p.y); const qubits = arity === 1 ? [q] : q + 1 < geom.n ? [q, q + 1] : [q - 1, q]; const later = gatesIn(column(p.x), geom.cols.length); @@ -88,7 +89,7 @@ export function circuitEditor({ engine, program, apply, status }) { function moveBy(g, dq) { const qs = qubitsOf(g).map((q) => q + dq); - if (qs.some((q) => q < 0 || q >= geom.n)) { status('No wire there.', 'bad'); return; } + if (qs.some((q) => q < 0 || q >= geom.n)) { status(t('visual_edit.no_wire'), 'bad'); return; } edit({ op: 'move', gate: opOf(g), qubits: qs }); } @@ -96,10 +97,13 @@ export function circuitEditor({ engine, program, apply, status }) { function attach(drawn, geometry) { svg = drawn; geom = geometry; del.disabled = true; + // An image's parts are not read one by one, so a diagram whose gates are + // buttons is a group of them, keeping its name. + if (svg && items().length) svg.setAttribute('role', 'group'); for (const g of items()) { g.setAttribute('tabindex', '0'); g.setAttribute('role', 'button'); - g.setAttribute('aria-label', `${g.querySelector('title')?.textContent ?? 'a gate'}: arrows move it, Delete removes it`); + g.setAttribute('aria-label', t('visual_edit.gate_label', { gate: g.querySelector('title')?.textContent ?? t('visual_edit.a_gate') })); g.addEventListener('keydown', (ev) => { const i = opOf(g); if (ev.key === 'Delete' || ev.key === 'Backspace') edit({ op: 'delete', gate: i }); @@ -137,7 +141,7 @@ export function circuitEditor({ engine, program, apply, status }) { const req = { op: 'move', gate: i }; if (dq) { const qs = qubitsOf(g).map((q) => q + dq); - if (qs.some((q) => q < 0 || q >= geom.n)) { status('No wire there.', 'bad'); return; } + if (qs.some((q) => q < 0 || q >= geom.n)) { status(t('visual_edit.no_wire'), 'bad'); return; } req.qubits = qs; } if (col < from.col) { diff --git a/wai/quantum-studio/src/workbench.js b/wai/quantum-studio/src/workbench.js index 7f7b55f6b..9cda606f3 100644 --- a/wai/quantum-studio/src/workbench.js +++ b/wai/quantum-studio/src/workbench.js @@ -19,6 +19,7 @@ // kept, it is offered as a reason to revise the rule. What is worth a second // look goes to Found. import { el } from './dom.js'; +import { t } from './i18n.js'; import { declared } from './generators.js'; import { noveltyLabel, unaskedText } from './novelty.js'; import { tastePanel } from './taste.js'; @@ -36,15 +37,15 @@ const svg = (tag, attrs = {}, ...kids) => { // The measures a cost or an axis may name, with the words the Studio shows. export const MEASURES = { - depth: 'depth', - two_qubit: 'two-qubit gates', - t_count: 'T gates', - gates: 'gates', - two_qubit_depth: 'two-qubit depth', - one_qubit: 'one-qubit gates', - three_qubit: 'three-qubit gates', - non_clifford: 'non-Clifford gates', - qubits: 'qubits', + depth: t('workbench.measure_depth'), + two_qubit: t('workbench.measure_two_qubit'), + t_count: t('workbench.measure_t_count'), + gates: t('workbench.measure_gates'), + two_qubit_depth: t('workbench.measure_two_qubit_depth'), + one_qubit: t('workbench.measure_one_qubit'), + three_qubit: t('workbench.measure_three_qubit'), + non_clifford: t('workbench.measure_non_clifford'), + qubits: t('workbench.measure_qubits'), }; // The sizes a reader writes ("n = 3, 5, 8", or "3, 5, 8" for a generator with @@ -57,14 +58,14 @@ export function parseSizes(text, names) { for (const part of parts) { const m = part.match(/^(?:([A-Za-z_]\w*)\s*=\s*)?(.+)$/); const name = m[1] ?? (names.length === 1 ? names[0] : null); - if (!name) return { error: `name each input: ${names.map((n) => `${n} = 3, 5`).join('; ')}` }; - if (!names.includes(name)) return { error: `the program takes no input ${name}; it takes ${names.join(', ')}` }; + if (!name) return { error: t('workbench.sizes_name_each', { list: names.map((n) => `${n} = 3, 5`).join('; ') }) }; + if (!names.includes(name)) return { error: t('workbench.sizes_no_input', { name, names: names.join(', ') }) }; const values = m[2].split(/[\s,]+/).filter(Boolean).map(Number); - if (!values.length || values.some((v) => !Number.isInteger(v) || v < 0)) return { error: `${name}: whole numbers, 0 or more, as ${name} = 3, 5, 8` }; + if (!values.length || values.some((v) => !Number.isInteger(v) || v < 0)) return { error: t('workbench.sizes_whole', { name }) }; out[name] = [...new Set(values)].sort((a, b) => a - b); } const missing = names.filter((n) => !out[n]); - if (missing.length) return { error: `give values for ${missing.join(', ')}` }; + if (missing.length) return { error: t('workbench.sizes_missing', { names: missing.join(', ') }) }; return { inputs: out }; } @@ -74,7 +75,7 @@ export function specOf({ reference, task, sizes, gates, connectivity, weights, a const s = parseSizes(sizes, names); if (s.error) return { error: s.error }; const cost = Object.fromEntries(Object.entries(weights).filter(([, w]) => Number(w) > 0).map(([k, w]) => [k, Number(w)])); - if (!Object.keys(cost).length) return { error: 'give a weight above 0 to at least one measure of the cost' }; + if (!Object.keys(cost).length) return { error: t('workbench.cost_needs_weight') }; const allowed = gates.split(/[\s,]+/).map((g) => g.trim().toLowerCase()).filter(Boolean); const spec = { name: 'studio', task, reference, inputs: s.inputs, connectivity, cost, axes }; if (allowed.length) spec.gates = allowed; @@ -90,7 +91,7 @@ export function archiveMap(summary, axes, selected, onPick) { const xmax = Math.max(1, ...xs) * 1.15, ymax = Math.max(1, ...ys) * 1.15; const sx = (v) => L + (v / xmax) * (W - L - R), sy = (v) => H - B - (v / ymax) * (H - B - T); const ticks = (max) => { const step = Math.max(1, Math.ceil(max / 4)); const out = []; for (let v = 0; v <= max; v += step) out.push(v); return out; }; - const g = svg('svg', { class: 'qs-atlas qs-wb-map', id: 'qs-wb-map', viewBox: `0 0 ${W} ${H}`, role: 'img', 'aria-label': `The archive: ${cells.length} cells by ${MEASURES[axes[0]] ?? axes[0]} and ${MEASURES[axes[1]] ?? axes[1]}` }); + const g = svg('svg', { class: 'qs-atlas qs-wb-map', id: 'qs-wb-map', viewBox: `0 0 ${W} ${H}`, role: 'img', 'aria-label': t('workbench.map_label', { n: cells.length, x: MEASURES[axes[0]] ?? axes[0], y: MEASURES[axes[1]] ?? axes[1] }) }); for (const v of ticks(xmax)) g.append(svg('line', { x1: sx(v), y1: T, x2: sx(v), y2: H - B, class: 'qs-gridline' }), svg('text', { x: sx(v), y: H - B + 13, class: 'qs-tick', 'text-anchor': 'middle' }, String(v))); for (const v of ticks(ymax)) g.append(svg('line', { x1: L, y1: sy(v), x2: W - R, y2: sy(v), class: 'qs-gridline' }), svg('text', { x: L - 6, y: sy(v) + 3, class: 'qs-tick', 'text-anchor': 'end' }, String(v))); g.append(svg('text', { x: W - R, y: H - 2, class: 'qs-axis', 'text-anchor': 'end' }, `${MEASURES[axes[0]] ?? axes[0]} →`), @@ -101,8 +102,8 @@ export function archiveMap(summary, axes, selected, onPick) { const rank = { mutate: 0, model: 1, person: 1, ref: 2, best: 3 }; for (const c of [...cells].sort((a, b) => rank[kindOf(a)] - rank[kindOf(b)])) { const kind = kindOf(c); - const p = svg('g', { class: `qs-pt qs-wb-${kind}${c.id === selected ? ' qs-sel' : ''}`, tabindex: '0', role: 'button', 'data-id': c.id, 'aria-label': `${c.id}, by ${c.proposer}: cost ${num(c.cost)}, ${c.axes[0]} and ${c.axes[1]}` }, - svg('title', {}, `${c.id} (${c.proposer}): cost ${num(c.cost)}`), + const p = svg('g', { class: `qs-pt qs-wb-${kind}${c.id === selected ? ' qs-sel' : ''}`, tabindex: '0', role: 'button', 'data-id': c.id, 'aria-label': t('workbench.point_label', { id: c.id, proposer: c.proposer, cost: num(c.cost), x: c.axes[0], y: c.axes[1] }) }, + svg('title', {}, t('workbench.point_title', { id: c.id, proposer: c.proposer, cost: num(c.cost) })), svg('circle', { cx: sx(c.axes[0]), cy: sy(c.axes[1]), r: 8, class: 'qs-hit' }), svg('circle', { cx: sx(c.axes[0]), cy: sy(c.axes[1]), r: kind === 'ref' ? 6 : 4.5, class: kind === 'ref' ? 'qs-ring' : 'qs-dot' })); p.addEventListener('click', () => onPick(c.id)); @@ -114,12 +115,12 @@ export function archiveMap(summary, axes, selected, onPick) { // One line for an outcome of a submission. export function outcomeText(o, who) { - if (o.duplicate) return `${who}: seen before, not evaluated again`; + if (o.duplicate) return t('workbench.outcome_duplicate', { who }); const e = o.evaluation; - if (!e?.verified) return `${who}: not verified: ${e?.failure ?? 'it did not read'}`; - if (o.kept && o.improvement) return `${who}: kept as ${o.kept}, cheaper than every generator before it (cost ${num(e.cost)})`; - if (o.kept) return `${who}: kept as ${o.kept}, the best of its cell (cost ${num(e.cost)})`; - return `${who}: verified (cost ${num(e.cost)}), but its cell already holds one as cheap`; + if (!e?.verified) return t('workbench.outcome_not_verified', { who, failure: e?.failure ?? t('workbench.outcome_did_not_read') }); + if (o.kept && o.improvement) return t('workbench.outcome_improvement', { who, id: o.kept, cost: num(e.cost) }); + if (o.kept) return t('workbench.outcome_kept', { who, id: o.kept, cost: num(e.cost) }); + return t('workbench.outcome_verified', { who, cost: num(e.cost) }); } // Who a proposer is, as the program's history records it. @@ -135,33 +136,33 @@ const byOf = (p) => (p === 'mutate' ? { kind: 'engine', name: 'the blind search' // `features.on(name)` says whether a feature is on (flags.js); `onChoice` hears // which kept generator the reader opened, by its rank in cost. export function workbenchPane({ engine, draft, model, apply, save, found = null, taste = null, paused = () => false, features = { on: () => true }, onChoice = null }) { - const startBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-wb-start', type: 'button', text: 'Start', title: 'A new archive, seeded with the program in the editor as the reference' }); - const task = el('select', { class: 'qs-input qs-small', id: 'qs-wb-task', 'aria-label': 'What each member must do' }, - el('option', { value: 'prepares', text: 'prepare the same state from |0…0⟩' }), el('option', { value: 'equivalent', text: 'be the same circuit (unitary)' })); - const sizes = el('input', { class: 'qs-input', id: 'qs-wb-sizes', value: 'n = 3, 5, 8', autocomplete: 'off', 'aria-label': 'The sizes to check at' }); - const gates = el('input', { class: 'qs-input', id: 'qs-wb-gates', placeholder: 'any', value: 'h, cx', autocomplete: 'off', 'aria-label': 'The gates a proposal may use' }); - const conn = el('select', { class: 'qs-input qs-small', id: 'qs-wb-conn', 'aria-label': 'Which qubits a gate may join' }, - el('option', { value: 'all', text: 'any pair' }), el('option', { value: 'line', text: 'neighbours on a line' })); - const weight = (k, v) => el('input', { class: 'qs-input qs-small qs-wb-w', type: 'number', min: '0', step: '0.1', value: String(v), id: `qs-wb-w-${k}`, 'aria-label': `the cost's weight on ${MEASURES[k]}` }); + const startBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-wb-start', type: 'button', text: t('workbench.start'), title: t('workbench.start_title') }); + const task = el('select', { class: 'qs-input qs-small', id: 'qs-wb-task', 'aria-label': t('workbench.task_label') }, + el('option', { value: 'prepares', text: t('workbench.task_prepares') }), el('option', { value: 'equivalent', text: t('workbench.task_equivalent') })); + const sizes = el('input', { class: 'qs-input', id: 'qs-wb-sizes', value: 'n = 3, 5, 8', autocomplete: 'off', 'aria-label': t('workbench.sizes_label') }); + const gates = el('input', { class: 'qs-input', id: 'qs-wb-gates', placeholder: t('workbench.gates_any'), value: 'h, cx', autocomplete: 'off', 'aria-label': t('workbench.gates_label') }); + const conn = el('select', { class: 'qs-input qs-small', id: 'qs-wb-conn', 'aria-label': t('workbench.conn_label') }, + el('option', { value: 'all', text: t('workbench.conn_all') }), el('option', { value: 'line', text: t('workbench.conn_line') })); + const weight = (k, v) => el('input', { class: 'qs-input qs-small qs-wb-w', type: 'number', min: '0', step: '0.1', value: String(v), id: `qs-wb-w-${k}`, 'aria-label': t('workbench.weight_label', { measure: MEASURES[k] }) }); const weights = { depth: weight('depth', 1), two_qubit: weight('two_qubit', 0.1), t_count: weight('t_count', 0), gates: weight('gates', 0) }; - const axis = (id, chosen) => el('select', { class: 'qs-input qs-small', id, 'aria-label': id.endsWith('x') ? 'The map’s first axis' : 'The map’s second axis' }, + const axis = (id, chosen) => el('select', { class: 'qs-input qs-small', id, 'aria-label': id.endsWith('x') ? t('workbench.axis_first') : t('workbench.axis_second') }, ...Object.entries(MEASURES).map(([k, w]) => el('option', { value: k, text: w, selected: k === chosen }))); const ax = axis('qs-wb-ax', 'two_qubit'), ay = axis('qs-wb-ay', 'depth'); - const form = el('details', { class: 'qs-wb-spec', id: 'qs-wb-spec', open: true }, el('summary', { text: 'The specification' }), + const form = el('details', { class: 'qs-wb-spec', id: 'qs-wb-spec', open: true }, el('summary', { text: t('workbench.spec') }), el('div', { class: 'qs-wb-form' }, - el('label', {}, el('span', { text: 'Each member must' }), task), - el('label', {}, el('span', { text: 'checked at' }), sizes), - el('label', {}, el('span', { text: 'using' }), gates), - el('label', {}, el('span', { text: 'on' }), conn), - el('div', { class: 'qs-wb-cost' }, el('span', { text: 'Cost, summed over the sizes:' }), + el('label', {}, el('span', { text: t('workbench.each_member_must') }), task), + el('label', {}, el('span', { text: t('workbench.checked_at') }), sizes), + el('label', {}, el('span', { text: t('workbench.using') }), gates), + el('label', {}, el('span', { text: t('workbench.on') }), conn), + el('div', { class: 'qs-wb-cost' }, el('span', { text: t('workbench.cost_summed') }), ...Object.entries(weights).map(([k, i]) => el('label', {}, i, el('span', { text: `× ${MEASURES[k]}` })))), - el('label', {}, el('span', { text: 'Map' }), ax, el('span', { text: 'against' }), ay))); - const searchBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-wb-search', type: 'button', text: 'Search ×100', title: 'The built-in, blind search: a hundred seeded edits of the archive’s best, each checked by the engine' }); - const askBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-wb-ask', type: 'button', text: 'Ask the model ×3', title: 'Three generators from the model, from the archive’s best; the engine judges each' }); - const submitBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-wb-submit', type: 'button', text: 'Submit the editor’s program', title: 'The program in the editor, judged and kept if it is the best of its cell' }); - const saveBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-wb-save', type: 'button', text: 'Download', title: 'The specification and its archive, every kept generator with its sealed verdict, for wai-quantum discover verify' }); + el('label', {}, el('span', { text: t('workbench.map') }), ax, el('span', { text: t('workbench.against') }), ay))); + const searchBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-wb-search', type: 'button', text: t('workbench.search'), title: t('workbench.search_title') }); + const askBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-wb-ask', type: 'button', text: t('workbench.ask'), title: t('workbench.ask_title') }); + const submitBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-wb-submit', type: 'button', text: t('workbench.submit'), title: t('workbench.submit_title') }); + const saveBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-wb-save', type: 'button', text: t('workbench.download'), title: t('workbench.download_title') }); const acts = el('div', { class: 'qs-wb-acts', hidden: true }, searchBtn, askBtn, submitBtn, saveBtn); - const stats = el('p', { class: 'qs-note', id: 'qs-wb-stats', text: 'Write a generator in the editor (a program with input uint n;), say what each member of its family must do and what it costs, and start. A program with no inputs works too: one circuit, one size.' }); + const stats = el('p', { class: 'qs-note', id: 'qs-wb-stats', text: t('workbench.intro') }); const mapBox = el('div', { class: 'qs-wb-mapbox', id: 'qs-wb-mapbox' }); const entry = el('div', { class: 'qs-wb-entry', id: 'qs-wb-entry' }); const log = el('ol', { class: 'qs-wb-log', id: 'qs-wb-log', 'aria-live': 'polite' }); @@ -171,7 +172,7 @@ export function workbenchPane({ engine, draft, model, apply, save, found = null, const elites = () => (archive ? Object.values(archive.cells).map((i) => archive.entries[i]).sort((a, b) => a.cost - b.cost).map((e) => ({ name: e.id, qasm: e.source, cost: e.cost })) : null); const tasteUi = taste ? tastePanel({ store: taste, candidates: elites, inputs: largest, task: () => spec?.task ?? 'equivalent', onPick: (id) => pick(id) }) : null; const node = el('section', { class: 'qs-pane qs-wb', 'aria-labelledby': 'qs-wb-h' }, - el('div', { class: 'qs-head' }, el('h2', { id: 'qs-wb-h', text: 'Workbench' }), el('span', { class: 'qs-grow' }), startBtn), + el('div', { class: 'qs-head' }, el('h2', { id: 'qs-wb-h', text: t('workbench.title') }), el('span', { class: 'qs-grow' }), startBtn), ...[form, acts, stats, mapBox, entry, tasteUi?.node, log].filter(Boolean)); const say = (text, cls = '', ...more) => { log.prepend(el('li', { class: cls }, text, ...more)); while (log.children.length > 12) log.lastChild.remove(); }; @@ -182,9 +183,10 @@ export function workbenchPane({ engine, draft, model, apply, save, found = null, function render() { if (!summary) return; const st = summary.stats; - const energy = st.joules != null ? `, ${st.joules.toFixed(2)} J` : ''; - stats.textContent = `${st.evaluations} evaluated, ${st.verified} verified, ${st.kept} kept, ${st.improvements} improvement${st.improvements === 1 ? '' : 's'}${energy}. ` - + `Best ${summary.best.id}: cost ${num(summary.best.cost)} against the reference’s ${num(summary.reference.cost)}, by ${summary.best.proposer}, checked by ${summary.best.strength}.`; + const energy = st.joules != null ? t('workbench.stats_energy', { joules: st.joules.toFixed(2) }) : ''; + const improvements = st.improvements === 1 ? t('workbench.improvement_one', { n: st.improvements }) : t('workbench.improvement_many', { n: st.improvements }); + stats.textContent = t('workbench.stats', { evaluations: st.evaluations, verified: st.verified, kept: st.kept, improvements, energy }) + + t('workbench.stats_best', { id: summary.best.id, cost: num(summary.best.cost), reference: num(summary.reference.cost), proposer: summary.best.proposer, strength: summary.best.strength }); mapBox.replaceChildren(archiveMap(summary, spec.axes, selected, pick)); acts.hidden = false; if (selected) pick(selected); @@ -211,7 +213,7 @@ export function workbenchPane({ engine, draft, model, apply, save, found = null, if (!e) return; selected = id; for (const p of mapBox.querySelectorAll('.qs-pt')) p.classList.toggle('qs-sel', p.dataset.id === id); - const open = el('button', { class: 'qs-btn qs-small', id: 'qs-wb-open', type: 'button', text: 'Open in editor' }); + const open = el('button', { class: 'qs-btn qs-small', id: 'qs-wb-open', type: 'button', text: t('workbench.open') }); open.addEventListener('click', () => { openedFrom = id; const ranked = elites() ?? []; @@ -228,12 +230,12 @@ export function workbenchPane({ engine, draft, model, apply, save, found = null, nv ? el('p', { class: 'qs-note', text: nv.why }) : null, u ? el('p', { class: 'qs-note', text: u }) : null].filter(Boolean)); }).catch(() => {}); entry.replaceChildren(...[ - el('div', { class: 'qs-cand-head' }, el('span', { class: 'qs-cand-title', text: `${e.id} · cost ${num(e.cost)} · ${MEASURES[spec.axes[0]]} ${e.axes[0]}, ${MEASURES[spec.axes[1]]} ${e.axes[1]}` }), - el('span', { class: 'qs-badge ok', text: `checked by ${e.strength}${e.sealed ? ', sealed' : ''}` }), el('span', { class: 'qs-grow' }), open), + el('div', { class: 'qs-cand-head' }, el('span', { class: 'qs-cand-title', text: t('workbench.entry_title', { id: e.id, cost: num(e.cost), x_measure: MEASURES[spec.axes[0]], x_value: e.axes[0], y_measure: MEASURES[spec.axes[1]], y_value: e.axes[1] }) }), + el('span', { class: 'qs-badge ok', text: t('workbench.checked_by', { strength: e.strength, sealed: e.sealed ? t('workbench.sealed') : '' }) }), el('span', { class: 'qs-grow' }), open), e.note ? el('p', { class: 'qs-wb-note', text: e.note }) : null, novel, - el('div', { class: 'qs-wb-chain', 'aria-label': 'Where it came from' }, ...chain), - el('details', {}, el('summary', { text: 'Its generator' }), el('pre', { class: 'qs-ai-code', text: e.source })), + el('div', { class: 'qs-wb-chain', 'aria-label': t('workbench.lineage') }, ...chain), + el('details', {}, el('summary', { text: t('workbench.its_generator') }), el('pre', { class: 'qs-ai-code', text: e.source })), ].filter(Boolean)); } @@ -243,14 +245,14 @@ export function workbenchPane({ engine, draft, model, apply, save, found = null, const made = specOf({ reference, task: task.value, sizes: sizes.value, gates: gates.value, connectivity: conn.value, weights: Object.fromEntries(Object.entries(weights).map(([k, i]) => [k, i.value])), axes: [ax.value, ay.value] }); if (made.error) { stats.textContent = made.error; return; } setBusy(true); - stats.textContent = 'The engine is checking the reference at every size…'; + stats.textContent = t('workbench.checking_reference'); try { const r = await engine.discoverStart(made.spec); ({ archive, summary } = r); spec = made.spec; selected = null; openedFrom = null; log.replaceChildren(); - say(`Started: the reference is ${summary.reference.id}, cost ${num(summary.reference.cost)}.`); + say(t('workbench.started', { id: summary.reference.id, cost: num(summary.reference.cost) })); form.open = false; persist(); render(); @@ -269,7 +271,7 @@ export function workbenchPane({ engine, draft, model, apply, save, found = null, if (broke.kind === 'gates' && broke.gate) gates.value = [...new Set([...gates.value.split(/[\s,]+/).filter(Boolean), broke.gate])].join(', '); else if (broke.kind === 'connectivity') conn.value = 'all'; form.open = true; - stats.textContent = `The specification now ${broke.kind === 'gates' ? `allows ${broke.gate}` : 'lets a gate join any pair'}. This archive was built under the old rule; start a new one to search under the new.`; + stats.textContent = t('workbench.loosened', { change: broke.kind === 'gates' ? t('workbench.loosened_allows', { gate: broke.gate }) : t('workbench.loosened_any_pair') }); startBtn.focus(); } @@ -279,8 +281,8 @@ export function workbenchPane({ engine, draft, model, apply, save, found = null, const v = await engine.discoverNovelty?.(spec, archive, id).catch(() => null); const nv = noveltyLabel(v, { verified: true }); if (!nv) return; - say(`${id}: ${nv.text}.`, nv.flag ? 'find' : ''); - if (nv.flag) found?.flag({ kind: nv.flag, title: `${id} (${who}): ${nv.text}`, why: nv.why, where: 'Workbench', source: entryById(id)?.source, by }); + say(t('workbench.novelty_said', { id, text: nv.text }), nv.flag ? 'find' : ''); + if (nv.flag) found?.flag({ kind: nv.flag, title: `${id} (${who}): ${nv.text}`, why: nv.why, where: t('workbench.title'), source: entryById(id)?.source, by }); } // What a judged candidate adds to its outcome: an aberration's invitation to @@ -288,11 +290,11 @@ export function workbenchPane({ engine, draft, model, apply, save, found = null, async function after(o, who, source, by) { const ab = features.on('novelty') ? o.evaluation?.aberration : null; if (ab) { - const broke = ab.broke?.kind === 'gates' ? `it uses ${ab.broke.gate}` : ab.broke?.kind === 'connectivity' ? 'it joins qubits that are not neighbours' : 'it breaks a rule'; - const why = `It breaks only one rule (${ab.rule}); without it, it would be verified (${ab.strength}) at cost ${num(ab.cost)}, ${MEASURES[spec.axes[0]]} ${ab.axes[0]}, ${MEASURES[spec.axes[1]]} ${ab.axes[1]}.`; - const tryBtn = ab.broke ? el('button', { class: 'qs-btn qs-small qs-wb-try', type: 'button', text: 'Try without that rule', onclick: () => loosen(ab.broke) }) : null; - say(`${who}: an aberration. ${why} The rule may be worth revising. `, 'find', tryBtn); - found?.flag({ kind: 'aberration', title: `${who}: ${broke}`, why, where: 'Workbench', source, by }); + const broke = ab.broke?.kind === 'gates' ? t('workbench.aberration_uses', { gate: ab.broke.gate }) : ab.broke?.kind === 'connectivity' ? t('workbench.aberration_joins') : t('workbench.aberration_breaks'); + const why = t('workbench.aberration_why', { rule: ab.rule, strength: ab.strength, cost: num(ab.cost), x_measure: MEASURES[spec.axes[0]], x_value: ab.axes[0], y_measure: MEASURES[spec.axes[1]], y_value: ab.axes[1] }); + const tryBtn = ab.broke ? el('button', { class: 'qs-btn qs-small qs-wb-try', type: 'button', text: t('workbench.try_without'), onclick: () => loosen(ab.broke) }) : null; + say(t('workbench.aberration_said', { who, why }), 'find', tryBtn); + found?.flag({ kind: 'aberration', title: `${who}: ${broke}`, why, where: t('workbench.title'), source, by }); } if (o.kept) await novelty(o.kept, who, by); } @@ -305,42 +307,42 @@ export function workbenchPane({ engine, draft, model, apply, save, found = null, const kept = []; try { for (let done = 0; done < total; done += chunk) { - stats.textContent = `The blind search: ${done} of ${total} edits judged…`; + stats.textContent = t('workbench.search_progress', { done, total }); // A fresh seed for every step, so a search taken up again does not replay. const r = await engine.discoverSearch(spec, archive, archive.stats.evaluations + archive.stats.duplicates + 1, chunk); ({ archive, summary } = r); kept.push(...(r.kept ?? [])); } - say(`The blind search: ${total} edits judged, ${summary.stats.kept - before} kept.`); + say(t('workbench.search_done', { total, kept: summary.stats.kept - before })); persist(); // Only what is worth a second look is said of the search's finds. for (const id of features.on('novelty') ? kept : []) { const nv = noveltyLabel(await engine.discoverNovelty?.(spec, archive, id).catch(() => null), { verified: true }); - if (nv?.flag) { say(`${id} (the blind search): ${nv.text}.`, 'find'); found?.flag({ kind: nv.flag, title: `${id} (the blind search): ${nv.text}`, why: nv.why, where: 'Workbench', source: entryById(id)?.source, by: byOf('mutate') }); } + if (nv?.flag) { say(t('workbench.search_find_said', { id, text: nv.text }), 'find'); found?.flag({ kind: nv.flag, title: t('workbench.search_find_title', { id, text: nv.text }), why: nv.why, where: t('workbench.title'), source: entryById(id)?.source, by: byOf('mutate') }); } } - } catch (e) { say(`The search stopped: ${e.message}`, 'bad'); } + } catch (e) { say(t('workbench.search_stopped', { error: e.message }), 'bad'); } finally { setBusy(false); render(); } } async function ask() { if (busy || !spec) return; const m = model(); - if (paused()) { say('You are exploring on your own: the model waits until you end that under Versions.', 'bad'); return; } - if (!m) { say('Ask the model needs one: connect it under “Model…” in the Assistant.', 'bad'); return; } + if (paused()) { say(t('workbench.paused'), 'bad'); return; } + if (!m) { say(t('workbench.needs_model'), 'bad'); return; } setBusy(true); - stats.textContent = 'The model is proposing; the engine will judge each…'; + stats.textContent = t('workbench.model_proposing'); try { const shown = Object.values(archive.cells).map((i) => archive.entries[i]).sort((a, b) => a.cost - b.cost).slice(0, 3); const ideas = await m.propose(spec, shown, 3); - if (!ideas.length) say('The model wrote no generator in a ```qasm block.', 'bad'); + if (!ideas.length) say(t('workbench.model_wrote_none'), 'bad'); for (const p of ideas) { const proposer = `model: ${m.model?.name || 'the model'}`; const o = await submit(p.qasm, shown[0]?.id ?? null, proposer, p.idea || null); - say(outcomeText(o, p.idea || 'an idea'), o.kept ? 'ok' : o.evaluation?.verified ? '' : 'bad'); - await after(o, p.idea || 'an idea', p.qasm, byOf(proposer)); + say(outcomeText(o, p.idea || t('workbench.an_idea')), o.kept ? 'ok' : o.evaluation?.verified ? '' : 'bad'); + await after(o, p.idea || t('workbench.an_idea'), p.qasm, byOf(proposer)); } persist(); - } catch (e) { say(`The model did not answer: ${e.message}`, 'bad'); } + } catch (e) { say(t('workbench.model_failed', { error: e.message }), 'bad'); } finally { setBusy(false); render(); } } @@ -351,10 +353,10 @@ export function workbenchPane({ engine, draft, model, apply, save, found = null, const src = draft(); const parent = openedFrom && entryById(openedFrom)?.source !== src ? openedFrom : null; const o = await submit(src, parent, 'person', null); - say(outcomeText(o, 'Your program'), o.kept ? 'ok' : o.evaluation?.verified ? '' : 'bad'); - await after(o, 'Your program', src, byOf('person')); + say(outcomeText(o, t('workbench.your_program')), o.kept ? 'ok' : o.evaluation?.verified ? '' : 'bad'); + await after(o, t('workbench.your_program'), src, byOf('person')); persist(); - } catch (e) { say(`It could not be judged: ${e.message}`, 'bad'); } + } catch (e) { say(t('workbench.not_judged', { error: e.message }), 'bad'); } finally { setBusy(false); render(); } } @@ -366,8 +368,8 @@ export function workbenchPane({ engine, draft, model, apply, save, found = null, try { const where = await save(`${stem}.json`, JSON.stringify(spec, null, 2) + '\n'); await save(`${stem}.archive.json`, JSON.stringify(archive, null, 2) + '\n'); - say(`Saved ${where} and its archive; wai-quantum discover verify ${stem}.json checks every seal.`, 'ok'); - } catch (e) { say(`It could not be saved: ${e.message}`, 'bad'); } + say(t('workbench.saved', { path: where, stem }), 'ok'); + } catch (e) { say(t('workbench.not_saved', { error: e.message }), 'bad'); } } // The last archive comes back with the page, if the engine still reads it @@ -378,7 +380,7 @@ export function workbenchPane({ engine, draft, model, apply, save, found = null, engine.discoverSearch(was.spec, was.archive, 1, 0).then((r) => { if (spec) return; spec = was.spec; ({ archive, summary } = r); fill(spec); form.open = false; - say(`Taken up again: ${summary.stats.evaluations} evaluated so far.`); + say(t('workbench.resumed', { n: summary.stats.evaluations })); render(); }).catch(() => {}); } diff --git a/wai/quantum-studio/src/worker.js b/wai/quantum-studio/src/worker.js index 858add187..6b673c0c8 100644 --- a/wai/quantum-studio/src/worker.js +++ b/wai/quantum-studio/src/worker.js @@ -47,6 +47,7 @@ self.onmessage = async (ev) => { case 'target_place': text = mod.engine_target_place(ev.data.qasm, JSON.stringify(ev.data.target)); break; case 'estimate': text = e().estimate(JSON.stringify(ev.data.request)); break; case 'estimate_verify': text = mod.engine_estimate_verify(JSON.stringify(ev.data.body)); break; + case 'qec': text = e().qec(ev.data.name, JSON.stringify(ev.data.args ?? {})); break; case 'verdict': text = mod.engine_verdict(JSON.stringify(ev.data.job), ev.data.intent ? JSON.stringify(ev.data.intent) : '', memoryMb); break; case 'repair': text = mod.engine_repair(ev.data.qasm, JSON.stringify(ev.data.options ?? {})); break; case 'lint_with': text = mod.engine_lint_with(ev.data.qasm, JSON.stringify(ev.data.inputs ?? {})); break; @@ -67,11 +68,36 @@ self.onmessage = async (ev) => { case 'visual_edit': text = mod.engine_visual_edit(ev.data.qasm, JSON.stringify(ev.data.edit ?? {})); break; case 'picture': text = mod.engine_picture(ev.data.qasm, ev.data.format); break; case 'discover_novelty': text = mod.engine_discover_novelty(JSON.stringify(ev.data.spec), JSON.stringify(ev.data.archive), ev.data.entry); break; + case 'answer': { + const a = ev.data.args ?? {}; + switch (ev.data.name) { + case 'parse': text = mod.engine_answer_parse(a.text); break; + case 'snapshot': { const s = new mod.AnswerStream(); s.push(a.text); text = s.snapshot(); s.free(); break; } + case 'value': text = e().answer_value(a.text, a.name, !!a.complete); break; + case 'evaluate': text = e().answer_evaluate(a.text); break; + case 'verify': text = mod.engine_answer_verify(a.text, memoryMb); break; + case 'marker': text = mod.engine_answer_marker(a.statement, JSON.stringify(a.sealed)); break; + case 'prompt': text = mod.engine_answer_prompt(); break; + default: throw new Error(`no answer call ${ev.data.name}`); + } + break; + } + case 'telemetry': { + const a = ev.data.args ?? {}; + if (ev.data.name === 'record') text = mod.engine_telemetry_record(JSON.stringify(a.fields ?? {})); + else if (ev.data.name === 'corpus') text = mod.engine_telemetry_corpus(JSON.stringify(a.records ?? [])); + else throw new Error(`no telemetry call ${ev.data.name}`); + break; + } + case 'course': text = mod.engine_course(ev.data.locale ?? 'en'); break; + case 'course_lesson': text = mod.engine_course_lesson(ev.data.lesson, ev.data.locale ?? 'en'); break; + case 'course_check': text = mod.engine_course_check(ev.data.lesson, ev.data.qasm, ev.data.locale ?? 'en', memoryMb); break; + case 'course_hint': text = mod.engine_course_hint(ev.data.lesson, ev.data.qasm ?? '', ev.data.level >>> 0, ev.data.locale ?? 'en', memoryMb); break; default: throw new Error(`no operation ${op}`); } const value = JSON.parse(text); // A lint or a critique of a program that does not read is an answer that names the problem. - if (value && value.error && !['lint', 'critique', 'lint_with', 'critique_with'].includes(op)) self.postMessage({ id, ok: false, error: value.error }); + if (value && value.error && !['lint', 'critique', 'lint_with', 'critique_with', 'answer', 'telemetry'].includes(op)) self.postMessage({ id, ok: false, error: value.error }); else self.postMessage({ id, ok: true, value }); } catch (err) { self.postMessage({ id, ok: false, error: String(err && err.message ? err.message : err) }); diff --git a/wai/quantum-studio/test/locales.test.mjs b/wai/quantum-studio/test/locales.test.mjs new file mode 100644 index 000000000..3d728ed98 --- /dev/null +++ b/wai/quantum-studio/test/locales.test.mjs @@ -0,0 +1,79 @@ +// The Studio's words in every locale, without a browser: +// +// node test/locales.test.mjs +// +// - locales// holds the same parts of the interface for every locale, +// and locales/index.js gathers each of them; +// - every key of English is in every locale, and no other, with the same +// placeholders; +// - every locale but English is marked machine-assisted, to be reviewed; +// - every key the interface asks for (t('…') or tr('…') in src/) is in English; +// - the language is the reader's choice, else the browser's first language the +// Studio is written in, else English, as the engine picks a locale. +// Exit 0 iff every check passes. +import { readFileSync, readdirSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const here = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const dir = join(here, 'locales'); +let fails = 0; +const check = (ok, what) => { console.log(`${ok ? 'PASS' : 'FAIL'} ${what}`); if (!ok) fails += 1; }; + +const locales = readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort(); +check(locales.includes('en') && locales.includes('zh-CN'), `the locales are ${locales.join(', ')}`); +const areas = (l) => readdirSync(join(dir, l)).filter((f) => f.endsWith('.json')).map((f) => f.replace(/\.json$/, '')).sort(); +const index = readFileSync(join(dir, 'index.js'), 'utf8'); +const { AREAS, default: gathered } = await import(join(dir, 'index.js')); +for (const l of locales) { + check(JSON.stringify(areas(l)) === JSON.stringify(areas('en')), `${l} has the parts English has (${areas(l).join(', ')})`); + for (const a of areas(l)) check(index.includes(`'./${l}/${a}.json'`), `locales/index.js gathers ${l}/${a}.json`); +} +check(JSON.stringify([...AREAS].sort()) === JSON.stringify(areas('en')), `AREAS names every part (${AREAS.join(', ')})`); + +const holes = (s) => [...String(s).matchAll(/\{([a-z_]+)\}/g)].map((m) => m[1]).sort().join(','); +const read = (l, a) => JSON.parse(readFileSync(join(dir, l, `${a}.json`), 'utf8')); +const seen = new Map(); +for (const a of areas('en')) { + const en = read('en', a); + for (const k of Object.keys(en)) { + if (k === '_meta') continue; + check(!seen.has(k), `${k} is in one part only (${a}${seen.has(k) ? ', ' + seen.get(k) : ''})`); + seen.set(k, a); + } + for (const l of locales.filter((x) => x !== 'en')) { + const other = read(l, a); + const keys = (o) => Object.keys(o).filter((k) => k !== '_meta').sort(); + const missing = keys(en).filter((k) => !(k in other)); + const extra = keys(other).filter((k) => !(k in en)); + check(!missing.length && !extra.length, `${l}/${a}.json has every key of en/${a}.json and no other${missing.length ? '; missing ' + missing.join(', ') : ''}${extra.length ? '; extra ' + extra.join(', ') : ''}`); + const off = keys(en).filter((k) => k in other && holes(en[k]) !== holes(other[k])); + check(!off.length, `${l}/${a}.json keeps each key's placeholders${off.length ? ': ' + off.join(', ') : ''}`); + const empty = keys(other).filter((k) => typeof other[k] !== 'string' || !other[k].trim()); + check(!empty.length, `${l}/${a}.json has words for every key${empty.length ? ': ' + empty.join(', ') : ''}`); + check(/^machine-assisted/.test(other._meta?.translation ?? ''), `${l}/${a}.json is marked machine-assisted, to be reviewed`); + } +} + +// Every key the interface asks for by name is in English. +const src = join(here, 'src'); +const asked = new Set(); +for (const f of readdirSync(src).filter((f) => f.endsWith('.js'))) { + for (const m of readFileSync(join(src, f), 'utf8').matchAll(/\b(?:t|tr)\('([a-z0-9_.-]+)'/g)) asked.add(m[1]); +} +const absent = [...asked].filter((k) => !(k in gathered.en)).sort(); +check(asked.size > 100 && !absent.length, `every key the interface asks for is in English (${asked.size} asked${absent.length ? '; absent: ' + absent.join(', ') : ''})`); +for (const l of locales) check(Object.keys(gathered[l]).length === Object.keys(gathered.en).length, `${l} gathers ${Object.keys(gathered[l]).length} keys, as English does`); + +// The language. +const { chooseLocale, localeOf, t, locale } = await import(join(src, 'i18n.js')); +check(locale() === 'en' && t('frame.run') === '▶ Run', 'outside a page the Studio speaks English'); +check(t('frame.lint', { qubits: 2, gates: 3 }) === '2 qubits · 3 gates', 'placeholders are filled'); +for (const [stored, languages, want] of [ + [null, ['zh-CN', 'en'], 'zh-CN'], [null, ['zh-Hans-CN'], 'zh-CN'], [null, ['fr-FR', 'zh-CN'], 'zh-CN'], [null, ['zh-TW', 'en-US'], 'en'], + [null, ['de-DE'], 'en'], ['en', ['zh-CN'], 'en'], ['zh-CN', ['en-GB'], 'zh-CN'], ['xx', ['zh'], 'zh-CN'], [null, [], 'en'], +]) check(chooseLocale({ stored, languages }) === want, `chosen ${stored ?? 'nothing'}, the browser ${JSON.stringify(languages)}: ${want}`); +for (const [tag, want] of [['zh_CN.UTF-8', 'zh-CN'], ['zh', 'zh-CN'], ['zh-SG', 'zh-CN'], ['zh-TW', 'en'], ['en-GB', 'en']]) check(localeOf(tag) === want, `${tag} reads as ${want}`); + +console.log(fails ? `${fails} failed` : 'every check passed'); +process.exit(fails ? 1 : 0); diff --git a/wai/quantum-studio/test/studio.test.mjs b/wai/quantum-studio/test/studio.test.mjs index b68e9f106..08d8e4487 100644 --- a/wai/quantum-studio/test/studio.test.mjs +++ b/wai/quantum-studio/test/studio.test.mjs @@ -56,6 +56,14 @@ // estimate marked stale once the program changes; // - predict first: the form stops a run until the reader predicts, refuses an outcome of // the wrong width, and sets the prediction against the counts; +// - the correctness record: off by default, and while off nothing about the model's +// answers is kept, not even a key in storage; turned on, a proposal is recorded with +// the level the engine checked it to, the reader's "Not this" kept, and the failures +// exported as an eval corpus in the conformance format; +// - answer views: an answer in the answer view layer drawn from its text, each view's +// numbers from the engine here, a number the model wrote in a view refused with its +// code, the seal checked here; and an answer the scripted model streams, each view +// drawn as its statement arrives, before the stream has ended; // - the assistant, against a scripted model server: a tool call run on the engine, a // proposal the engine cannot read sent back with its reason, the corrected one checked // and run before it is offered, each proposal with its verdict's badge, and Apply; it asks @@ -87,6 +95,19 @@ // text recorded as the reader's, and a gate of a broadcast refused with its line; // - the circuit in words for a screen reader, and the drawing saved as SVG and LaTeX, // each the engine's; +// - error correction: a pane closed until opened; a code shown with its certified distance and its seal checked, +// qubits flipped on the lattice and their syndrome lit, the colour decoder's +// correction marked and the residual judged, the certificate's witness shown, a +// threshold measured and plotted one line per distance; and a run mitigated by +// virtual distillation under its noise, raw and mitigated with their errors; +// - the course, in English: a lesson's starter loaded and checked, not passed, What went +// wrong? from the engine's verdict, a hint with the engine's pointer, the solution passed +// and marked, a rule and a generator's cases each with their verdict, the assistant's hint +// asked for without a model, the solution never on the page, and the progress kept in this +// browser across a reload; +// - the course in Chinese, from the browser's language: the interface, the lesson, the check +// and what went wrong in Chinese, marked machine-assisted; the switch to English and back, +// the choice kept; nothing scrolls sideways at a phone's width with the course open; // - no console error and no failed request; at a phone's width nothing scrolls sideways. // Exit 0 iff every check passes. import { createServer } from 'node:http'; @@ -120,8 +141,8 @@ const settles = (page, ...res) => page.waitForFunction((srcs) => { }, res.map((r) => r.source), { timeout: LONG }).then(() => true, () => false); const browser = await chromium.launch({ channel: process.env.WAI_E2E_CHANNEL ?? 'chrome', headless: true }); -async function open(viewport = { width: 1400, height: 950 }) { - const ctx = await browser.newContext({ viewport, colorScheme: 'dark' }); +async function open(viewport = { width: 1400, height: 950 }, locale = 'en-US') { + const ctx = await browser.newContext({ viewport, colorScheme: 'dark', locale }); const page = await ctx.newPage(); const seen = { errors: [], failed: [] }; page.on('console', (m) => { if (m.type() === 'error') seen.errors.push(m.text()); }); @@ -829,16 +850,31 @@ const wbIdle = () => page.waitForFunction(() => !document.querySelector('#qs-wb- { const asked = []; const GOOD = 'OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n'; + const STREAMED_FIRST = `root = Answer([bell, counts, check, seal])\n\`\`\`qasm bell\n${GOOD}\`\`\`\ncounts = Histogram(@Sample(bell, 500))\n`; + const STREAMED_REST = 'check = Verdict(@Check(bell, {"00": 0.5, "11": 0.5}))\nseal = Receipt(@Seal([bell, counts, check]))\n'; + let release; + const streamHeld = new Promise((r) => { release = r; }); const model = createServer((req, res) => { const cors = { 'access-control-allow-origin': '*', 'access-control-allow-headers': 'content-type', 'access-control-allow-methods': 'GET, POST, OPTIONS' }; if (req.method === 'OPTIONS') { res.writeHead(204, cors).end(); return; } if (req.url.endsWith('/models')) { res.writeHead(200, { ...cors, 'content-type': 'application/json' }).end(JSON.stringify({ data: [{ id: 'scripted' }] })); return; } let body = ''; req.on('data', (c) => (body += c)); - req.on('end', () => { + req.on('end', async () => { const r = JSON.parse(body); asked.push(r); const msgs = r.messages; + // An answer in the answer view layer, streamed: its first part at once, the rest + // only once the test has seen the first part drawn. + if (r.stream) { + res.writeHead(200, { ...cors, 'content-type': 'text/event-stream' }); + const send = (t) => res.write(`data: ${JSON.stringify({ choices: [{ delta: { content: t } }] })}\n\n`); + for (const piece of STREAMED_FIRST.match(/[\s\S]{1,9}/g)) send(piece); + await streamHeld; + for (const piece of STREAMED_REST.match(/[\s\S]{1,9}/g)) send(piece); + res.end('data: [DONE]\n\n'); + return; + } // Alternatives' calls carry no tools: candidates, one of them wrong on purpose. if (!r.tools) { const want = msgs[1].content; @@ -883,6 +919,8 @@ const wbIdle = () => page.waitForFunction(() => !document.querySelector('#qs-wb- await page.click('#qs-review-apply'); await page.waitForFunction((q) => window.quantumStudio.view.state.doc.toString() === q, GOOD, { timeout: LONG }); check(true, 'taken, it is the program in the editor'); + check(await page.evaluate(() => localStorage.getItem('wai.quantum-studio.telemetry')) === null && /0 answers recorded; recording is off/.test(await page.textContent('#qs-tel-count')), + 'with the correctness record off, as it is by default, nothing about the model’s answer is kept, not even a key in storage'); check((await page.inputValue('#qs-ai-stance')) === 'questions' && (await page.inputValue('#qs-ai-initiative')) === 'asked' && /ask one short question/.test(asked[0].messages[0].content), 'by default the assistant asks questions first and speaks only when asked'); const quick = await page.$$eval('.qs-ai-quick button', (bs) => bs.map((b) => b.textContent)); @@ -946,6 +984,52 @@ const wbIdle = () => page.waitForFunction(() => !document.querySelector('#qs-wb- await page.waitForFunction(() => /4 gates/.test(document.querySelector('#qs-lint')?.textContent ?? '') && document.querySelectorAll('#qs-diagram .qc-flag').length === 2, null, { timeout: LONG }); check(!/null/.test(await page.textContent('#qs-alt-body')) && !/null/.test(await page.textContent('#qs-measures')), 'no empty part is written out as “null”'); await page.screenshot({ path: join(SHOTS, 'studio-alternatives.png'), fullPage: true }); + // The correctness record, turned on: the proposal recorded, "Not this" kept, the failures exported. + await setProgram(GOOD); + await page.click('#qs-tel > summary'); + await page.check('#qs-tel-on'); + await page.click('.qs-ai-quick button >> text=Explain this circuit'); + await page.waitForFunction(() => document.querySelectorAll('.qs-ai-reject').length >= 2 && /^1 answer recorded/.test(document.querySelector('#qs-tel-count')?.textContent ?? ''), null, { timeout: LONG }); + await page.click('#qs-ai-log > li:last-child .qs-ai-reject'); + const kept = await page.evaluate(() => JSON.parse(localStorage.getItem('wai.quantum-studio.telemetry'))); + const rec = kept?.records?.[0]; + check(kept?.on === true && kept.records.length === 1 && rec.format === 'wai.quantum.telemetry/1' && rec.verdict?.level === 'executed' && rec.decision === 'rejected' && /^[0-9a-f]{64}$/.test(rec.receipt ?? '') && rec.program === GOOD, + `turned on, the proposal is one record: the level the engine checked it to, its receipt, and the reader's "Not this" (${JSON.stringify({ level: rec?.verdict?.level, decision: rec?.decision })})`); + const telFiles = []; + page.on('download', (d) => telFiles.push(d)); + await page.click('#qs-tel-corpus'); + await page.waitForTimeout(800); + const corpus = telFiles.length ? JSON.parse(readFileSync(await telFiles.at(-1).path(), 'utf8')) : null; + check(corpus?.format === 'wai.quantum-intent-conformance-bundle/1' && corpus.cases.length === 1 && corpus.cases[0].covers[0] === 'the person rejected it' && corpus.cases[0].qasm === GOOD, + `the failures export as an eval corpus in the conformance format (${telFiles.at(-1)?.suggestedFilename()})`); + // Answer views: an answer drawn from its text, a number the model wrote refused. + await page.click('#qs-answers > summary'); + const PASTED = `root = Answer([bell, counts, made_up, check, seal])\n\`\`\`qasm bell\n${GOOD}\`\`\`\ncounts = Histogram(@Sample(bell, 1000))\nmade_up = Histogram(512)\ncheck = Verdict(@Check(bell, {"00": 0.5, "11": 0.5}))\nseal = Receipt(@Seal([bell, counts, check]))\n`; + await page.fill('#qs-av-input', PASTED); + await page.click('#qs-av-draw'); + await page.waitForSelector('#qs-av[data-done="true"]', { timeout: LONG }); + await page.waitForFunction(() => document.querySelector('.qs-av-card[data-name="seal"] [data-receipt]')?.dataset.receipt !== 'checking', null, { timeout: LONG }); + const av = await page.evaluate(() => Object.fromEntries([...document.querySelectorAll('.qs-av-card')].map((c) => [c.dataset.name, c.dataset.state]))); + const bars = await page.$$eval('.qs-av-card[data-name="counts"] .qs-bar', (bs) => bs.map((b) => Number(b.dataset.count))); + check(av.counts === 'drawn' && bars.length === 2 && bars.reduce((a, b) => a + b, 0) === 1000, `the histogram's counts are the engine's, run here (${bars.join(' + ')})`); + check(await page.$eval('.qs-av-card[data-name="check"] .qs-level', (b) => b.dataset.level) === 'proof', 'the verdict is the engine’s: checked as proof'); + check(await page.$eval('.qs-av-card[data-name="seal"] [data-receipt]', (b) => b.dataset.receipt) === 'valid', 'the seal over the program, the counts and the verdict is checked here'); + const diag = await page.$$eval('#qs-av-diags li', (ls) => ls.map((l) => [l.dataset.code, l.dataset.severity, l.textContent])); + check(av.made_up === 'dropped' && diag.some(([c, s, t]) => c === 'J104' && s === 'error' && /comes from the engine/.test(t)), `a number the model wrote in a view is refused, with its code (${diag.map(([c]) => c).join(', ')})`); + check((await page.evaluate(() => JSON.parse(localStorage.getItem('wai.quantum-studio.telemetry')))).records.at(-1)?.codes?.includes('J104'), 'the answer is recorded with the codes it drew'); + // An answer the model streams: each view drawn as its statement arrives. + await page.fill('#qs-av-q', 'Show me what this program does.'); + await page.click('#qs-av-ask'); + await page.waitForFunction(() => document.querySelector('.qs-av-card[data-name="counts"]')?.dataset.state === 'drawn', null, { timeout: LONG }); + const before = await page.evaluate(() => ({ done: document.querySelector('#qs-av').dataset.done ?? null, seal: document.querySelector('.qs-av-card[data-name="seal"]')?.dataset.state })); + check(before.done === null && before.seal === 'waiting', `the histogram is drawn while the model is still writing, the seal's card waiting for its statement (${JSON.stringify(before)})`); + release(); + await page.waitForSelector('#qs-av[data-done="true"]', { timeout: LONG }); + await page.waitForFunction(() => document.querySelector('.qs-av-card[data-name="seal"] [data-receipt]')?.dataset.receipt === 'valid', null, { timeout: LONG }).catch(() => {}); + const streamed = asked.find((r) => r.stream); + check(await page.$eval('.qs-av-card[data-name="seal"] [data-receipt]', (b) => b.dataset.receipt).catch(() => null) === 'valid' && /Engine calls:/.test(streamed?.messages?.[0]?.content ?? '') && /never write a count/.test(streamed.messages[0].content), + 'the rest arrives and is drawn, its seal checked here; the model was told the layer, and that it writes no numbers'); + await page.screenshot({ path: join(SHOTS, 'studio-answer-views.png'), fullPage: true }); model.close(); } // Step through: the engine's debugger, forward and back, on the branch the reader chooses; @@ -1053,10 +1137,160 @@ const wbIdle = () => page.waitForFunction(() => !document.querySelector('#qs-wb- await page.screenshot({ path: join(SHOTS, 'studio-debugger.png'), fullPage: true }); } +// Error correction: a code drawn with its certified distance, qubits flipped on +// the lattice, the syndrome lit, the decoder's correction marked and the residual +// judged, the certificate's witness shown, a threshold plotted; and a run that +// mitigates its value under its noise. +{ + check(!(await page.$eval('#qs-qec', (d) => d.open)), 'the error-correction pane waits closed until it is opened'); + await page.click('#qs-qec > summary'); + await page.waitForFunction(() => /\[\[9, 1, 3\]\]/.test(document.querySelector('#qs-qec-summary')?.textContent ?? ''), null, { timeout: LONG }); + check(/Every logical operator has weight at least 3/.test(await page.textContent('#qs-qec-cert')) && /seal verifies here/.test(await page.textContent('#qs-qec-cert')), `the surface code is shown with its certified distance, sealed (${await page.textContent('#qs-qec-cert')})`); + await page.selectOption('#qs-qec-code', 'color:5'); + await page.waitForFunction(() => /\[\[19, 1, 5\]\]/.test(document.querySelector('#qs-qec-summary')?.textContent ?? ''), null, { timeout: LONG }); + const faces = await page.$$eval('#qs-qec-lattice .qs-qec-face', (f) => [f.length, f.filter((x) => /c[012]/.test(x.getAttribute('class'))).length]); + check(faces[0] === 9 && faces[1] === 9, `the colour code's nine faces are drawn in their colours (${faces})`); + await page.click('#qs-qec-lattice [data-q="3"]'); + await page.click('#qs-qec-lattice [data-q="11"]'); + const lit = await page.$$eval('#qs-qec-lattice .qs-qec-face.on', (f) => f.length); + check(lit > 0 && /2 qubits flipped/.test(await page.textContent('#qs-qec-result')), `two flipped qubits light their syndrome (${lit} faces; ${await page.textContent('#qs-qec-result')})`); + check((await page.$$eval('#qs-qec-decoder option', (o) => o.map((x) => x.value))).join(',') === 'color,relay_bp,search', 'the decoders the colour code takes are offered'); + await page.click('#qs-qec-decode'); + await page.waitForFunction(() => /unharmed|logical error/.test(document.querySelector('#qs-qec-result')?.textContent ?? ''), null, { timeout: LONG }); + const res = await page.textContent('#qs-qec-result'); + check(/explains the syndrome/.test(res) && /unharmed/.test(res) && /seal verifies here/.test(res) && (await page.$$('#qs-qec-lattice .qs-qec-qubit.fix')).length >= 1, `the colour decoder corrects them and the residual is judged (${res})`); + await page.click('#qs-qec-witness'); + check((await page.$$('#qs-qec-lattice .qs-qec-qubit.wit')).length === 5, 'the certificate’s witness, a logical of weight 5, is shown on the lattice'); + await (await page.$('#qs-qec-pane')).screenshot({ path: join(SHOTS, 'studio-qec.png') }); + await page.fill('#qs-qec-shots', '600'); + await page.click('#qs-qec-measure'); + await page.waitForSelector('#qs-qec-plot svg', { timeout: LONG }); + const series = await page.$$eval('#qs-qec-plot svg .qs-qec-line', (l) => l.length); + const reading = await page.textContent('#qs-qec-reading'); + check(series === 3 && /code capacity/.test(reading) && /seal verifies here/.test(reading), `a threshold is measured and plotted, one line per distance (${series}; ${reading})`); + // A run that mitigates its value under its noise. + await setProgram('OPENQASM 3.0;\nqubit[2] q;\nh q[0];\ncx q[0], q[1];\nrz(0.3) q[1];\n'); + await page.fill('#qs-obs', 'Z0 Z1'); + await page.evaluate(() => { document.querySelector('.qs-noise').open = true; }); + await page.fill('#qs-noise-depolarizing_2q', '0.03'); + await page.selectOption('#qs-mitigate', 'vd'); + await page.click('#qs-run'); + await done(page); + await page.waitForSelector('#qs-mitigation', { timeout: LONG }); + const m = await page.textContent('#qs-mitigation'); + check(/Mitigated: vd/.test(m) && /⟨Z0 Z1⟩/.test(m) && /Bias: Assumes the error-free state/.test(m), `the run carries its mitigation, raw and mitigated with their errors and the method's assumptions (${m.slice(0, 160)})`); + await page.selectOption('#qs-mitigate', 'none'); + await page.fill('#qs-noise-depolarizing_2q', ''); + await page.fill('#qs-obs', ''); +} + check(seen.errors.length === 0, `no console error${seen.errors.length ? ': ' + seen.errors.join(' | ') : ''}`); check(seen.failed.length === 0, `no failed request${seen.failed.length ? ': ' + seen.failed.join(' | ') : ''}`); await ctx.close(); +// ---- the course ---- +const COURSE = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', 'wai-quantum', 'course', 'lessons'); +const exercise = (id) => JSON.parse(readFileSync(join(COURSE, id, 'exercise.json'), 'utf8')); +const lessonProgram = (v) => (Array.isArray(v) ? v.join('\n') + '\n' : v); +const putProgram = (page, src) => page.evaluate((s) => { const v = window.quantumStudio.view; v.dispatch({ changes: { from: 0, to: v.state.doc.length, insert: s } }); }, src); +const courseChecked = (page) => page.waitForFunction(() => !document.querySelector('#qs-course-check')?.disabled && /\S/.test(document.querySelector('#qs-course-statement')?.textContent ?? ''), null, { timeout: LONG }); +async function checkCourse(page) { + await page.evaluate(() => { document.querySelector('#qs-course-statement').textContent = ''; }); + await page.click('#qs-course-check'); + await courseChecked(page); + return page.textContent('#qs-course-passed'); +} +{ + const { ctx, page, seen } = await open(); + await page.waitForFunction(() => /qubits/.test(document.querySelector('#qs-lint')?.textContent ?? ''), null, { timeout: LONG }); + await page.click('#qs-course-open'); + await page.waitForFunction(() => document.querySelectorAll('#qs-course-list [data-lesson]').length === 21 && /\S/.test(document.querySelector('#qs-course-title')?.textContent ?? ''), null, { timeout: LONG }); + check((await page.textContent('#qs-course-title')) === 'A qubit, a gate, a measurement' && /Lesson 1 of 21/.test(await page.textContent('#qs-course-where')), `the course opens at its first lesson (${await page.textContent('#qs-course-title')})`); + await page.click('[data-lesson="bell"]'); + await page.waitForFunction(() => document.querySelector('#qs-course-title')?.textContent === 'Two qubits, one answer', null, { timeout: LONG }); + const bell = exercise('bell'); + const courseText = await page.textContent('#qs-course'); + check(!courseText.includes('cx q[0], q[1];') && /00 with probability 0.5/.test(await page.textContent('#qs-course-must')), 'the lesson states what the program must do, and not the solution'); + await page.click('#qs-course-starter'); + await page.waitForFunction((s) => window.quantumStudio.view.state.doc.toString() === s, lessonProgram(bell.starter), { timeout: LONG }); + check(true, 'the starter program is loaded into the editor'); + check((await checkCourse(page)) === 'not yet', `the starter is checked and not passed (${await page.textContent('#qs-course-statement')})`); + const levels = await page.$$eval('#qs-course-cases .qs-level', (b) => b.map((x) => x.dataset.level)); + check(levels.join() === 'proof', `its verdict is the engine's, checked as proof (${levels})`); + await page.click('#qs-course-why'); + await page.waitForSelector('#qs-course-wrong:not([hidden]) li', { timeout: LONG }); + const wrongText = await page.textContent('#qs-course-wrong'); + check(/00: your program gives 1, the exercise asks for 0\.5/.test(wrongText) && /Where to look:/.test(wrongText), `What went wrong? names the gap the engine measured (${wrongText.slice(0, 160)})`); + await page.click('#qs-course-hint'); + await page.waitForFunction(() => document.querySelectorAll('#qs-course-hints li').length === 1 && !document.querySelector('#qs-course-pointer').hidden, null, { timeout: LONG }); + check(/Hint 1 of 2/.test(await page.textContent('#qs-course-hints')) && /outcome/.test(await page.textContent('#qs-course-pointer')), `a hint, with the engine's pointer (${await page.textContent('#qs-course-pointer')})`); + await page.click('#qs-course-ask'); + await page.waitForFunction(() => /needs a model/.test(document.querySelector('#qs-course-asked')?.textContent ?? ''), null, { timeout: LONG }); + check(true, 'the assistant is asked for a hint only with a model connected, and says so'); + await putProgram(page, lessonProgram(bell.reference)); + check((await checkCourse(page)) === 'passed', `the solution passes (${await page.textContent('#qs-course-statement')})`); + check(await page.$eval('[data-lesson="bell"]', (b) => b.classList.contains('done')), 'and the lesson is marked passed'); + check(/1 of 21 lessons passed · kept only in this browser/.test(await page.textContent('#qs-course-progress')), `the progress says where it is kept (${await page.textContent('#qs-course-progress')})`); + // A rule the engine measures, and a generator's cases each with their verdict. + await page.click('[data-lesson="phase"]'); + await page.waitForFunction(() => /^A phase you cannot see/.test(document.querySelector('#qs-course-title')?.textContent ?? ''), null, { timeout: LONG }); + await putProgram(page, lessonProgram(exercise('phase').wrong.find((w) => w.fails === 'constraint').qasm)); + await checkCourse(page); + const rules = await page.$$eval('#qs-course-rules li', (l) => l.map((x) => x.className + ' ' + x.textContent)); + check(rules.some((r) => /^bad ✗ only these gates: h, z/.test(r)), `a gate the exercise does not allow breaks its rule (${rules})`); + await page.click('[data-lesson="bit-flip-code"]'); + await page.waitForFunction(() => /^Three copies/.test(document.querySelector('#qs-course-title')?.textContent ?? ''), null, { timeout: LONG }); + await putProgram(page, lessonProgram(exercise('bit-flip-code').wrong[1].qasm)); + await checkCourse(page); + const cases = await page.$$eval('#qs-course-cases li', (l) => l.map((x) => x.className)); + check(cases.join() === 'ok,bad,ok', `a generator is checked in each case, each with its verdict (${cases})`); + // Kept in this browser: the course reopens where it was, the pass still marked. + await page.reload({ waitUntil: 'domcontentloaded' }); + await page.waitForFunction(() => !document.querySelector('#qs-course')?.hidden && document.querySelectorAll('#qs-course-list [data-lesson]').length === 21 && /^Three copies/.test(document.querySelector('#qs-course-title')?.textContent ?? ''), null, { timeout: LONG }); + check(await page.$eval('[data-lesson="bell"]', (b) => b.classList.contains('done')), 'after a reload the course reopens where it was, the pass kept'); + await page.screenshot({ path: join(SHOTS, 'studio-course.png'), fullPage: true }); + check(seen.errors.length === 0, `the course: no console error${seen.errors.length ? ': ' + seen.errors.join(' | ') : ''}`); + check(seen.failed.length === 0, `the course: no failed request${seen.failed.length ? ': ' + seen.failed.join(' | ') : ''}`); + await ctx.close(); +} +{ + // The browser's language is Chinese: so is the Studio, until the reader picks another. + const { ctx, page, seen } = await open(undefined, 'zh-CN'); + await page.waitForFunction(() => /量子比特/.test(document.querySelector('#qs-lint')?.textContent ?? ''), null, { timeout: LONG }); + check((await page.evaluate(() => document.documentElement.lang)) === 'zh-CN' && (await page.textContent('#qs-run')) === '▶ 运行', `the Studio follows the browser's language (${await page.textContent('#qs-run')}, ${await page.textContent('#qs-lint')})`); + check(/机器辅助翻译/.test(await page.textContent('#qs-translation-note')), 'and says its words are a machine-assisted translation'); + await page.click('#qs-course-open'); + await page.waitForFunction(() => document.querySelector('#qs-course-title')?.textContent === '一个量子比特、一个门、一次测量', null, { timeout: LONG }); + check(/第 1\/21 课/.test(await page.textContent('#qs-course-where')) && /机器辅助翻译/.test(await page.textContent('#qs-course-translation')), `the lesson in Chinese (${await page.textContent('#qs-course-where')})`); + await page.click('#qs-course-starter'); + await page.waitForFunction((s) => window.quantumStudio.view.state.doc.toString() === s, lessonProgram(exercise('first-circuit').starter), { timeout: LONG }); + check((await checkCourse(page)) === '尚未通过', `checked in Chinese (${await page.textContent('#qs-course-statement')})`); + await page.click('#qs-course-why'); + await page.waitForSelector('#qs-course-wrong:not([hidden]) li', { timeout: LONG }); + check(/1:你的程序给出 0,练习要求 1/.test(await page.textContent('#qs-course-wrong')), `What went wrong? in Chinese (${(await page.textContent('#qs-course-wrong')).slice(0, 120)})`); + // At a phone's width, the course open, nothing scrolls sideways. + await page.setViewportSize({ width: 390, height: 844 }); + const over = await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth); + check(over <= 0, `in Chinese at 390 px, the course open, nothing scrolls sideways (overflow ${over} px)`); + await page.setViewportSize({ width: 1400, height: 950 }); + await page.screenshot({ path: join(SHOTS, 'studio-course-zh.png'), fullPage: true }); + // The switch: English, kept over the browser's language; then Chinese again. + await Promise.all([page.waitForEvent('load'), page.selectOption('#qs-lang', 'en')]); + await page.waitForFunction(() => document.querySelector('#qs-run')?.textContent === '▶ Run', null, { timeout: LONG }); + check((await page.evaluate(() => document.documentElement.lang)) === 'en', 'the switch picks English over the browser’s language'); + await page.reload({ waitUntil: 'domcontentloaded' }); + await page.waitForFunction(() => document.querySelector('#qs-run')?.textContent === '▶ Run', null, { timeout: LONG }); + check(true, 'and the choice is kept in this browser'); + await Promise.all([page.waitForEvent('load'), page.selectOption('#qs-lang', 'zh-CN')]); + await page.waitForFunction(() => document.querySelector('#qs-run')?.textContent === '▶ 运行', null, { timeout: LONG }); + check(true, 'and back to Chinese'); + check(seen.errors.length === 0, `in Chinese: no console error${seen.errors.length ? ': ' + seen.errors.join(' | ') : ''}`); + // A switch reloads the page, which aborts what was still loading; nothing else may fail. + const failed = seen.failed.filter((f) => !f.endsWith('(net::ERR_ABORTED)')); + check(failed.length === 0, `in Chinese: no failed request${failed.length ? ': ' + failed.join(' | ') : ''}`); + await ctx.close(); +} + { const { ctx, page } = await open({ width: 390, height: 844 }); await page.waitForFunction(() => /qubits/.test(document.querySelector('#qs-lint')?.textContent ?? ''), null, { timeout: LONG }); diff --git a/wai/quantum-studio/test/telemetry.test.mjs b/wai/quantum-studio/test/telemetry.test.mjs new file mode 100644 index 000000000..fbb8d68ab --- /dev/null +++ b/wai/quantum-studio/test/telemetry.test.mjs @@ -0,0 +1,62 @@ +// The correctness record (src/telemetry.js), without a browser: off by default, +// and while it is off nothing is recorded, nothing is written to storage, and the +// engine is not even asked; turned on, each answer is one record, the reader's +// decision is kept, and the failures go to the corpus call. +// +// node test/telemetry.test.mjs +import { telemetryStore, KEY } from '../src/telemetry.js'; + +let fails = 0; +const check = (ok, what) => { console.log(`${ok ? 'PASS' : 'FAIL'} ${what}`); if (!ok) fails += 1; }; + +// A storage that remembers every write. +function storage() { + const m = new Map(); + const writes = []; + return { getItem: (k) => (m.has(k) ? m.get(k) : null), setItem: (k, v) => { writes.push(k); m.set(k, String(v)); }, removeItem: (k) => m.delete(k), writes, m }; +} +// An engine that counts what it is asked, and answers as the real one does. +function engine() { + const asked = []; + return { + asked, + async telemetry(name, args) { + asked.push(name); + if (name === 'record') return { format: 'wai.quantum.telemetry/1', decision: 'pending', codes: [], repairs: [], ...args.fields }; + return { format: 'wai.quantum-intent-conformance-bundle/1', cases: args.records.filter((r) => r.decision === 'rejected').map((r, i) => ({ name: `failure-${i + 1}` })) }; + }, + }; +} + +const s = storage(); +const e = engine(); +const t = telemetryStore({ engine: e, storage: s, now: () => 1 }); +check(!t.on(), 'it is off by default'); +for (let i = 0; i < 5; i++) check((await t.record({ program: 'OPENQASM 3.0;', codes: ['Q103'], verdict: { level: 'refused' } })) === null, `while off, an answer is not recorded (${i + 1})`); +t.decide(0, 'accepted'); +check(t.records().length === 0, 'while off, the record is empty'); +check(s.getItem(KEY) === null && s.writes.length === 0, 'while off, nothing is written to storage, not even a key'); +check(e.asked.length === 0, 'while off, the engine is not asked to make a record'); + +t.set(true); +const id = await t.record({ program: 'OPENQASM 3.0;', verdict: { level: 'proof', holds: true } }); +check(id === 0 && t.records().length === 1 && t.records()[0].at === 1 && t.records()[0].surface === 'studio', 'turned on, an answer is one record, with its time and surface'); +t.decide(id, 'rejected'); +check(t.records()[0].decision === 'rejected', 'the reader’s decision is kept'); +check(JSON.parse(s.getItem(KEY)).records.length === 1, 'kept in this browser’s storage'); +check((await t.corpus()).cases.length === 1 && e.asked.at(-1) === 'corpus', 'the failures go to the engine’s corpus'); + +t.set(false); +const writes = s.writes.length; +check((await t.record({ program: 'x' })) === null && t.records().length === 1 && e.asked.filter((n) => n === 'record').length === 1, 'turned off again, nothing more is recorded'); +t.decide(0, 'accepted'); +check(t.records()[0].decision === 'rejected' && s.writes.length === writes, 'and no decision is changed'); +t.clear(); +check(t.records().length === 0, 'the record can be deleted'); + +// A store read back from storage keeps its state. +const again = telemetryStore({ engine: e, storage: s }); +check(!again.on() && again.records().length === 0, 'read back, it is as it was left'); + +console.log(fails ? `${fails} check(s) failed` : 'every check passed'); +process.exit(fails ? 1 : 0); diff --git a/wai/quantum-studio/test/views.test.mjs b/wai/quantum-studio/test/views.test.mjs new file mode 100644 index 000000000..de2ba93ec --- /dev/null +++ b/wai/quantum-studio/test/views.test.mjs @@ -0,0 +1,326 @@ +// The engine's views inside an agent host, driven by a scripted host in a +// headless browser: +// +// WQ_CLI=PATH/wai-quantum [WQ_SERVICE=PATH/wai-quantum-service] node test/views.test.mjs +// +// The host speaks the extension for interactive views (io.modelcontextprotocol/ui, +// 2026-01-26) as a host does: it says so in initialize, lists the tools and finds each +// one's ui:// resource, reads the resource, and draws it in a sandboxed frame under a +// content-security policy, answering the view's ui/initialize, giving it the tool's +// input and result, carrying its tool calls to the server (only to tools whose +// visibility lets a view call them), and keeping what it tells the model. +// +// For the command line's server (stdio), under the extension's default policy (no +// WebAssembly) and under that policy with 'wasm-unsafe-eval': +// - every tool with a view names it, and the six views are listed and read, each +// one self-contained page within the limit (the sizes are printed); +// - a run: its verdict, its counts, its circuit, and its receipt checked in the view, +// by the engine as WebAssembly where the policy lets it and as JavaScript where it +// does not; the model told the check; a count changed after sealing fails the check; +// - a refusal with its code; a receipt checked beside the server's own answer, for a +// sealed job and a changed one; a compile's passes and its certificate checked; a +// lint's refusal; a repair's changes; a critique's smells marked on the circuit; an +// estimate's frontier and its seal; a device target's map; a run on a target; +// - a view makes no request of its own, and draws without a script error. +// With WQ_SERVICE, the job service started here over HTTP: the session id that says +// the host draws views, the view's own tool kept from the model, a run drawn and its +// receipt checked as the service's, and a job still running followed by the view +// until it is done. +// Exit 0 iff every check passes. +import { spawn } from 'node:child_process'; +import { createServer } from 'node:http'; +import { mkdirSync, mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const { chromium } = (await import(process.env.WAI_E2E_DRIVER ?? 'playwright')).default; +const SHOTS = resolve(dirname(fileURLToPath(import.meta.url)), 'screenshots'); +const CLI = process.env.WQ_CLI; +const SERVICE = process.env.WQ_SERVICE; +if (!CLI) { console.error('give WQ_CLI: the wai-quantum binary'); process.exit(2); } +const LIMIT = 1 << 20; +const UI = { extensions: { 'io.modelcontextprotocol/ui': { mimeTypes: ['text/html;profile=mcp-app'] } } }; +const STRICT = "default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; media-src 'self' data:; connect-src 'none';"; +const WASM = STRICT.replace("script-src 'self' 'unsafe-inline';", "script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval';"); +const LONG = 60_000; + +let fails = 0; +const check = (ok, what) => { console.log(`${ok ? 'PASS' : 'FAIL'} ${what}`); if (!ok) fails += 1; }; + +// ---- the servers ---- + +function stdio(bin) { + const p = spawn(bin, ['mcp'], { stdio: ['pipe', 'pipe', 'inherit'] }); + let buf = ''; + let next = 1; + const waiting = new Map(); + p.stdout.setEncoding('utf8'); + p.stdout.on('data', (d) => { + buf += d; + for (let i = buf.indexOf('\n'); i >= 0; i = buf.indexOf('\n')) { + const line = buf.slice(0, i); + buf = buf.slice(i + 1); + if (!line.trim()) continue; + const m = JSON.parse(line); + waiting.get(m.id)?.(m); + waiting.delete(m.id); + } + }); + const rpc = (method, params = {}) => new Promise((ok) => { const id = next++; waiting.set(id, ok); p.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n'); }); + return { name: 'the command line’s server', rpc, close: () => p.kill() }; +} + +function http(base) { + let session = null; + let next = 1; + const rpc = async (method, params = {}) => { + const headers = { 'content-type': 'application/json', accept: 'application/json, text/event-stream' }; + if (session) headers['mcp-session-id'] = session; + const r = await fetch(`${base}/mcp`, { method: 'POST', headers, body: JSON.stringify({ jsonrpc: '2.0', id: next++, method, params }) }); + session = r.headers.get('mcp-session-id') ?? session; + return r.json(); + }; + return { name: 'the job service’s server', rpc, session: () => session }; +} + +// ---- the host page ---- + +// The host's side of the protocol, in the page: each view in a sandboxed frame of its +// own, under `csp`, given its tool's input and result; what it says is kept in `log`. +const HOST = `scripted host
`; +const host = createServer((req, res) => { res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); res.end(HOST); }); +await new Promise((r) => host.listen(0, '127.0.0.1', r)); +const ORIGIN = `http://127.0.0.1:${host.address().port}`; + +const browser = await chromium.launch({ channel: process.env.WAI_E2E_CHANNEL ?? 'chrome', headless: true }); + +// One page for a server, its tool calls carried to it. +async function hostFor(server) { + const ctx = await browser.newContext({ viewport: { width: 1000, height: 1000 }, colorScheme: 'dark' }); + const page = await ctx.newPage(); + const seen = { errors: [], requests: [] }; + page.on('console', (m) => { if (m.type() === 'error') seen.errors.push(m.text()); }); + page.on('pageerror', (e) => seen.errors.push(String(e))); + page.on('request', (r) => seen.requests.push(r.url())); + await page.exposeFunction('callServer', async (name, args) => (await server.rpc('tools/call', { name, arguments: args })).result); + await page.goto(ORIGIN + '/', { waitUntil: 'domcontentloaded' }); + return { ctx, page, seen }; +} + +let keys = 0; +// Draw `view` for a tool's `result` (and its `input`) under `csp`; the frame, once the +// view says it is drawn. +async function draw(page, pages, view, input, result, { csp = STRICT, appTools = [], state = 'shown', theme = 'dark' } = {}) { + const key = `v${keys++}`; + await page.evaluate(([k, html, c, i, r, a, t]) => window.mount(k, html, c, i, r, a, t), [key, pages[view], csp, input, result, appTools, theme]); + const frame = await (await page.waitForSelector(`#f-${key}`)).contentFrame(); + const ok = await frame.waitForSelector(`body[data-state="${state}"]`, { timeout: LONG }).then(() => true, () => false); + return { key, frame, ok, log: () => page.evaluate((k) => window.log[k], key) }; +} +const receiptOf = (frame) => frame.waitForFunction(() => { const r = document.querySelector('#wq-receipt'); return r && r.dataset.receipt !== 'checking' && r.dataset.receipt; }, null, { timeout: LONG }).then((h) => h.jsonValue(), () => null); +const text = (frame, sel) => frame.$eval(sel, (e) => e.textContent).catch(() => ''); + +// What the server lists for a host that draws views: each tool's view, the views, and their pages. +async function discover(server) { + const init = await server.rpc('initialize', { protocolVersion: '2025-11-25', capabilities: UI, clientInfo: { name: 'scripted-host', version: '1' } }); + check(/never write HTML/.test(init.result?.instructions ?? ''), `${server.name}: the model is told not to write views itself`); + const tools = (await server.rpc('tools/list')).result.tools; + const named = Object.fromEntries(tools.filter((t) => t._meta?.ui?.resourceUri).map((t) => [t.name, t._meta.ui])); + const resources = (await server.rpc('resources/list')).result.resources.filter((r) => r.uri.startsWith('ui://')); + check(resources.length === 6 && resources.every((r) => r.mimeType === 'text/html;profile=mcp-app'), `${server.name}: the six views are listed (${resources.map((r) => r.uri.slice(17)).join(', ')})`); + const pages = {}; + for (const r of resources) { + const c = (await server.rpc('resources/read', { uri: r.uri })).result.contents[0]; + const bytes = Buffer.byteLength(c.text, 'utf8'); + const name = r.uri.slice('ui://wai-quantum/'.length); + pages[name] = c.text; + check(c.mimeType === 'text/html;profile=mcp-app' && bytes <= LIMIT && c.text.includes(`content="${name}"`) && !/