mirror of
https://github.com/saymrwulf/QuantumLearning.git
synced 2026-05-14 20:58:00 +00:00
781 lines
33 KiB
Python
781 lines
33 KiB
Python
#!/usr/bin/env python3
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
from quantum_learning import (
|
|
canonical_course_steps,
|
|
completion_notebook_path,
|
|
entry_notebook_path,
|
|
legacy_archive_dir,
|
|
project_root,
|
|
)
|
|
from quantum_learning.course_flow import LEGACY_SINGLE_NOTEBOOKS
|
|
|
|
|
|
ROOT = project_root()
|
|
NOTEBOOKS = ROOT / "notebooks"
|
|
LEGACY_DIR = legacy_archive_dir()
|
|
NAV_TOP_MARKER = "<!-- COURSE_NAV_TOP -->"
|
|
NAV_BOTTOM_MARKER = "<!-- COURSE_NAV_BOTTOM -->"
|
|
BADGE_MARKER = "<!-- QL_BADGE -->"
|
|
OPTIONAL_ZONE_MARKER = "<!-- QL_OPTIONAL_ZONE -->"
|
|
|
|
META_NOTEBOOKS = {"START_HERE.ipynb", "COURSE_BLUEPRINT.ipynb", "COURSE_COMPLETE.ipynb"}
|
|
INTRO_META_MARKDOWN_COUNTS = {
|
|
"lecture": 3,
|
|
"lab": 5,
|
|
"problems": 6,
|
|
"studio": 6,
|
|
}
|
|
|
|
PALETTES = {
|
|
"meta": {
|
|
"background": "#dbeafe",
|
|
"border": "#2563eb",
|
|
"text": "#1e3a8a",
|
|
},
|
|
"mandatory": {
|
|
"background": "#dcfce7",
|
|
"border": "#15803d",
|
|
"text": "#14532d",
|
|
},
|
|
"facultative": {
|
|
"background": "#ffedd5",
|
|
"border": "#ea580c",
|
|
"text": "#9a3412",
|
|
},
|
|
"setup": {
|
|
"background": "#e5e7eb",
|
|
"border": "#6b7280",
|
|
"text": "#111827",
|
|
},
|
|
}
|
|
|
|
WRAPPER_SYMBOLS = (
|
|
"quiz_block(",
|
|
"reflection_box(",
|
|
"editable_circuit_lab(",
|
|
"step_reference_table(",
|
|
"rubric_scorecard(",
|
|
"feedback_iteration_panel(",
|
|
"evidence_checklist(",
|
|
)
|
|
|
|
|
|
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, *, metadata: dict | None = None) -> dict:
|
|
return {
|
|
"cell_type": "markdown",
|
|
"metadata": metadata or {},
|
|
"source": [line if line.endswith("\n") else f"{line}\n" for line in text.splitlines()],
|
|
}
|
|
|
|
|
|
def code_cell(source: str, *, metadata: dict | None = None) -> dict:
|
|
return {
|
|
"cell_type": "code",
|
|
"execution_count": None,
|
|
"metadata": metadata or {},
|
|
"outputs": [],
|
|
"source": [line if line.endswith("\n") else f"{line}\n" for line in source.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 cell_text(cell: dict) -> str:
|
|
return "".join(cell.get("source", []))
|
|
|
|
|
|
def set_cell_text(cell: dict, text: str) -> None:
|
|
cell["source"] = [line if line.endswith("\n") else f"{line}\n" for line in text.splitlines()]
|
|
|
|
|
|
def replace_text(path: Path, replacements: list[tuple[str, str]]) -> None:
|
|
notebook = load_notebook(path)
|
|
changed = False
|
|
for cell in notebook.get("cells", []):
|
|
text = cell_text(cell)
|
|
original = text
|
|
for old, new in replacements:
|
|
text = text.replace(old, new)
|
|
if text != original:
|
|
set_cell_text(cell, text)
|
|
changed = True
|
|
if changed:
|
|
save_notebook(path, notebook)
|
|
|
|
|
|
def should_hide_code_input(text: str) -> bool:
|
|
return any(symbol in text for symbol in WRAPPER_SYMBOLS)
|
|
|
|
|
|
def hide_code_input(cell: dict) -> None:
|
|
metadata = cell.setdefault("metadata", {})
|
|
metadata.setdefault("jupyter", {})
|
|
metadata["jupyter"]["source_hidden"] = True
|
|
tags = metadata.setdefault("tags", [])
|
|
if "hide-input" not in tags:
|
|
tags.append("hide-input")
|
|
|
|
|
|
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. Extend the capstone with your own circuit family and defend the design choices in writing.\n"
|
|
"3. Repeat a full module bundle deliberately only when you know exactly which skill you are repairing.\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 archive_legacy_notebooks() -> None:
|
|
LEGACY_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
obsolete_professional_paths = [
|
|
NOTEBOOKS / "PROFESSIONAL_PATH.ipynb",
|
|
NOTEBOOKS / "reference" / "PROFESSIONAL_PATH.ipynb",
|
|
NOTEBOOKS / ".reference" / "PROFESSIONAL_PATH.ipynb",
|
|
]
|
|
for source in obsolete_professional_paths:
|
|
if source.exists():
|
|
target = LEGACY_DIR / "PROFESSIONAL_PATH.ipynb"
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
if target.exists():
|
|
source.unlink()
|
|
else:
|
|
source.replace(target)
|
|
|
|
for obsolete_dir in (NOTEBOOKS / "reference", NOTEBOOKS / ".reference"):
|
|
if obsolete_dir.exists():
|
|
shutil.rmtree(obsolete_dir, ignore_errors=True)
|
|
|
|
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
|
|
kind = notebook_kind(path)
|
|
|
|
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 official walkthrough."
|
|
)
|
|
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 official walkthrough."
|
|
)
|
|
top_text = (
|
|
f"{NAV_TOP_MARKER}\n"
|
|
"## Mainline Navigation\n\n"
|
|
f"Step {index + 1} of {total_steps}. Follow the official walkthrough in order.\n\n"
|
|
f"{prev_line}\n\n"
|
|
f"{next_line}\n\n"
|
|
"Rule: complete the mandatory cells in this notebook before you open the next one."
|
|
)
|
|
if next_step is None:
|
|
bottom_text = (
|
|
f"{NAV_BOTTOM_MARKER}\n"
|
|
"## What To Open Next\n\n"
|
|
f"{next_line}\n\n"
|
|
"This is the end of the mandatory walkthrough."
|
|
)
|
|
elif kind == "meta":
|
|
bottom_text = (
|
|
f"{NAV_BOTTOM_MARKER}\n"
|
|
"## What To Open Next\n\n"
|
|
f"{next_line}\n\n"
|
|
"Official walkthrough rule: after you finish the cells above, open the next notebook. "
|
|
"There is no facultative material in this notebook."
|
|
)
|
|
else:
|
|
bottom_text = (
|
|
f"{NAV_BOTTOM_MARKER}\n"
|
|
"## What To Open Next\n\n"
|
|
f"{next_line}\n\n"
|
|
"Official walkthrough rule: once every mandatory cell above is complete, open the next notebook. "
|
|
"Anything below this cell is facultative."
|
|
)
|
|
|
|
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 cell_text(cell)
|
|
),
|
|
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 cell_text(cell)
|
|
),
|
|
None,
|
|
)
|
|
if bottom_index is None:
|
|
notebook["cells"].append(bottom_cell)
|
|
else:
|
|
notebook["cells"][bottom_index] = bottom_cell
|
|
|
|
save_notebook(path, notebook)
|
|
|
|
|
|
def notebook_kind(path: Path) -> str:
|
|
if path.name in META_NOTEBOOKS:
|
|
return "meta"
|
|
stem = path.stem
|
|
if stem in {"lecture", "lab", "problems", "studio"}:
|
|
return stem
|
|
return "other"
|
|
|
|
|
|
def strip_badge_prefix(text: str) -> str:
|
|
if not text.startswith(BADGE_MARKER):
|
|
return text
|
|
closing = text.find("</div>")
|
|
if closing == -1:
|
|
return text
|
|
return text[closing + len("</div>") :].lstrip("\n")
|
|
|
|
|
|
def strip_injected_artifacts(notebook: dict) -> None:
|
|
cleaned_cells = []
|
|
for cell in notebook.get("cells", []):
|
|
metadata = cell.get("metadata", {})
|
|
if metadata.get("ql_injected") in {"badge", "facultative", "facultative_zone"}:
|
|
continue
|
|
if cell.get("cell_type") == "markdown":
|
|
text = strip_badge_prefix(cell_text(cell))
|
|
set_cell_text(cell, text)
|
|
cleaned_cells.append(cell)
|
|
notebook["cells"] = cleaned_cells
|
|
|
|
|
|
def label_palette(track: str, role: str) -> dict[str, str]:
|
|
if role == "setup":
|
|
return PALETTES["setup"]
|
|
return PALETTES[track]
|
|
|
|
|
|
def render_badge(track: str, role: str, difficulty: int, note: str) -> str:
|
|
palette = label_palette(track, role)
|
|
label = f"{track.upper()} {role.upper()}"
|
|
return (
|
|
f"{BADGE_MARKER}\n"
|
|
f"<div style=\"padding:0.55rem 0.8rem; border-left:6px solid {palette['border']}; "
|
|
f"background:{palette['background']}; color:{palette['text']}; border-radius:0.35rem; "
|
|
f"font-family:Helvetica, Arial, sans-serif; margin:0.15rem 0 0.85rem 0;\">\n"
|
|
f"<strong>{label}</strong> · Difficulty {difficulty}/10 · {note}\n"
|
|
"</div>"
|
|
)
|
|
|
|
|
|
def badge_markdown_cell(track: str, role: str, difficulty: int, note: str) -> dict:
|
|
return markdown_cell(
|
|
render_badge(track, role, difficulty, note),
|
|
metadata={"ql_injected": "badge", "ql_track": track, "ql_role": role},
|
|
)
|
|
|
|
|
|
def badge_markdown_content(cell: dict, track: str, role: str, difficulty: int, note: str) -> dict:
|
|
text = strip_badge_prefix(cell_text(cell))
|
|
set_cell_text(cell, f"{render_badge(track, role, difficulty, note)}\n\n{text}")
|
|
return cell
|
|
|
|
|
|
def supports_symbol(notebook: dict, symbol: str) -> bool:
|
|
return any(
|
|
cell.get("cell_type") == "code" and symbol in cell_text(cell) for cell in notebook.get("cells", [])
|
|
)
|
|
|
|
|
|
def extract_title(notebook: dict, fallback: str) -> str:
|
|
for cell in notebook.get("cells", []):
|
|
if cell.get("cell_type") != "markdown":
|
|
continue
|
|
for line in cell_text(cell).splitlines():
|
|
if line.startswith("# "):
|
|
return line[2:].strip()
|
|
return fallback
|
|
|
|
|
|
def facultative_difficulty(kind: str, role: str) -> int:
|
|
base = {
|
|
"meta": {"reading": 4, "test": 4, "exercise": 4},
|
|
"lecture": {"reading": 4, "test": 5, "exercise": 6},
|
|
"lab": {"reading": 4, "test": 5, "exercise": 6},
|
|
"problems": {"reading": 4, "test": 5, "exercise": 6},
|
|
"studio": {"reading": 5, "test": 6, "exercise": 7},
|
|
"other": {"reading": 4, "test": 5, "exercise": 6},
|
|
}
|
|
return base.get(kind, base["other"])[role]
|
|
|
|
|
|
def facultative_reading_text(kind: str, title: str) -> str:
|
|
if kind == "meta":
|
|
return (
|
|
f"## Facultative Extension Reading\n\n"
|
|
f"You have already finished the mandatory route for **{title}**. Use this optional cell only if you want to tighten your study method. "
|
|
"Write down which mandatory cell types help you most, which ones slow you down, and how you will keep the one official walkthrough intact without guessing from the filesystem."
|
|
)
|
|
if kind == "lecture":
|
|
return (
|
|
f"## Facultative Extension Reading\n\n"
|
|
f"Treat **{title}** as a transfer checkpoint. Ask which mechanism from the mandatory cells would still matter if one qubit, one basis choice, or one comparison target changed. "
|
|
"This optional reading cell exists to stretch the idea without changing the official route."
|
|
)
|
|
if kind == "lab":
|
|
return (
|
|
f"## Facultative Extension Reading\n\n"
|
|
f"In **{title}**, the mandatory labs already gave you the official practice loop. This optional cell is for deeper experimentation discipline: decide one variable you would perturb next, one quantity you would track, and one false conclusion you want to avoid."
|
|
)
|
|
if kind == "problems":
|
|
return (
|
|
f"## Facultative Extension Reading\n\n"
|
|
f"Use this optional problems extension only if you want stronger diagnostic habits. In **{title}**, the useful move is not just to know the right answer; it is to record which wrong answer tempted you and what that says about your current mental model."
|
|
)
|
|
return (
|
|
f"## Facultative Extension Reading\n\n"
|
|
f"Use this optional studio extension only if the mandatory route in **{title}** feels stable. The right stretch here is not random complexity; it is one extra candidate, one sharper criterion, and one clearer written defence of the final decision."
|
|
)
|
|
|
|
|
|
def facultative_quiz_source(kind: str, title: str) -> str:
|
|
prompts = {
|
|
"meta": [
|
|
{
|
|
"prompt": "What is the only supported walkthrough rule?",
|
|
"options": [
|
|
"Complete the mandatory cells in order and use facultative cells only as optional extensions",
|
|
"Mix mandatory and facultative cells in any order as long as every notebook opens once",
|
|
"Start from whichever folder looks most interesting",
|
|
],
|
|
"correct_index": 0,
|
|
"explanation": "The route is single-path. Facultative cells never replace the mandatory path.",
|
|
},
|
|
{
|
|
"prompt": "Why are meta cells colored differently?",
|
|
"options": [
|
|
"To separate route and motivation guidance from the actual technical payload",
|
|
"To mark the easiest cells only",
|
|
"To hide the course contract from advanced users",
|
|
],
|
|
"correct_index": 0,
|
|
"explanation": "The meta layer should never masquerade as technical content.",
|
|
},
|
|
],
|
|
"lecture": [
|
|
{
|
|
"prompt": f"What should stay stable if you transfer the main mechanism from {title} into a new small circuit?",
|
|
"options": [
|
|
"The causal idea being tested",
|
|
"The exact notebook filename",
|
|
"The original counts histogram regardless of the change",
|
|
],
|
|
"correct_index": 0,
|
|
"explanation": "Transfer begins by preserving the mechanism, not the surface form.",
|
|
},
|
|
{
|
|
"prompt": "What is the point of a facultative lecture extension?",
|
|
"options": [
|
|
"To deepen transfer after the mandatory path is already complete",
|
|
"To replace the mandatory cells",
|
|
"To skip the lab and studio",
|
|
],
|
|
"correct_index": 0,
|
|
"explanation": "Facultative cells extend; they do not redefine the route.",
|
|
},
|
|
],
|
|
"lab": [
|
|
{
|
|
"prompt": "In an optional lab variant, what should you change first?",
|
|
"options": [
|
|
"One design variable you can explain",
|
|
"As many gates as possible to make the result surprising",
|
|
"The notebook order itself",
|
|
],
|
|
"correct_index": 0,
|
|
"explanation": "Optional exploration is still strongest when the perturbation is controlled.",
|
|
},
|
|
{
|
|
"prompt": "What makes an optional lab note useful?",
|
|
"options": [
|
|
"It records what changed, what stayed fixed, and what evidence moved",
|
|
"It only reports that the circuit still ran",
|
|
"It avoids writing predictions to save time",
|
|
],
|
|
"correct_index": 0,
|
|
"explanation": "The extension is about deeper evidence discipline.",
|
|
},
|
|
],
|
|
"problems": [
|
|
{
|
|
"prompt": "Why revisit a missed question in a facultative problems block?",
|
|
"options": [
|
|
"To name the specific misconception that produced the miss",
|
|
"To memorize the right option letter only",
|
|
"To skip written explanation entirely",
|
|
],
|
|
"correct_index": 0,
|
|
"explanation": "The value is diagnostic precision, not rote recovery.",
|
|
},
|
|
{
|
|
"prompt": "What should an optional error log contain?",
|
|
"options": [
|
|
"The tempting wrong model and the corrected model",
|
|
"Only the final score",
|
|
"Only the notebook path",
|
|
],
|
|
"correct_index": 0,
|
|
"explanation": "The error log is useful when it captures the broken idea, not just the result.",
|
|
},
|
|
],
|
|
"studio": [
|
|
{
|
|
"prompt": "What makes a facultative studio stretch worthwhile?",
|
|
"options": [
|
|
"It sharpens a design judgement without breaking the single mandatory route",
|
|
"It replaces the mandatory brief",
|
|
"It adds complexity with no explicit criterion",
|
|
],
|
|
"correct_index": 0,
|
|
"explanation": "A studio stretch should sharpen judgement, not blur the route.",
|
|
},
|
|
{
|
|
"prompt": "If a stretch candidate fails, what should change first?",
|
|
"options": [
|
|
"The written explanation of the failure and the next criterion",
|
|
"The notebook order",
|
|
"The insistence that the failed candidate was still best",
|
|
],
|
|
"correct_index": 0,
|
|
"explanation": "The useful move is clearer review language and a better criterion.",
|
|
},
|
|
],
|
|
}
|
|
questions = prompts.get(kind, prompts["lecture"])
|
|
return f"quiz_block({questions!r}, heading='Facultative Extension Test')"
|
|
|
|
|
|
def facultative_reflection_source(kind: str, title: str) -> str:
|
|
prompts = {
|
|
"meta": (
|
|
f"Write one sentence that explains how you will follow the mandatory route in {title} without losing the distinction between meta cells and technical cells."
|
|
),
|
|
"lecture": (
|
|
f"State one mechanism from {title} that you could now transfer to a slightly different circuit, and name the first thing you would hold fixed while testing that transfer."
|
|
),
|
|
"lab": (
|
|
f"Describe one optional circuit variation you would run after {title}, what you would predict before running it, and what evidence would make you abandon the prediction."
|
|
),
|
|
"problems": (
|
|
f"Name one tempting wrong answer pattern from {title} and write the shortest correction that would stop you from repeating it."
|
|
),
|
|
"studio": (
|
|
f"Write one optional stretch brief for {title}: objective, criterion, and the single risk that would matter most in your design review."
|
|
),
|
|
}
|
|
prompt = prompts.get(kind, prompts["lecture"])
|
|
return f"reflection_box({prompt!r})"
|
|
|
|
|
|
def build_facultative_extension_cells(path: Path, notebook: dict) -> list[dict]:
|
|
kind = notebook_kind(path)
|
|
if kind == "meta" or path.name == "COURSE_COMPLETE.ipynb":
|
|
return []
|
|
|
|
title = extract_title(notebook, path.stem.replace("_", " ").title())
|
|
zone_cells = [
|
|
markdown_cell(
|
|
f"{OPTIONAL_ZONE_MARKER}\n"
|
|
"## Facultative Extension Zone\n\n"
|
|
f"You have already completed the mandatory walkthrough for **{title}**. Everything below is optional. "
|
|
"Use it only if you want deeper consolidation or extra transfer work.",
|
|
metadata={
|
|
"ql_injected": "facultative_zone",
|
|
"ql_track": "meta",
|
|
"ql_role": "reading",
|
|
"ql_difficulty": 1,
|
|
"ql_note": "Optional-zone boundary. The official walkthrough is already complete above.",
|
|
},
|
|
),
|
|
markdown_cell(
|
|
facultative_reading_text(kind, title),
|
|
metadata={
|
|
"ql_injected": "facultative",
|
|
"ql_track": "facultative",
|
|
"ql_role": "reading",
|
|
"ql_difficulty": facultative_difficulty(kind, "reading"),
|
|
"ql_note": "Optional extension reading.",
|
|
},
|
|
),
|
|
]
|
|
|
|
if supports_symbol(notebook, "quiz_block("):
|
|
zone_cells.append(
|
|
code_cell(
|
|
facultative_quiz_source(kind, title),
|
|
metadata={
|
|
"ql_injected": "facultative",
|
|
"ql_track": "facultative",
|
|
"ql_role": "test",
|
|
"ql_difficulty": facultative_difficulty(kind, "test"),
|
|
"ql_note": "Optional multiple-choice extension.",
|
|
},
|
|
)
|
|
)
|
|
|
|
if supports_symbol(notebook, "reflection_box("):
|
|
zone_cells.append(
|
|
code_cell(
|
|
facultative_reflection_source(kind, title),
|
|
metadata={
|
|
"ql_injected": "facultative",
|
|
"ql_track": "facultative",
|
|
"ql_role": "exercise",
|
|
"ql_difficulty": facultative_difficulty(kind, "exercise"),
|
|
"ql_note": "Optional written exercise.",
|
|
},
|
|
)
|
|
)
|
|
|
|
return zone_cells
|
|
|
|
|
|
def classify_cell(path: Path, cell: dict, markdown_index: int) -> tuple[str, str, int, str]:
|
|
metadata = cell.get("metadata", {})
|
|
if metadata.get("ql_track"):
|
|
return (
|
|
metadata["ql_track"],
|
|
metadata["ql_role"],
|
|
int(metadata["ql_difficulty"]),
|
|
metadata["ql_note"],
|
|
)
|
|
|
|
kind = notebook_kind(path)
|
|
text = cell_text(cell)
|
|
|
|
if kind == "meta":
|
|
if cell.get("cell_type") == "markdown":
|
|
return ("meta", "reading", 1, "Course route, objective, pacing, or motivation.")
|
|
if "quiz_block(" in text:
|
|
return ("meta", "test", 1, "Meta-level comprehension check.")
|
|
return ("meta", "exercise", 1, "Meta-level setup or reflection cell.")
|
|
|
|
if cell.get("cell_type") == "markdown":
|
|
if NAV_TOP_MARKER in text or NAV_BOTTOM_MARKER in text:
|
|
return ("meta", "reading", 1, "Official walkthrough guardrail.")
|
|
if markdown_index < INTRO_META_MARKDOWN_COUNTS.get(kind, 2):
|
|
return ("meta", "reading", 1, "Notebook-level rule, objective, or usage guidance.")
|
|
difficulty = 2 if kind in {"lecture", "lab", "problems"} else 3
|
|
return ("mandatory", "reading", difficulty, "Official walkthrough reading cell.")
|
|
|
|
if "project_root" in text or "src_path" in text or "load_curriculum" in text:
|
|
return ("mandatory", "setup", 1, "Environment, import, or helper cell required by the notebook.")
|
|
if "quiz_block(" in text:
|
|
difficulty = 2 if kind in {"lecture", "lab"} else 3
|
|
return ("mandatory", "test", difficulty, "Official walkthrough multiple-choice test.")
|
|
difficulty = 2 if kind in {"lecture", "lab"} else 3
|
|
return ("mandatory", "exercise", difficulty, "Official walkthrough runnable or written exercise.")
|
|
|
|
|
|
def annotate_notebook(path: Path) -> None:
|
|
notebook = load_notebook(path)
|
|
strip_injected_artifacts(notebook)
|
|
|
|
bottom_index = next(
|
|
(
|
|
index
|
|
for index, cell in enumerate(notebook["cells"])
|
|
if cell.get("cell_type") == "markdown" and NAV_BOTTOM_MARKER in cell_text(cell)
|
|
),
|
|
None,
|
|
)
|
|
if bottom_index is not None:
|
|
extension_cells = build_facultative_extension_cells(path, notebook)
|
|
notebook["cells"][bottom_index + 1 : bottom_index + 1] = extension_cells
|
|
|
|
annotated_cells: list[dict] = []
|
|
markdown_index = 0
|
|
for cell in notebook["cells"]:
|
|
track, role, difficulty, note = classify_cell(path, cell, markdown_index)
|
|
if cell.get("cell_type") == "markdown":
|
|
annotated_cells.append(badge_markdown_content(cell, track, role, difficulty, note))
|
|
markdown_index += 1
|
|
else:
|
|
if should_hide_code_input(cell_text(cell)):
|
|
hide_code_input(cell)
|
|
annotated_cells.append(badge_markdown_cell(track, role, difficulty, note))
|
|
annotated_cells.append(cell)
|
|
|
|
notebook["cells"] = annotated_cells
|
|
save_notebook(path, notebook)
|
|
|
|
|
|
def main() -> None:
|
|
archive_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 official walkthrough. After that, follow the notebook-to-notebook handoff from inside the notebooks themselves instead of guessing from the filesystem.",
|
|
),
|
|
(
|
|
"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`.",
|
|
),
|
|
(
|
|
"Open `PROFESSIONAL_PATH.ipynb` next. Then begin the technical sequence.",
|
|
"Open `COURSE_BLUEPRINT.ipynb` next. Then follow the official walkthrough from notebook to notebook.",
|
|
),
|
|
(
|
|
"'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 official walkthrough.'",
|
|
),
|
|
],
|
|
)
|
|
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(
|
|
entry_notebook_path(),
|
|
[
|
|
(
|
|
"## Final Instruction\n\nOpen `COURSE_BLUEPRINT.ipynb` next. Then follow the mainline handoff from notebook to notebook. Do not branch away from that route on the first run. The notebooks are now written to be read in detail, quizzed actively, and modified interactively. That is the intended workflow.",
|
|
"## Final Instruction\n\nOpen `COURSE_BLUEPRINT.ipynb` next. Then complete the mandatory cells in each notebook in order. Facultative cells are optional extensions, not part of the official walkthrough.",
|
|
)
|
|
],
|
|
)
|
|
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`.",
|
|
),
|
|
(
|
|
"7. Only then return to the later single-notebook materials as transition content",
|
|
"7. Continue following the explicit next-notebook handoff until `COURSE_COMPLETE.ipynb`",
|
|
),
|
|
(
|
|
"That is the new mainline.",
|
|
"That is the one official walkthrough.",
|
|
),
|
|
],
|
|
)
|
|
replace_text(
|
|
NOTEBOOKS / "COURSE_BLUEPRINT.ipynb",
|
|
[
|
|
(
|
|
"## What To Open Next\n\nThe full technical mainline is now rebuilt. Start with **Principles and Circuit Literacy**, continue through the rest of the foundations modules, then move into **Circuit Construction and Analysis**, **Transpilation and Visualization**, and **Simulation and Noise Models**, then continue into **Deutsch Family and Oracle Thinking**, **Bernstein-Vazirani and Structured Oracles**, **QFT and Periodic Structure**, and **Grover and Amplitude Amplification**, and finally complete the professional band with **Qiskit Patterns and Workflow Design**, **Hardware-Aware Redesign Studio**, **Noise-Aware Verification and Mitigation**, and **Capstone Circuit Design Review**. Inside each module, read the lecture notebook first. Then move into the lab, then problems, then the studio. Keep that order.\n\nThe sequence matters. If you jump straight into the studio, you may produce activity without comprehension. If you stop after the lecture, you may feel recognition without control. The four-notebook loop is the point.",
|
|
"## What To Open Next\n\nThe full technical mainline is now rebuilt. Start with **Principles and Circuit Literacy**, continue through the rest of the foundations modules, then move into **Circuit Construction and Analysis**, **Transpilation and Visualization**, and **Simulation and Noise Models**, then continue into **Deutsch Family and Oracle Thinking**, **Bernstein-Vazirani and Structured Oracles**, **QFT and Periodic Structure**, and **Grover and Amplitude Amplification**, and finally complete the professional band with **Qiskit Patterns and Workflow Design**, **Hardware-Aware Redesign Studio**, **Noise-Aware Verification and Mitigation**, and **Capstone Circuit Design Review**. Inside each module, finish the mandatory lecture cells first. Then finish the mandatory lab cells, then the mandatory problems cells, then the mandatory studio cells. Facultative cells never replace that order.\n\nThe sequence matters. If you jump straight into the studio, you may produce activity without comprehension. If you stop after the lecture, you may feel recognition without control. The four-notebook loop is the point.",
|
|
)
|
|
],
|
|
)
|
|
ensure_navigation()
|
|
|
|
for step in canonical_course_steps():
|
|
annotate_notebook(ROOT / step.path)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|