…"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "2efb7eba17864530a1bf7f4a6da64edb",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"VBox(children=(HTML(value='
…"
]
},
"execution_count": 27,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"quiz(tracker, \"q9_stabilizer_meaning\",\n",
" question=\"What does it mean when \\u27E8ZZZZ\\u27E9 = +1?\",\n",
" options=[\n",
" \"All four qubits are in the |0\\u27E9 state\",\n",
" \"The state is in the codespace \\u2014 no X-type error has been detected\",\n",
" \"The Z-gate has been applied to all four qubits\",\n",
" \"The state has zero energy\",\n",
" ],\n",
" correct=1,\n",
" section=\"6. Stabilizers\",\n",
" bloom=\"understand\",\n",
" explanation=(\n",
" \"\\u27E8ZZZZ\\u27E9 = +1 means the state is an eigenstate of ZZZZ with eigenvalue +1. \"\n",
" \"This is the codespace condition. If a single X-error occurs on any qubit, \"\n",
" \"ZZZZ anti-commutes with that X, flipping the eigenvalue to \\u22121.\"\n",
" ))"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"quiz(tracker, \"q10_which_detects_what\",\n",
" question=\"A single Z error on qubit 3 occurs. Which stabilizer detects it?\",\n",
" options=[\n",
" \"ZZZZ (because Z commutes with Z, so ZZZZ stays at +1) \\u2014 wait, that means ZZZZ does NOT detect it\",\n",
" \"XXXX detects it (because Z anti-commutes with X)\",\n",
" \"Both XXXX and ZZZZ detect it\",\n",
" \"Neither \\u2014 Z errors are undetectable\",\n",
" ],\n",
" correct=1,\n",
" bloom=\"apply\",\n",
" explanation=(\n",
" \"Z commutes with Z (ZZ = ZZ), so ZZZZ is unaffected by a Z error. \"\n",
" \"But Z anti-commutes with X (ZX = \\u2212XZ), so XXXX flips to \\u22121. \"\n",
" \"Rule of thumb: X-errors are caught by the Z-stabilizer, and Z-errors by the X-stabilizer.\"\n",
" ))\n",
"\n",
"checkpoint_summary(tracker, \"6. Stabilizers\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 7. Error Detection in Action — Break It on Purpose\n",
"\n",
"### The experiment\n",
"\n",
"Let's deliberately inject errors and watch the stabilizers respond. We'll apply X (bit-flip), Z (phase-flip), and Y (both) errors on every qubit.\n",
"\n",
"**Prediction guide** before you run the next cell:\n",
"- An **X error** on any qubit: $X_j$ anti-commutes with $Z_j$ in $ZZZZ$, so $ZZZZ$ flips to $-1$. $XXXX$ is unaffected.\n",
"- A **Z error** on any qubit: $Z_j$ anti-commutes with $X_j$ in $XXXX$, so $XXXX$ flips to $-1$. $ZZZZ$ is unaffected.\n",
"- A **Y error** ($Y = iXZ$): both stabilizers flip to $-1$."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"predict_choice(tracker, \"q11_y_error_prediction\",\n",
" question=\"A Y error on qubit 2: how many stabilizers will flip to \\u22121?\",\n",
" options=[\"0 \\u2014 Y errors are undetectable\", \"1 \\u2014 either XXXX or ZZZZ\", \"2 \\u2014 both XXXX and ZZZZ\"],\n",
" correct=2,\n",
" section=\"7. Error detection\",\n",
" bloom=\"understand\",\n",
" explanation=(\n",
" \"Y = iXZ contains both an X-part and a Z-part. \"\n",
" \"The X-part is caught by ZZZZ and the Z-part by XXXX. \"\n",
" \"Both stabilizers flip \\u2192 Y errors have the most distinctive syndrome signature.\"\n",
" ))"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Single error example: X on qubit 1\n",
"error_circuit = prep.copy()\n",
"error_circuit.x(1)\n",
"corrupted_sv = Statevector.from_instruction(error_circuit)\n",
"\n",
"print(\"Stabilizers AFTER X error on qubit 1:\")\n",
"for name, operator in STABILIZERS.items():\n",
" expectation = corrupted_sv.expectation_value(operator)\n",
" status = \"PASS\" if abs(expectation.real - 1.0) < 1e-6 else \"FAIL (error detected!)\"\n",
" print(f\" <{operator.to_list()[0][0]}> = {expectation.real:+.6f} [{status}]\")\n",
"\n",
"print(\"\\nCompare with the error-free case:\")\n",
"for name, operator in STABILIZERS.items():\n",
" expectation = encoded_sv.expectation_value(operator)\n",
" print(f\" <{operator.to_list()[0][0]}> = {expectation.real:+.6f} [PASS]\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Complete error detection table\n",
"print(\"Error detection summary:\")\n",
"print(f\"{'Error':>12s} | {'':>8s} | {'':>8s} | Detected by\")\n",
"print(\"-\" * 55)\n",
"\n",
"for qubit in range(4):\n",
" for error_name, error_gate in [(\"X\", \"x\"), (\"Z\", \"z\"), (\"Y\", \"y\")]:\n",
" err_circ = prep.copy()\n",
" getattr(err_circ, error_gate)(qubit)\n",
" err_sv = Statevector.from_instruction(err_circ)\n",
" xxxx = err_sv.expectation_value(STABILIZERS[\"x_stabilizer\"]).real\n",
" zzzz = err_sv.expectation_value(STABILIZERS[\"z_stabilizer\"]).real\n",
" detected_by = []\n",
" if abs(xxxx - 1.0) > 0.01:\n",
" detected_by.append(\"XXXX\")\n",
" if abs(zzzz - 1.0) > 0.01:\n",
" detected_by.append(\"ZZZZ\")\n",
" print(f\"{error_name}(q{qubit}): | {xxxx:+.4f} | {zzzz:+.4f} | {', '.join(detected_by) or 'none'}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Reading the table\n",
"\n",
"Every single-qubit error is caught by at least one stabilizer:\n",
"\n",
"| Error type | Caught by | Reason |\n",
"|-----------|-----------|--------|\n",
"| X (bit-flip) | ZZZZ | X anti-commutes with Z |\n",
"| Z (phase-flip) | XXXX | Z anti-commutes with X |\n",
"| Y (both) | XXXX and ZZZZ | Y = iXZ, so both parts are caught |\n",
"\n",
"This is the **distance-2 guarantee**: the code detects all weight-1 errors. A weight-2 error (two qubits affected simultaneously) could go undetected — that's the limitation of distance 2."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"order(tracker, \"q12_error_ranking\",\n",
" instruction=\"Sort error types by number of stabilizers they trigger (fewest \\u2192 most):\",\n",
" items=[\"X\", \"Z\", \"Y\"],\n",
" correct_order=[\"X\", \"Z\", \"Y\"],\n",
" bloom=\"analyze\",\n",
" explanation=(\n",
" \"X and Z each trigger exactly 1 stabilizer (tied). \"\n",
" \"Y triggers 2. So the order is X=Z < Y. \"\n",
" \"(X and Z are interchangeable in this ranking.)\"\n",
" ),\n",
" ties=[[\"X\", \"Z\"]])\n",
"\n",
"checkpoint_summary(tracker, \"7. Error detection\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 8. Comparing Encoder Styles\n",
"\n",
"### Why two encoders?\n",
"\n",
"Just as there are multiple seed styles, there are multiple ways to build the encoder circuit. The project implements:\n",
"\n",
"| Style | Native gates | Pre-transpilation depth |\n",
"|-------|-------------|------------------------|\n",
"| `cx_chain` | CNOT (CX) | 7 |\n",
"| `cz_compiled` | CZ (controlled-Z) | 11 |\n",
"\n",
"Both produce the **exact same** logical state. The choice matters after **transpilation**: if your hardware natively supports CZ gates, then `cz_compiled` may actually be *shorter* after transpilation than `cx_chain` (which would need each CNOT decomposed into CZ + Hadamard pairs)."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Compare the two encoder styles\n",
"encoder_cz = build_encoder(\"cz_compiled\")\n",
"print(\"cz_compiled encoder circuit:\")\n",
"fig = encoder_cz.draw('mpl', style='iqp')\n",
"plt.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Verify both encoders produce the same state\n",
"prep_cx = build_preparation_circuit(\"h_p\", \"cx_chain\")\n",
"prep_cz = build_preparation_circuit(\"h_p\", \"cz_compiled\")\n",
"\n",
"sv_cx = Statevector.from_instruction(prep_cx)\n",
"sv_cz = Statevector.from_instruction(prep_cz)\n",
"\n",
"fid = state_fidelity(sv_cx, sv_cz)\n",
"print(f\"Fidelity between cx_chain and cz_compiled: {fid:.6f}\")\n",
"print(f\"cx_chain depth: {prep_cx.depth()}, cz_compiled depth: {prep_cz.depth()}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"reflect(tracker, \"q13_encoder_tradeoff\",\n",
" question=\"The cz_compiled encoder has depth 11 vs cx_chain's depth 7. When might you prefer the deeper circuit?\",\n",
" section=\"8. Encoder comparison\",\n",
" bloom=\"evaluate\",\n",
" model_answer=(\n",
" \"If the hardware natively supports CZ gates (like many superconducting processors), \"\n",
" \"cx_chain would need each CNOT decomposed into CZ + Hadamard pairs, \"\n",
" \"potentially making it deeper AFTER transpilation. \"\n",
" \"The lesson: pre-transpilation depth is misleading. \"\n",
" \"What matters is depth after transpilation to the hardware's native gate set.\"\n",
" ))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 9. Verification Circuits with Ancilla Qubits\n",
"\n",
"### The measurement problem\n",
"\n",
"We just computed $\\langle ZZZZ \\rangle$ and $\\langle XXXX \\rangle$ using the full statevector. But on a real quantum computer, we don't have access to the statevector — we can only **measure** qubits.\n",
"\n",
"**The catch:** If we measure the 4 data qubits directly, the measurement collapses the superposition and **destroys** the encoded state. We'd learn the stabilizer value, but the T-state would be gone.\n",
"\n",
"### The solution: ancilla qubits\n",
"\n",
"Instead, we use **ancilla** (helper) qubits. The idea:\n",
"1. Prepare an ancilla qubit in $|0\\rangle$\n",
"2. Apply controlled gates between the ancilla and each data qubit to transfer the stabilizer's **parity** onto the ancilla\n",
"3. Measure **only the ancilla** — the data qubits remain untouched\n",
"\n",
"The ancilla measurement result (0 or 1) tells us the stabilizer eigenvalue ($+1$ or $-1$) without collapsing the data qubits. This is called **syndrome extraction**.\n",
"\n",
"The result is a **syndrome** bitstring. For our code with two stabilizers, the syndrome is 2 bits:\n",
"- `00` → both stabilizers +1 → no error detected → **keep this shot**\n",
"- Anything else → error detected → **discard this shot** (postselection)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Build the full circuit bundle for a standard experiment\n",
"spec = ExperimentSpec(\n",
" rung=1,\n",
" seed_style=\"h_p\",\n",
" encoder_style=\"cx_chain\",\n",
" verification=\"both\",\n",
" postselection=\"all_measured\",\n",
" ancilla_strategy=\"dedicated_pair\",\n",
" shots=256,\n",
" repeats=1,\n",
")\n",
"\n",
"bundle = build_circuit_bundle(spec)\n",
"print(f\"Bundle contains:\")\n",
"print(f\" prep circuit: {bundle.prep.num_qubits} qubits (data only)\")\n",
"print(f\" acceptance circuit: {bundle.acceptance.num_qubits} qubits ({DATA_QUBITS} data + {bundle.acceptance.num_qubits - DATA_QUBITS} ancilla)\")\n",
"print(f\" witness circuits: {list(bundle.witness_circuits.keys())}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Show the acceptance circuit\n",
"print(\"Acceptance circuit (syndrome extraction + data readout):\")\n",
"print(f\" Classical registers: {[creg.name + f'[{creg.size}]' for creg in bundle.acceptance.cregs]}\")\n",
"fig = bundle.acceptance.draw('mpl', style='iqp')\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Reading the acceptance circuit\n",
"\n",
"The circuit has two **classical registers**:\n",
"- **syndrome** [2 bits]: Results from ancilla measurements. Bit 0 = $ZZZZ$ check, bit 1 = $XXXX$ check.\n",
"- **readout** [4 bits]: Results from measuring the data qubits.\n",
"\n",
"In the circuit diagram, look for:\n",
"- The **ancilla qubits** (extra qubits above/below the data register)\n",
"- **Controlled gates** connecting ancillas to data qubits (these extract the parity)\n",
"- **Measurement gates** (the meter symbol) on the ancillas and data qubits"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"quiz(tracker, \"q14_ancilla_purpose\",\n",
" question=\"Why can't we just measure the 4 data qubits to check the stabilizers?\",\n",
" options=[\n",
" \"We can, but ancillas make it faster\",\n",
" \"Direct measurement collapses the superposition and destroys the encoded state\",\n",
" \"The stabilizer operators are not physical observables\",\n",
" ],\n",
" correct=1,\n",
" section=\"9. Verification circuits\",\n",
" bloom=\"understand\",\n",
" explanation=(\n",
" \"Measuring individual qubits in the Z basis projects them into |0\\u27E9 or |1\\u27E9, \"\n",
" \"destroying the superposition that encodes the T-state. \"\n",
" \"Ancilla-based syndrome extraction reads the stabilizer eigenvalue \"\n",
" \"without disturbing the logical information.\"\n",
" ))"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Show a witness circuit (logical_x)\n",
"print(\"Logical X witness circuit:\")\n",
"print(f\" Measures operator: {MEASUREMENT_OPERATORS['logical_x']}\")\n",
"fig = bundle.witness_circuits[\"logical_x\"].draw('mpl', style='iqp')\n",
"plt.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"quiz(tracker, \"q15_three_circuits\",\n",
" question=\"The bundle has 3 witness circuits (logical_x, logical_y, spectator_z). Why not measure all operators in one circuit?\",\n",
" options=[\n",
" \"The operators don't commute \\u2014 measuring one would disturb the others\",\n",
" \"It would require too many qubits\",\n",
" \"The simulator can only handle one operator at a time\",\n",
" ],\n",
" correct=0,\n",
" bloom=\"analyze\",\n",
" explanation=(\n",
" \"Logical X (X\\u2080X\\u2082) and Logical Y (Y\\u2080Z\\u2081X\\u2082) do not commute. \"\n",
" \"Measuring one changes the state in a way that invalidates the other measurement. \"\n",
" \"So each must be measured on a separate, independently prepared copy of the state.\"\n",
" ))\n",
"\n",
"checkpoint_summary(tracker, \"9. Verification circuits\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 10. Running an Ideal Simulation\n",
"\n",
"### What to expect\n",
"\n",
"In a **noiseless** simulation, the state is always in the codespace. That means:\n",
"- Both stabilizers always return +1\n",
"- Syndrome is always `00`\n",
"- **Every** shot passes postselection → acceptance rate = 100%\n",
"\n",
"This is our baseline. Real hardware will be much noisier."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Run on ideal AerSimulator\n",
"sim = AerSimulator()\n",
"shots = 512\n",
"\n",
"result = sim.run(bundle.acceptance, shots=shots).result()\n",
"counts = result.get_counts()\n",
"\n",
"print(f\"Ran {shots} shots on ideal simulator.\")\n",
"print(f\"Number of distinct outcomes: {len(counts)}\")\n",
"print(f\"\\nRaw counts (syndrome + readout):\")\n",
"for bitstring, count in sorted(counts.items(), key=lambda x: -x[1]):\n",
" parts = bitstring.split(\" \")\n",
" if len(parts) == 2:\n",
" syndrome, readout = parts[0], parts[1]\n",
" else:\n",
" syndrome, readout = \"\", parts[0]\n",
" print(f\" syndrome={syndrome} readout={readout} : {count} shots ({100*count/shots:.1f}%)\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"quiz(tracker, \"q16_ideal_acceptance\",\n",
" question=\"In the ideal simulation above, what fraction of shots have syndrome = '00'?\",\n",
" options=[\"About 25%\", \"About 50%\", \"About 75%\", \"100%\"],\n",
" correct=3,\n",
" section=\"10. Ideal simulation\",\n",
" bloom=\"understand\",\n",
" explanation=(\n",
" \"With no noise, the state never leaves the codespace. \"\n",
" \"Every stabilizer measurement returns +1, giving syndrome 00 on every shot.\"\n",
" ))"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Visualize\n",
"fig = plot_histogram(counts, figsize=(12, 5), title=\"Ideal Simulation: Syndrome + Readout\")\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 11. Postselection — Keeping Only the Good Shots\n",
"\n",
"### The idea\n",
"\n",
"**Postselection** is a filter: after collecting all measurement results, we look at the syndrome bits and **discard** any shot where the syndrome indicates an error was detected.\n",
"\n",
"In the ideal case (above), this filter has no effect — all shots pass. But under noise (Notebook 2), many shots will fail the syndrome check. The trade-off:\n",
"\n",
"| | Without postselection | With postselection |\n",
"|---|---|---|\n",
"| **Shots used** | All | Only syndrome = \"00\" |\n",
"| **Quality** | Lower (includes error-corrupted shots) | Higher (only clean shots) |\n",
"| **Statistical power** | More data points | Fewer data points |\n",
"| **Cost** | Lower | Higher (need more total shots for same statistics) |\n",
"\n",
"The scoring formula in this project captures this tension directly:\n",
"\n",
"$$\\text{score} = \\frac{\\text{quality} \\times \\text{acceptance\\_rate}}{\\text{cost}}$$"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from autoresearch_quantum.execution.analysis import (\n",
" postselection_passes, local_memory_records, summarize_context,\n",
")\n",
"\n",
"# Run with memory to access individual shot records\n",
"shots = 1024\n",
"sim = AerSimulator()\n",
"result = sim.run(bundle.acceptance, shots=shots, memory=True).result()\n",
"memory = result.get_memory(bundle.acceptance)\n",
"\n",
"# Parse into structured records\n",
"records = local_memory_records(memory, [creg.name for creg in bundle.acceptance.cregs])\n",
"syndrome_labels = bundle.acceptance.metadata.get(\"syndrome_labels\", [])\n",
"postselection_rule = bundle.acceptance.metadata.get(\"postselection\", \"all_measured\")\n",
"\n",
"summary = summarize_context(records, syndrome_labels, postselection_rule)\n",
"\n",
"print(f\"Total shots: {summary['total_shots']}\")\n",
"print(f\"Accepted shots: {summary['accepted_shots']}\")\n",
"print(f\"Acceptance rate: {summary['acceptance_rate']:.4f}\")\n",
"print(f\"\\nSyndrome distribution:\")\n",
"for syndrome, count in sorted(summary['syndrome_counts'].items()):\n",
" pct = count / summary['total_shots'] * 100\n",
" label = \"PASS\" if all(b == '0' for b in syndrome) else \"FAIL\"\n",
" print(f\" syndrome={syndrome} count={count:4d} ({pct:5.1f}%) [{label}]\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"quiz(tracker, \"q17_postselection_cost\",\n",
" question=\"What is the fundamental cost of postselection?\",\n",
" options=[\n",
" \"It makes the circuit deeper and noisier\",\n",
" \"It throws away shots, reducing the amount of usable data\",\n",
" \"It introduces classical computation overhead\",\n",
" \"It requires extra qubits\",\n",
" ],\n",
" correct=1,\n",
" section=\"11. Postselection\",\n",
" bloom=\"understand\",\n",
" explanation=(\n",
" \"Postselection discards shots where errors were detected. \"\n",
" \"Fewer usable shots means worse statistics or more total shots needed. \"\n",
" \"This is why acceptance_rate appears in the scoring formula \\u2014 \"\n",
" \"a code that rejects 90% of shots must produce 10x the quality to break even.\"\n",
" ))\n",
"\n",
"checkpoint_summary(tracker, \"11. Postselection\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 12. Summary\n",
"\n",
"In this notebook you learned:\n",
"\n",
"| Concept | What you now know |\n",
"|---------|------------------|\n",
"| **T-state** | The non-Clifford resource $|T\\rangle = (|0\\rangle + e^{i\\pi/4}|1\\rangle)/\\sqrt{2}$ needed for universal QC |\n",
"| **Seed styles** | Three equivalent gate sequences (h_p, ry_rz, u_magic) — same physics, different engineering trade-offs |\n",
"| **[[4,2,2]] code** | 4 physical qubits, 2 logical qubits, distance 2 — detects but doesn't correct single errors |\n",
"| **Stabilizers** | XXXX and ZZZZ act as quantum checksums — eigenvalue +1 means \"no error\" |\n",
"| **Error detection** | X→caught by ZZZZ, Z→caught by XXXX, Y→caught by both |\n",
"| **Ancilla qubits** | Allow syndrome extraction without destroying the encoded state |\n",
"| **Postselection** | Discard error-flagged shots to improve quality at the cost of acceptance rate |\n",
"\n",
"**Next up (Notebook 2):** What happens when we add realistic noise? How bad does it get? And how do we measure whether the encoding actually *helped*? We'll introduce the magic witness formula, the full scoring function, and explore how noise degrades each metric."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## Final Assessment"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"tracker.dashboard()\n",
"path = tracker.save()\n",
"print(f\"\\nProgress saved to: {path}\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.14.2"
}
},
"nbformat": 4,
"nbformat_minor": 4
}