diff --git a/README.md b/README.md index a9b003e..23275ce 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ Quantum Folk Lab is intended for: ## The learning journey 1. **Make a prediction.** Look at eight small, invented tune variants and predict how they belong - together. + together. The console records the split without revealing whether it is right. 2. **Reveal every answer.** The app checks all 256 possible groupings and shows the best answers. 3. **Compare a quantum simulation.** See how a bounded quantum method behaves when the exact result is already known. @@ -61,7 +61,9 @@ Quantum Folk Lab is intended for: from IBM quantum hardware. 6. **Ask for an explanation.** Choose a learner level and optionally ask GPT-5.6 to explain the validated evidence. -7. **Keep the record.** Download the governed result for inspection or further discussion in +7. **Check your understanding.** Answer five short questions about exact truth, measurement, + hardware noise and the role of GPT-5.6. +8. **Keep the record.** Download the governed result for inspection or further discussion in ChatGPT or Codex. The synthetic teaching example is deliberately small. Checking all 256 answers is simpler and @@ -123,8 +125,10 @@ python -m streamlit run apps/learning_console/app.py ### What success looks like -Your browser should open the Quantum Folk Lab Learning Console. Select **Experiments**, begin with -**Start here · Guided experiment**, make a prediction, and press **Reveal all 256 answers**. +Your browser should open the Quantum Folk Lab Learning Console. Judges and first-time learners +should select **Experiments**, begin with **Start here · Guided experiment**, make a prediction, +press **Reveal all 256 answers**, and follow exact truth → simulation → hardware → explanation → +exit check. Then visit **Real folk data & IBM results** to follow the same exact-first method using committed real-data and hardware evidence. Use **Foundations** when you want the concepts explained from the diff --git a/apps/learning_console/renderers/compact_experiment.py b/apps/learning_console/renderers/compact_experiment.py index a23b421..155220e 100644 --- a/apps/learning_console/renderers/compact_experiment.py +++ b/apps/learning_console/renderers/compact_experiment.py @@ -42,6 +42,19 @@ def render_compact_experiment() -> None: "folk-tune families, check all sixteen combinations exactly, then compare simulation " "with governed IBM hardware evidence." ) + st.info( + "You learned the method using a small invented eight-choice problem. This separate " + "registered experiment applies the same exact-first comparison method to a smaller " + "four-choice problem that was executed on IBM quantum hardware." + ) + with st.expander("What stays the same—and what changes"): + st.markdown( + "**Stays the same:** define binary choices, calculate exact truth, inspect quantum " + "measurements, and compare their distribution with the reference.\n\n" + "**Changes:** the problem instance, number of variables, execution environment and " + "presence of physical hardware noise. These are not measurements from the synthetic " + "eight-choice circuit." + ) st.caption( "EXP-010A · compact real-data formulation · four families · two choices each · " "sixteen valid combinations" @@ -56,6 +69,9 @@ def render_compact_experiment() -> None: optimum = exact["optimum_bitstrings"][0] st.markdown("## First: what is the exact answer?") + with st.container(border=True): + st.markdown("### ✓ EXACT CLASSICAL REFERENCE") + st.caption("Complete enumeration supplies the answer used to judge every quantum result.") st.markdown("**Which combination is best when every possibility is checked?**") st.write( "Before looking at a simulator or quantum computer, the application checks all 16 " @@ -64,13 +80,16 @@ def render_compact_experiment() -> None: ) left, middle, right = st.columns(3) left.metric("Exact optimum", optimum) - middle.metric("Mapped R2 state", "01100110") + middle.metric("Equivalent earlier encoding", "01100110") right.metric("States checked", "16 / 16") st.success( f"Exact enumeration is authoritative. The minimum energy is {exact['minimum_energy']:.15f}." ) st.markdown("## Ideal quantum simulation") + with st.container(border=True): + st.markdown("### ◫ IDEAL OR REGISTERED SIMULATION") + st.caption("Committed simulated measurement evidence; not physical hardware.") st.markdown("**Does the ideal quantum circuit concentrate on the better choices?**") st.write( "R measures improvement over uniform sampling: higher positive values mean the circuit " @@ -90,6 +109,14 @@ def render_compact_experiment() -> None: ) st.markdown("## First IBM hardware validation") + with st.container(border=True): + st.markdown("### ◉ RECORDED IBM HARDWARE") + st.caption("Recorded physical-device evidence, subject to hardware imperfections.") + st.caption( + "Real quantum hardware is physically imperfect: gates and measurements sometimes " + "introduce small errors. That blurring of the ideal result is called noise. Noise is " + "one reason hardware and ideal simulation can differ." + ) st.markdown("**Did the correct answer remain visible on real hardware?**") st.write( "One IBM hardware run tested whether the compact real-data result remained visible under " @@ -135,26 +162,26 @@ def render_compact_experiment() -> None: { "bit": "y0", "family": "Blackbird", - "0 maps to": "R2 pair 10", - "1 maps to": "R2 pair 01", + "0 maps to": "earlier pair 10", + "1 maps to": "earlier pair 01", }, { "bit": "y1", "family": "Bold Deserter", - "0 maps to": "R2 pair 10", - "1 maps to": "R2 pair 01", + "0 maps to": "earlier pair 10", + "1 maps to": "earlier pair 01", }, { "bit": "y2", "family": "Catherine Tyrrell", - "0 maps to": "R2 pair 10", - "1 maps to": "R2 pair 01", + "0 maps to": "earlier pair 10", + "1 maps to": "earlier pair 01", }, { "bit": "y3", "family": "The Merry Old Woman", - "0 maps to": "R2 pair 10", - "1 maps to": "R2 pair 01", + "0 maps to": "earlier pair 10", + "1 maps to": "earlier pair 01", }, ] st.dataframe(choices, width="stretch", hide_index=True) @@ -176,7 +203,7 @@ def render_compact_experiment() -> None: ) st.markdown("## Technical evidence") - with st.expander("Encoding equivalence and earlier R2 comparison"): + with st.expander("Encoding equivalence and earlier technical comparison"): st.markdown((EXPERIMENT_ROOT / "R2-COMPARISON.md").read_text(encoding="utf-8")) with st.expander("Frozen QAOA report"): st.markdown((EXPERIMENT_ROOT / "QAOA-REPORT.md").read_text(encoding="utf-8")) diff --git a/apps/learning_console/renderers/directives.py b/apps/learning_console/renderers/directives.py index d291dab..48caabf 100644 --- a/apps/learning_console/renderers/directives.py +++ b/apps/learning_console/renderers/directives.py @@ -8,16 +8,104 @@ def render_visual(visual_id: str) -> None: + if visual_id == "bit-vs-qubit": + probability = st.slider( + "Chance of measuring 1", + min_value=0, + max_value=100, + value=50, + step=5, + key="foundations-qubit-probability", + help="The state also has phase information, which this probability view cannot show.", + ) + st.bar_chart( + {"Measured outcome": ["0", "1"], "Probability": [100 - probability, probability]}, + x="Measured outcome", + y="Probability", + horizontal=True, + ) + st.caption( + "Takeaway: amplitudes determine measurement probabilities, while phase affects how " + "later operations interfere. A qubit is not simply ‘both values at once’." + ) + return + if visual_id == "hadamard-probability-split": + shots = st.select_slider( + "Illustrated shot count", + options=[8, 32, 128, 512], + value=32, + key="foundations-shot-count", + ) + zero_count = shots // 2 + rows = [ + {"Outcome": "0", "Count": zero_count}, + {"Outcome": "1", "Count": shots - zero_count}, + ] + st.bar_chart(rows, x="Outcome", y="Count", horizontal=True) + st.caption( + "Takeaway: each shot produces one bit; repeated shots build an estimated distribution." + ) + return + if visual_id == "ideal-vs-noisy": + noise = st.slider( + "Illustrative hardware noise", + min_value=0, + max_value=20, + value=8, + step=2, + key="foundations-noise-level", + help="A teaching illustration, not a model of a particular device.", + ) + rows = [ + {"Evidence": "Ideal simulator", "Expected answer": 80, "Other answers": 20}, + { + "Evidence": "Illustrative hardware", + "Expected answer": 80 - noise, + "Other answers": 20 + noise, + }, + ] + st.bar_chart(rows, x="Evidence", y=["Expected answer", "Other answers"]) + st.caption( + "Takeaway: noise can blur a distribution, so hardware is compared with an exact " + "reference rather than treated as truth by itself." + ) + return + if visual_id == "z-phase-reveal": + st.markdown("**Same immediate probabilities:** 50% `0`, 50% `1` ") + st.markdown( + "**Different relative phase:** later gates can make the amplitudes add or cancel." + ) + st.caption("Takeaway: probability alone does not describe phase or interference.") + return + if visual_id == "double-h-interference": + st.bar_chart( + {"Measured outcome": ["0", "1"], "Probability": [100, 0]}, + x="Measured outcome", + y="Probability", + horizontal=True, + ) + st.caption( + "Takeaway: two Hadamard gates can interfere back to the definite starting state." + ) + return + if visual_id == "bell-correlation": + st.bar_chart( + {"Joint outcome": ["00", "01", "10", "11"], "Probability": [50, 0, 0, 50]}, + x="Joint outcome", + y="Probability", + ) + st.caption("Takeaway: the pair is correlated even though either result alone is uncertain.") + return + if visual_id == "x-gate-visual": + st.bar_chart( + {"Input": ["0", "1"], "After X": [1, 0]}, + x="Input", + y="After X", + ) + st.caption("Takeaway: X swaps the computational-basis states 0 and 1.") + return captions = { - "bit-vs-qubit": "Classical bit: definite 0 or 1. Qubit: amplitudes until measurement.", - "hadamard-probability-split": "After H on |0⟩, Theory predicts equal P(0) and P(1).", - "z-phase-reveal": ( - "A Z gate can change phase without changing computational-basis probabilities." - ), - "double-h-interference": "H then H can return to |0⟩ because amplitudes interfere.", - "bell-correlation": "Bell outcomes favour 00 and 11; each bit alone looks random.", - "circuit-thumbnail": "Circuit sketch placeholder — use EXP-001 for full circuit diagrams.", - "x-gate-visual": "X gate swaps |0⟩ and |1⟩.", + "circuit-thumbnail": "Circuit journey: prepare → apply a gate → measure → record one bit.", } st.info(captions.get(visual_id, f"Visual: {visual_id}")) diff --git a/apps/learning_console/renderers/guided_experiment.py b/apps/learning_console/renderers/guided_experiment.py index 62cfb59..580ee71 100644 --- a/apps/learning_console/renderers/guided_experiment.py +++ b/apps/learning_console/renderers/guided_experiment.py @@ -3,7 +3,15 @@ from __future__ import annotations import streamlit as st -from services.build_week_service import GuidedExperimentView, execute_quick_qiskit +from services.build_week_service import ( + VARIANT_DISPLAY_NAMES, + GuidedExperimentView, + PartitionView, + execute_quick_qiskit, + partition_from_indices, + partitions_are_equivalent, + summarise_quick_qiskit, +) from quantum_folk_lab.build_week import LearnerLevel, explain_result @@ -14,6 +22,123 @@ } +def _evidence_identity(label: str, icon: str, authority: str) -> None: + with st.container(border=True): + st.markdown(f"### {icon} {label}") + st.caption(authority) + + +def _display_evidence_pairs(view: GuidedExperimentView) -> list[dict[str, object]]: + display_by_id = dict(zip(view.result.tune_ordering, VARIANT_DISPLAY_NAMES, strict=True)) + return [ + { + "left variant": display_by_id[str(pair["left_tune"])], + "right variant": display_by_id[str(pair["right_tune"])], + "interval similarity": pair["interval_similarity"], + "contour similarity": pair["contour_similarity"], + "rhythm similarity": pair["rhythm_similarity"], + "combined similarity": pair["combined_similarity"], + } + for pair in view.result.evidence_summary["pairs"] + ] + + +def _render_partition(title: str, partition: PartitionView) -> None: + st.markdown(f"**{title}**") + left, right = st.columns(2) + left.markdown("**One family**") + left.write("\n".join(f"- {name}" for name in partition.first_group)) + right.markdown("**The complementary family**") + right.write("\n".join(f"- {name}" for name in partition.second_group)) + + +def _render_exit_check() -> None: + st.subheader("Check what you now understand") + st.write("Five short questions connect the exact answer, quantum evidence and AI boundary.") + questions = ( + ( + "Why are there 256 possible groupings?", + ( + "Eight independent binary choices give 2⁸ possibilities.", + "The circuit uses 256 qubits.", + "There are 256 tune recordings.", + ), + 0, + "Eight yes-or-no assignments create 2 × 2 × 2 × 2 × 2 × 2 × 2 × 2 = 256 states.", + ), + ( + "Why is exact classical calculation still the reference?", + ( + "It evaluates every possible answer for this small problem.", + "Classical computers are always faster.", + "It removes all modelling choices.", + ), + 0, + "Complete enumeration checks the full finite answer space, so sampled methods can " + "be judged against it.", + ), + ( + "Why does a quantum method return a distribution?", + ( + "Each shot measures one outcome, so repeated shots accumulate counts.", + "The answer changes culturally.", + "The exact result is unknown.", + ), + 0, + "A circuit measurement yields one bitstring per shot; many shots estimate its " + "outcome probabilities.", + ), + ( + "What is hardware noise?", + ( + "Errors and disturbances in physical qubits, gates and measurements.", + "Background music in the laboratory.", + "A deliberate change to the exact score.", + ), + 0, + "Noise is the collective effect of physical imperfections; one disagreement does " + "not identify a single cause.", + ), + ( + "What may GPT‑5.6 do here?", + ( + "Explain a validated evidence packet without changing it.", + "Calculate the exact optimum.", + "Authorise new hardware jobs.", + ), + 0, + "GPT‑5.6 is an optional explanation layer; validation fails closed to " + "deterministic text.", + ), + ) + with st.form("guided-exit-check"): + answers = [ + st.radio(prompt, options, index=None, key=f"guided-exit-{index}") + for index, (prompt, options, _, _) in enumerate(questions) + ] + submitted = st.form_submit_button("Check my answers") + if submitted: + st.session_state["guided_exit_submitted"] = True + if st.session_state.get("guided_exit_submitted"): + score = sum( + answer == options[correct] + for answer, (_, options, correct, _) in zip(answers, questions, strict=True) + ) + st.success(f"You answered {score} of 5 correctly.") + for answer, (prompt, options, correct, explanation) in zip(answers, questions, strict=True): + status = "Correct" if answer == options[correct] else "Review" + st.markdown(f"**{status} — {prompt}** \n{explanation}") + if st.button("Try the exit check again"): + st.session_state["guided_exit_submitted"] = False + st.rerun() + st.info( + "Quantum computers are a different way to process a scored problem, not magic. For this " + "small experiment, exact classical calculation supplies the truth. Simulation and hardware " + "can then be compared with it. The evidence is educational—not a claim of quantum " + "advantage." + ) + + def _render_landscape(view: GuidedExperimentView) -> None: landscape = view.landscape rows = landscape.chart_rows() @@ -59,7 +184,7 @@ def _render_landscape(view: GuidedExperimentView) -> None: "field": "display_energy", "type": "quantitative", "scale": {"scheme": "viridis", "reverse": True}, - "legend": {"title": "Exact energy"}, + "legend": {"title": "Exact energy (lower = better)"}, }, "stroke": { "condition": {"test": "datum.is_global_optimum", "value": "white"}, @@ -103,6 +228,101 @@ def _render_landscape(view: GuidedExperimentView) -> None: def _render_registered_comparison(view: GuidedExperimentView) -> None: evidence = view.registered_qaoa + _evidence_identity( + "IDEAL OR REGISTERED SIMULATION", + "◫", + "Committed simulated measurement evidence; not physical hardware.", + ) + st.subheader("What does the quantum method actually do?") + st.write( + "A quantum computer does not print one authoritative answer. The circuit is prepared " + "and measured once — one shot — and that measurement returns one candidate grouping. " + "Repeating this thousands of times creates a distribution of answers. A useful " + "optimisation method should place more measurement weight on better-scoring groupings. " + "The exact calculation above remains the authority used to judge it." + ) + st.markdown("### What did 4,096 shots actually return?") + measurement_rows = [item.chart_row() for item in view.registered_measurements] + measurement_order = [item["bitstring"] for item in measurement_rows] + st.vega_lite_chart( + measurement_rows, + { + "height": 320, + "layer": [ + { + "mark": {"type": "bar"}, + "encoding": { + "y": { + "field": "bitstring", + "type": "nominal", + "sort": measurement_order, + "title": "Returned grouping", + }, + "x": { + "field": "count", + "type": "quantitative", + "title": "Registered measurement count", + }, + "color": { + "field": "status", + "type": "nominal", + "scale": { + "domain": ["Exact optimum", "Other grouping"], + "range": ["#2a9d8f", "#6c83a6"], + }, + "legend": None, + }, + "tooltip": [ + {"field": "bitstring", "type": "nominal", "title": "Grouping"}, + {"field": "count", "type": "quantitative", "title": "Count"}, + {"field": "status", "type": "nominal", "title": "Exact check"}, + ], + }, + }, + { + "mark": { + "type": "text", + "align": "left", + "dx": 6, + "color": "#ffffff", + "fontWeight": "bold", + }, + "encoding": { + "y": { + "field": "bitstring", + "type": "nominal", + "sort": measurement_order, + }, + "x": {"field": "zero", "type": "quantitative"}, + "text": {"field": "optimum_note"}, + }, + }, + { + "mark": {"type": "text", "align": "left", "dx": 5, "color": "#ffffff"}, + "encoding": { + "y": { + "field": "bitstring", + "type": "nominal", + "sort": measurement_order, + }, + "x": {"field": "count", "type": "quantitative"}, + "text": {"field": "count", "type": "quantitative"}, + }, + }, + ], + }, + width="stretch", + ) + st.caption( + "Each bar is one answer returned by the registered ideal simulation. These are the ten " + f"most frequent states, not all {view.registered_distinct_state_count} observed states. " + "Exact-optimum states are labelled so the simulated measurements can be checked against " + "the known answer. This is registered simulation evidence, not IBM hardware." + ) + st.write( + "Many different groupings appeared, but the measurement weight was uneven. The next " + "comparison checks how much of it landed on the two known best groupings." + ) st.subheader("How did the quantum method do?") st.markdown( "**Registered evidence · Local ideal simulation · Not quantum hardware · " @@ -199,6 +419,11 @@ def _render_registered_comparison(view: GuidedExperimentView) -> None: def _render_evidence_hierarchy() -> None: + _evidence_identity( + "GPT‑5.6 EXPLANATION", + "◇", + "Explains a validated packet; does not calculate or change the result.", + ) st.markdown( "**Exact enumeration** \n" "↓ governs \n" @@ -225,9 +450,8 @@ def render_guided_experiment(view: GuidedExperimentView) -> None: st.header("Can you spot the hidden split?") st.markdown( "**Eight tune variants. Two hidden families. 256 possible groupings.**\n\n" - "Before revealing the answer, inspect the musical evidence and decide which variants " - "you think belong together. You do not need to enter a formal answer — make a mental " - "prediction, then test it." + "Before revealing the answer, inspect the musical evidence and record which four variants " + "you think belong together. Choosing one family automatically defines the other." ) with st.expander("Look at the musical evidence"): @@ -236,13 +460,38 @@ def render_guided_experiment(view: GuidedExperimentView) -> None: "variants may belong to the same hidden family. This is synthetic teaching material, " "not authentic cultural material." ) - pairs = result.evidence_summary["pairs"] + pairs = _display_evidence_pairs(view) st.dataframe(pairs, width="stretch", hide_index=True) st.caption( "An edge joins a pair when its combined synthetic similarity passes the fixed graph " "threshold. This is not authentic cultural data." ) + st.markdown("### Make and record your prediction") + selected_names = st.multiselect( + "Choose exactly four variants for one family", + VARIANT_DISPLAY_NAMES, + default=[ + VARIANT_DISPLAY_NAMES[index] + for index in st.session_state.get("build_week_prediction_indices", ()) + ], + help="The other four variants automatically form the complementary family.", + ) + selected_indices = tuple( + index for index, name in enumerate(VARIANT_DISPLAY_NAMES) if name in selected_names + ) + if len(selected_indices) == 4: + prediction = partition_from_indices(selected_indices) + st.session_state["build_week_prediction_indices"] = selected_indices + _render_partition("Your recorded prediction", prediction) + st.caption("You can revise this split until Reveal. No score or answer is shown yet.") + elif selected_names: + st.session_state.pop("build_week_prediction_indices", None) + st.warning(f"Choose exactly four variants. You currently selected {len(selected_names)}.") + else: + st.session_state.pop("build_week_prediction_indices", None) + st.caption("You may skip the prediction and still reveal the governed evidence.") + if st.button("Reveal all 256 answers", type="primary"): st.session_state["build_week_256_revealed"] = True if not st.session_state.get("build_week_256_revealed", False): @@ -250,27 +499,85 @@ def render_guided_experiment(view: GuidedExperimentView) -> None: return st.subheader("What did every possible grouping score?") + st.write( + "Each of the eight variants can join either family, so there are " + "2 × 2 × 2 × 2 × 2 × 2 × 2 × 2 = 256 possible groupings." + ) + st.info( + "Energy is this model's score for one possible grouping. Lower energy means a better " + "answer. The outlined cells mark the exact best groupings." + ) _render_landscape(view) st.subheader("What is the exact answer?") + _evidence_identity( + "EXACT CLASSICAL REFERENCE", + "✓", + "Complete enumeration supplies the answer used to judge every quantum result.", + ) st.write("This answer is not a prediction — the computer tried every possibility.") exact = result.exact_result left, middle, right = st.columns(3) left.metric("Minimum energy", f"{float(exact['minimum_energy']):.6f}") middle.metric("Canonical split", str(exact["canonical_complement_class"])) right.metric("Assignments checked", str(exact["evaluated_assignments"])) + st.write( + "`00001111` has one digit for each tune variant. A `0` places that variant in one " + "family and a `1` places it in the other. `11110000` describes the same split with the " + "family labels exchanged." + ) st.success( "Exhaustive enumeration is authoritative for this eight-variable fixture. The global " "bitwise complement denotes the same unlabeled partition." ) - - st.info( - "Because the exact answer is known, every quantum result below can be checked rather " - "than taken on trust." + exact_partition = partition_from_indices( + tuple( + index + for index, bit in enumerate(str(exact["canonical_complement_class"])) + if bit == "1" + ) ) - _render_registered_comparison(view) + _render_partition("Exact split in named variants", exact_partition) + st.caption( + "The displayed names follow bit positions from left to right. Family labels are " + "exchangeable and carry no inherent musical or cultural meaning." + ) + recorded_indices = st.session_state.get("build_week_prediction_indices") + if recorded_indices is None: + st.info("You skipped the prediction step. The exact journey remains complete.") + else: + learner_partition = partition_from_indices(tuple(recorded_indices)) + _render_partition("Your prediction", learner_partition) + if partitions_are_equivalent( + learner_partition.bitstring, str(exact["canonical_complement_class"]) + ): + st.success( + "Your prediction found the exact split—even if its family labels are swapped." + ) + else: + entry = next( + item + for item in view.landscape.entries + if item.assignment == learner_partition.bitstring + ) + st.info( + "Your split was a valid candidate rather than the exact minimum. Its governed " + f"energy was {entry.display_energy:.6f}. Comparing predictions is part of the " + "lesson." + ) + st.caption( + "Your family labels can be swapped, so we compare the split itself—not whether you " + "called a group A or B." + ) st.subheader("How was the question turned into a model?") + st.write( + "To hand this question to a quantum method, it is rewritten in three steps. First, " + "each tune variant becomes a 0-or-1 choice. Second, any complete set of eight choices — " + "such as `00001111` — is one possible answer. Third, a score called energy says how " + "good that answer is; lower is better. The quantum circuit is built from that scored " + "problem." + ) with st.expander("Technical model and QUBO"): st.write(result.fixture_description) st.write( @@ -279,6 +586,12 @@ def render_guided_experiment(view: GuidedExperimentView) -> None: ) st.json({"parameters": result.parameters, "QUBO summary": result.qubo_summary}) + st.info( + "Because the exact answer is known, every quantum result below can be checked rather " + "than taken on trust." + ) + _render_registered_comparison(view) + st.subheader("Want to run a small local quantum comparison?") st.caption( "This live bounded quick run uses a smaller submission-safe contract. It is separate " @@ -294,14 +607,40 @@ def render_guided_experiment(view: GuidedExperimentView) -> None: st.warning(f"The quick run is unavailable: {exc}") quantum = st.session_state.get("build_week_quantum") if quantum: - st.markdown("**Quick local-Qiskit result computed now**") - st.json(quantum) - st.warning( - "A best sampled optimum does not prove an optimal expectation, speedup, or " - "quantum advantage." - ) + st.markdown("**OPTIONAL LOCAL SIMULATION · computed on this machine**") + try: + summary = summarise_quick_qiskit(quantum) + except (TypeError, ValueError) as exc: + st.warning(f"The local run completed but its display summary was invalid: {exc}") + else: + first, second, third = st.columns(3) + first.metric("Total shots", f"{summary.shots:,}") + second.metric("Distinct states", str(summary.distinct_states)) + third.metric("Most frequent state", summary.most_frequent_state) + st.write( + "The most frequent state was " + f"{'an' if summary.most_frequent_is_optimum else 'not an'} " + "exact optimum. Across both exact optima, " + f"{summary.optimum_count:,} measurements " + f"({summary.optimum_probability:.2%}) landed " + "on the known answer class." + ) + rows = [item.chart_row() for item in summary.top_states] + st.bar_chart(rows, x="bitstring", y="count") + st.caption("This optional local ideal simulation is not registered IBM hardware.") + st.warning( + "A best sampled optimum does not prove an optimal expectation, speedup, or " + "quantum advantage." + ) + with st.expander("View technical run data"): + st.json(quantum) else: - st.code(view.quantum.install_command) + st.info( + "Qiskit is optional. The registered exact, simulated and recorded-hardware journey " + "above remains complete without it." + ) + with st.expander("Optional installation command"): + st.code(view.quantum.install_command) st.subheader("Can AI explain the result safely?") _render_evidence_hierarchy() label = st.selectbox("Explanation level", list(LEVEL_LABELS)) @@ -319,6 +658,8 @@ def render_guided_experiment(view: GuidedExperimentView) -> None: st.caption(f"Validated grounded explanation from {generated.model}.") st.write(explanation) + _render_exit_check() + st.subheader("Want to inspect or share the reproducibility record?") first, second = st.columns(2) first.download_button( diff --git a/apps/learning_console/renderers/lesson_renderer.py b/apps/learning_console/renderers/lesson_renderer.py index 993f894..ab9fe53 100644 --- a/apps/learning_console/renderers/lesson_renderer.py +++ b/apps/learning_console/renderers/lesson_renderer.py @@ -2,6 +2,9 @@ from __future__ import annotations +import html +import re + import streamlit as st from quantum_folk_lab.learning.models import ( @@ -18,6 +21,87 @@ from quantum_folk_lab.learning.validation import validate_lesson_document from renderers.directives import render_interaction, render_visual +FLOWCHART_HEADER_RE = re.compile(r"^flowchart\s+(LR|TB)$") +FLOWCHART_EDGE_RE = re.compile( + r'^([A-Za-z][A-Za-z0-9_]*)(?:\["([^"]+)"\])?\s*-->\s*' + r'([A-Za-z][A-Za-z0-9_]*)(?:\["([^"]+)"\])?$' +) + + +def _flowchart_paths(source: str) -> tuple[str, tuple[tuple[str, ...], ...]] | None: + """Parse the repository's small, validated Mermaid flowchart subset.""" + + lines = [line.strip() for line in source.splitlines() if line.strip()] + if not lines or not (header := FLOWCHART_HEADER_RE.fullmatch(lines[0])): + return None + labels: dict[str, str] = {} + children: dict[str, list[str]] = {} + targets: set[str] = set() + node_order: list[str] = [] + for line in lines[1:]: + edge = FLOWCHART_EDGE_RE.fullmatch(line) + if edge is None: + return None + source_id, source_label, target_id, target_label = edge.groups() + for node_id in (source_id, target_id): + if node_id not in node_order: + node_order.append(node_id) + if source_label: + labels[source_id] = source_label + if target_label: + labels[target_id] = target_label + children.setdefault(source_id, []).append(target_id) + targets.add(target_id) + if not node_order or any(node_id not in labels for node_id in node_order): + return None + roots = [node_id for node_id in node_order if node_id not in targets] + if not roots: + return None + paths: list[tuple[str, ...]] = [] + + def visit(node_id: str, path: tuple[str, ...]) -> bool: + if node_id in path: + return False + next_path = (*path, node_id) + next_nodes = children.get(node_id, []) + if not next_nodes: + paths.append(tuple(labels[item] for item in next_path)) + return True + return all(visit(child, next_path) for child in next_nodes) + + if not all(visit(root, ()) for root in roots): + return None + return header.group(1), tuple(paths) + + +def _flowchart_html(source: str) -> str | None: + parsed = _flowchart_paths(source) + if parsed is None: + return None + direction, paths = parsed + arrow = "→" if direction == "LR" else "↓" + rows: list[str] = [] + for path in paths: + parts: list[str] = [] + for index, label in enumerate(path): + if index: + parts.append(f'') + parts.append( + '' + f"{html.escape(label)}" + ) + flex_direction = "row" if direction == "LR" else "column" + rows.append( + f'
Interactive in Learning Console: " f"{html.escape(block.interaction_id)}
" ) + elif isinstance(block, DisclosureDirective) and block.body: + body_parts.append( + f"{html.escape(block.body)}