QuantumLearning/scripts/harden_course_flow.py

280 lines
12 KiB
Python

#!/usr/bin/env python3
from __future__ import annotations
import hashlib
import json
from pathlib import Path
from quantum_learning import (
canonical_course_steps,
completion_notebook_path,
entry_notebook_path,
legacy_archive_dir,
project_root,
reference_notebook_path,
)
from quantum_learning.course_flow import LEGACY_SINGLE_NOTEBOOKS
ROOT = project_root()
NOTEBOOKS = ROOT / "notebooks"
REFERENCE_DIR = reference_notebook_path().parent
LEGACY_DIR = legacy_archive_dir()
NAV_TOP_MARKER = "<!-- COURSE_NAV_TOP -->"
NAV_BOTTOM_MARKER = "<!-- COURSE_NAV_BOTTOM -->"
def cell_id(index: int, cell: dict) -> str:
source = "".join(cell.get("source", []))
digest = hashlib.sha1(f"{cell.get('cell_type', 'cell')}:{index}:{source}".encode()).hexdigest()
return digest[:8]
def markdown_cell(text: str) -> dict:
return {
"cell_type": "markdown",
"metadata": {},
"source": [line if line.endswith("\n") else f"{line}\n" for line in text.splitlines()],
}
def write_notebook(path: Path, cells: list[dict]) -> None:
for index, cell in enumerate(cells):
cell["id"] = cell_id(index, cell)
data = {
"cells": cells,
"metadata": {
"kernelspec": {
"display_name": "QuantumLearning (.venv)",
"language": "python",
"name": "python3",
},
"language_info": {
"name": "python",
"version": "3.12",
},
},
"nbformat": 4,
"nbformat_minor": 5,
}
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n")
def load_notebook(path: Path) -> dict:
return json.loads(path.read_text())
def save_notebook(path: Path, notebook: dict) -> None:
for index, cell in enumerate(notebook.get("cells", [])):
cell["id"] = cell_id(index, cell)
path.write_text(json.dumps(notebook, indent=2, ensure_ascii=False) + "\n")
def relative_link(source: Path, target: Path) -> str:
return str(target.relative_to(source.parent, walk_up=True))
def replace_text(path: Path, replacements: list[tuple[str, str]]) -> None:
notebook = load_notebook(path)
changed = False
for cell in notebook.get("cells", []):
text = "".join(cell.get("source", []))
original = text
for old, new in replacements:
text = text.replace(old, new)
if text != original:
cell["source"] = [line if line.endswith("\n") else f"{line}\n" for line in text.splitlines()]
changed = True
if changed:
save_notebook(path, notebook)
def insert_reference_note(path: Path) -> None:
notebook = load_notebook(path)
note = markdown_cell(
"## Optional Deep-Dive Reference\n\n"
"This notebook is not part of the mandatory first-pass mainline. "
"Use it when you want the full backward-designed mastery map in more depth. "
"For the mainline flow, return to `../COURSE_BLUEPRINT.ipynb` and follow the next-notebook handoff there."
)
if any(NAV_TOP_MARKER in "".join(cell.get("source", [])) for cell in notebook["cells"]):
pass
existing = "".join(notebook["cells"][1].get("source", [])) if len(notebook["cells"]) > 1 else ""
if "Optional Deep-Dive Reference" not in existing:
notebook["cells"].insert(1, note)
save_notebook(path, notebook)
def build_course_complete() -> None:
path = completion_notebook_path()
cells = [
markdown_cell(
"# Course Complete\n\n"
"You have reached the end of the enforced mainline course path. "
"That does not mean circuit design is 'done.' It means you now have a coherent end-to-end foundation: "
"principles, engineering workflow, algorithmic patterns, and professional review habits."
),
markdown_cell(
"## What You Should Be Able To Do Now\n\n"
"- build and explain gate-model circuits from first principles\n"
"- move between code, circuit drawings, state intuition, counts, transpiled structure, and noisy behavior\n"
"- compare candidate circuit designs under explicit constraints\n"
"- write review-style justifications instead of vague impressions\n"
"- diagnose whether a mismatch is conceptual, structural, transpilation-driven, or noise-driven"
),
markdown_cell(
"## What To Do Next\n\n"
"1. Revisit the hardest studio notebooks and tighten your written design judgements.\n"
"2. Use `notebooks/reference/PROFESSIONAL_PATH.ipynb` if you want the long-range mastery map again.\n"
"3. Extend the capstone with your own circuit family and defend the design choices in writing.\n"
"4. Only after that, add external hardware targets or new research notebooks."
),
markdown_cell(
"## Final Rule\n\n"
"Do not measure progress by how many notebooks you opened. "
"Measure it by whether you can predict, explain, edit, compare, and defend circuit behavior without hand-waving."
),
]
write_notebook(path, cells)
def move_optional_and_legacy_notebooks() -> None:
REFERENCE_DIR.mkdir(parents=True, exist_ok=True)
LEGACY_DIR.mkdir(parents=True, exist_ok=True)
root_professional_path = NOTEBOOKS / "PROFESSIONAL_PATH.ipynb"
if root_professional_path.exists():
target = reference_notebook_path()
target.parent.mkdir(parents=True, exist_ok=True)
root_professional_path.replace(target)
for notebook_name in LEGACY_SINGLE_NOTEBOOKS:
source = NOTEBOOKS / notebook_name
if source.exists():
source.replace(LEGACY_DIR / notebook_name)
def ensure_navigation() -> None:
steps = canonical_course_steps()
total_steps = len(steps)
for index, step in enumerate(steps):
path = ROOT / step.path
notebook = load_notebook(path)
previous_step = steps[index - 1] if index > 0 else None
next_step = steps[index + 1] if index + 1 < total_steps else None
prev_line = (
f"Previous notebook: [{previous_step.title}]({relative_link(path, previous_step.absolute_path)})"
if previous_step is not None
else "Previous notebook: none. This is the start of the mainline course."
)
next_line = (
f"Next notebook: [{next_step.title}]({relative_link(path, next_step.absolute_path)})"
if next_step is not None
else "Next notebook: none. This notebook closes the mainline course."
)
top_text = (
f"{NAV_TOP_MARKER}\n"
"## Mainline Navigation\n\n"
f"Step {index + 1} of {total_steps}. Follow the mainline in order and do not skip ahead.\n\n"
f"{prev_line}\n\n"
f"{next_line}\n\n"
"Rule: finish this notebook top-to-bottom before you open the next one."
)
bottom_text = (
f"{NAV_BOTTOM_MARKER}\n"
"## What To Open Next\n\n"
f"{next_line}\n\n"
"If this notebook still feels unstable, repeat it before you move on. "
"The mainline only works if each handoff is earned."
)
top_cell = markdown_cell(top_text)
bottom_cell = markdown_cell(bottom_text)
top_index = next(
(
cell_index
for cell_index, cell in enumerate(notebook["cells"])
if cell.get("cell_type") == "markdown"
and NAV_TOP_MARKER in "".join(cell.get("source", []))
),
None,
)
if top_index is None:
notebook["cells"].insert(1 if notebook["cells"] else 0, top_cell)
else:
notebook["cells"][top_index] = top_cell
bottom_index = next(
(
cell_index
for cell_index, cell in enumerate(notebook["cells"])
if cell.get("cell_type") == "markdown"
and NAV_BOTTOM_MARKER in "".join(cell.get("source", []))
),
None,
)
if bottom_index is None:
notebook["cells"].append(bottom_cell)
else:
notebook["cells"][bottom_index] = bottom_cell
save_notebook(path, notebook)
def main() -> None:
move_optional_and_legacy_notebooks()
build_course_complete()
replace_text(
entry_notebook_path(),
[
(
"Read the rest of this notebook, then open `PROFESSIONAL_PATH.ipynb`. That notebook explains the backward-designed apprenticeship model behind the entire course. After that, return and begin the technical sequence at `00_circuit_literacy.ipynb`.\n\nThe reason for this order is simple. If you do not know the target profession, the beginner material feels either trivial or arbitrary. Once you know the target profession, the same material reads as deliberate foundation-building.",
"Read the rest of this notebook, then open `COURSE_BLUEPRINT.ipynb`. That is the first serious orientation notebook in the enforced mainline path. After that, follow the notebook-to-notebook handoff from inside the notebooks themselves instead of guessing from the filesystem.\n\nIf you later want the deeper mastery-map background, use `reference/PROFESSIONAL_PATH.ipynb` as an optional side reference. It is not the next required notebook in the first-pass learner journey.",
),
(
"After this notebook, open `PROFESSIONAL_PATH.ipynb`. Then move into `00_circuit_literacy.ipynb`.",
"After this notebook, open `COURSE_BLUEPRINT.ipynb`. Then begin `foundations/module_01_principles_and_circuit_literacy/lecture.ipynb`. If you want the deeper mastery map later, use `reference/PROFESSIONAL_PATH.ipynb` as an optional side reference.",
),
(
"Open `PROFESSIONAL_PATH.ipynb` next. Then begin the technical sequence.",
"Open `COURSE_BLUEPRINT.ipynb` next. Then follow the mainline handoff from notebook to notebook. `reference/PROFESSIONAL_PATH.ipynb` is optional and not required for the first pass.",
),
(
"'What should you open immediately after this notebook?', 'options': ['The capstone review notebook', 'The README again', 'PROFESSIONAL_PATH.ipynb'], 'correct_index': 2, 'explanation': 'The professional path notebook gives the didactical context for the whole curriculum.'",
"'What should you open immediately after this notebook?', 'options': ['The capstone review notebook', 'The README again', 'COURSE_BLUEPRINT.ipynb'], 'correct_index': 2, 'explanation': 'Course Blueprint is the next required notebook in the guarded mainline path.'",
),
],
)
replace_text(
entry_notebook_path(),
[
(
"# Start Here\n\nThis notebook is the front door of the course.",
"# Start Here\n\nThis notebook is the only supported starting point for the course. Do not guess where to begin from the filesystem. Start here, run the cells in order, and follow the next-notebook handoff at the end.",
)
],
)
replace_text(
NOTEBOOKS / "COURSE_BLUEPRINT.ipynb",
[
(
"This notebook is the new front door of the platform.",
"This notebook is the first serious orientation notebook after `START_HERE.ipynb`.",
),
(
"5. Continue with `qiskit_engineering`, then `algorithms`, then `professional` using the same bundle rhythm",
"5. Continue with `qiskit_engineering`, then `algorithms`, then `professional` using the same bundle rhythm\n6. Use `reference/PROFESSIONAL_PATH.ipynb` only if you want the optional deep mastery map",
),
],
)
insert_reference_note(reference_notebook_path())
ensure_navigation()
if __name__ == "__main__":
main()