File size: 11,492 Bytes
7c6ffa6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 | """Concept-level learning graph.
Data-driven registry of teachable concepts, keyed by the SAME ``topic_key``
convention the product already persists mastery under:
``"{chapter_catalog_id}:{mission_id}"`` (see ``topic_mastery.topic_key`` and
GuidedClass's assessment sync). For v1 each curated Sound Waves mission is one
concept node; the shape is chapter-agnostic so later chapters extend the data,
not the code.
Honesty rules encoded here:
- Only Sound Waves (``phy-p1-c1``) is a complete curated class; its nodes carry
real objectives/misconceptions lifted from the hand-authored, source-backed
lesson seeds (``src/lib/teaching/physicsLessonSeeds.ts``).
- ``pyq`` resources are ``False`` everywhere because no PYQ evidence is curated
yet (``pyqEvidenceStatus: "not_curated"`` in the seeds).
- Unknown chapters/missions simply are not in the graph — callers must treat
absence as "content unavailable", never invent coverage.
"""
from __future__ import annotations
from dataclasses import dataclass, field
SOUND_WAVES_CHAPTER_ID = "phy-p1-c1"
@dataclass(frozen=True)
class ConceptResources:
lesson: bool = False
quiz: bool = False
flashcards: bool = False
board_answer: bool = False
numericals: bool = False
pyq: bool = False
@dataclass(frozen=True)
class ConceptNode:
concept_key: str # "{chapter_catalog_id}:{mission_id}"
chapter_catalog_id: str
mission_id: str
subject: str
title: str
objective: str
prerequisites: tuple[str, ...] # concept_keys
exam_importance: float # 0..1, relative weight inside the chapter
typical_misconceptions: tuple[str, ...]
practice_types: tuple[str, ...]
estimated_minutes: int
resources: ConceptResources = field(default_factory=ConceptResources)
availability: str = "curated" # curated | notes_only | locked
textbook_reference: str = ""
def _sw(mission_id: str) -> str:
return f"{SOUND_WAVES_CHAPTER_ID}:{mission_id}"
_SW_REFERENCE = "Kerala SCERT Physics, Standard X (2025) — Sound Waves, pp. 7-26"
_SW_RESOURCES = ConceptResources(lesson=True, quiz=True, flashcards=True, board_answer=True, pyq=False)
_SW_NUMERIC_RESOURCES = ConceptResources(lesson=True, quiz=True, flashcards=True, board_answer=True, numericals=True, pyq=False)
SOUND_WAVES_CONCEPTS: tuple[ConceptNode, ...] = (
ConceptNode(
concept_key=_sw("M1"),
chapter_catalog_id=SOUND_WAVES_CHAPTER_ID,
mission_id="M1",
subject="Physics",
title="Oscillation, Amplitude, Period and Frequency",
objective="Understand the basic terms of oscillatory motion using a pendulum and swing.",
prerequisites=(),
exam_importance=0.8,
typical_misconceptions=(
"One oscillation is just going forward — it must return to complete one.",
),
practice_types=("recall", "mcq", "board_answer"),
estimated_minutes=25,
resources=_SW_RESOURCES,
textbook_reference=_SW_REFERENCE,
),
ConceptNode(
concept_key=_sw("M2"),
chapter_catalog_id=SOUND_WAVES_CHAPTER_ID,
mission_id="M2",
subject="Physics",
title="Natural Frequency, Forced Vibration & Resonance",
objective="Understand natural frequency and how forced vibrations lead to resonance.",
prerequisites=(_sw("M1"),),
exam_importance=0.7,
typical_misconceptions=(
"Not recognising that the table is forced to vibrate by the mixie (forced vibration).",
"Forgetting that resonance needs matching frequencies AND produces maximum amplitude.",
),
practice_types=("recall", "mcq", "board_answer"),
estimated_minutes=25,
resources=_SW_RESOURCES,
textbook_reference=_SW_REFERENCE,
),
ConceptNode(
concept_key=_sw("M3"),
chapter_catalog_id=SOUND_WAVES_CHAPTER_ID,
mission_id="M3",
subject="Physics",
title="Wave Motion & Types of Waves",
objective="Learn how waves transfer energy without transferring particles, and differentiate longitudinal and transverse waves.",
prerequisites=(_sw("M1"),),
exam_importance=0.85,
typical_misconceptions=(
"Thinking the material itself moves forward with the wave — only energy moves.",
),
practice_types=("recall", "mcq", "diagram", "board_answer"),
estimated_minutes=30,
resources=_SW_RESOURCES,
textbook_reference=_SW_REFERENCE,
),
ConceptNode(
concept_key=_sw("M4"),
chapter_catalog_id=SOUND_WAVES_CHAPTER_ID,
mission_id="M4",
subject="Physics",
title="Frequency, Wavelength and Wave Speed (v = fλ)",
objective="Define wavelength, speed of a wave, and understand the relationship v = fλ.",
prerequisites=(_sw("M3"),),
exam_importance=1.0,
typical_misconceptions=(
"Confusing wavelength with amplitude.",
),
practice_types=("recall", "mcq", "formula", "board_answer"),
estimated_minutes=30,
resources=_SW_NUMERIC_RESOURCES,
textbook_reference=_SW_REFERENCE,
),
ConceptNode(
concept_key=_sw("M5"),
chapter_catalog_id=SOUND_WAVES_CHAPTER_ID,
mission_id="M5",
subject="Physics",
title="Numericals using v = fλ",
objective="Solve exam-style numericals step-by-step using v = fλ.",
prerequisites=(_sw("M4"),),
exam_importance=1.0,
typical_misconceptions=(
"Substituting wavelength in centimetres without converting to metres.",
),
practice_types=("numerical", "board_answer"),
estimated_minutes=30,
resources=_SW_NUMERIC_RESOURCES,
textbook_reference=_SW_REFERENCE,
),
ConceptNode(
concept_key=_sw("M6"),
chapter_catalog_id=SOUND_WAVES_CHAPTER_ID,
mission_id="M6",
subject="Physics",
title="Reflection, Echo & Reverberation",
objective="Understand how sound reflects, the conditions for an echo, and reverberation.",
prerequisites=(_sw("M3"),),
exam_importance=0.9,
typical_misconceptions=(
"Thinking reflection doesn't happen in a small room — it does, we just cannot distinguish the echo.",
),
practice_types=("recall", "mcq", "numerical", "board_answer"),
estimated_minutes=30,
resources=_SW_NUMERIC_RESOURCES,
textbook_reference=_SW_REFERENCE,
),
ConceptNode(
concept_key=_sw("M7"),
chapter_catalog_id=SOUND_WAVES_CHAPTER_ID,
mission_id="M7",
subject="Physics",
title="Limits of Audibility & Ultrasonic Uses",
objective="Understand audible range, infrasonic and ultrasonic sounds, and their applications.",
prerequisites=(_sw("M4"),),
exam_importance=0.7,
typical_misconceptions=(
"Assuming humans can hear everything — dogs and bats hear higher frequencies than us.",
),
practice_types=("recall", "mcq", "board_answer"),
estimated_minutes=25,
resources=_SW_RESOURCES,
textbook_reference=_SW_REFERENCE,
),
ConceptNode(
concept_key=_sw("M8"),
chapter_catalog_id=SOUND_WAVES_CHAPTER_ID,
mission_id="M8",
subject="Physics",
title="Seismic Waves and Tsunami",
objective="Understand the destructive power of seismic waves and tsunamis.",
prerequisites=(_sw("M3"),),
exam_importance=0.6,
typical_misconceptions=(
"Waves are not just sound or light — they can carry massive destructive energy.",
),
practice_types=("recall", "mcq", "board_answer"),
estimated_minutes=25,
resources=_SW_RESOURCES,
textbook_reference=_SW_REFERENCE,
),
ConceptNode(
concept_key=_sw("M9"),
chapter_catalog_id=SOUND_WAVES_CHAPTER_ID,
mission_id="M9",
subject="Physics",
title="Sound Waves Chapter Test & Revision",
objective="Complete a source-backed final chapter test and turn remaining mistakes into targeted revision.",
prerequisites=tuple(_sw(f"M{index}") for index in range(1, 9)),
exam_importance=1.0,
typical_misconceptions=(
"Knowing isolated definitions without connecting the formula, condition and application.",
),
practice_types=("revision", "chapter_test", "numerical", "board_answer"),
estimated_minutes=40,
resources=_SW_NUMERIC_RESOURCES,
textbook_reference=_SW_REFERENCE,
),
)
_CONCEPTS_BY_KEY: dict[str, ConceptNode] = {node.concept_key: node for node in SOUND_WAVES_CONCEPTS}
_CONCEPTS_BY_CHAPTER: dict[str, tuple[ConceptNode, ...]] = {SOUND_WAVES_CHAPTER_ID: SOUND_WAVES_CONCEPTS}
def concept_for(topic_key: str) -> ConceptNode | None:
return _CONCEPTS_BY_KEY.get(topic_key)
def concepts_for_chapter(chapter_catalog_id: str) -> tuple[ConceptNode, ...]:
return _CONCEPTS_BY_CHAPTER.get(chapter_catalog_id, ())
def all_concepts() -> tuple[ConceptNode, ...]:
return SOUND_WAVES_CONCEPTS
def prerequisites_met(concept: ConceptNode, secure_or_developing_keys: set[str]) -> bool:
"""A concept may be scheduled as NEW learning only when every prerequisite
has at least been introduced/learned (present in the provided key set)."""
return all(prerequisite in secure_or_developing_keys for prerequisite in concept.prerequisites)
def ordered_chapter_concepts(chapter_catalog_id: str) -> list[ConceptNode]:
"""Concepts in a safe teaching order (prerequisites before dependants).
The seed data is already authored in teaching order (M1..M9); this sorts
defensively anyway so data edits cannot silently break ordering.
"""
nodes = list(concepts_for_chapter(chapter_catalog_id))
placed: list[ConceptNode] = []
placed_keys: set[str] = set()
remaining = nodes[:]
while remaining:
progressed = False
for node in list(remaining):
if all(prerequisite in placed_keys for prerequisite in node.prerequisites):
placed.append(node)
placed_keys.add(node.concept_key)
remaining.remove(node)
progressed = True
if not progressed: # cycle in data — fail safe by appending as-authored
placed.extend(remaining)
break
return placed
def concept_to_dict(node: ConceptNode) -> dict[str, object]:
return {
"concept_key": node.concept_key,
"chapter_catalog_id": node.chapter_catalog_id,
"mission_id": node.mission_id,
"subject": node.subject,
"title": node.title,
"objective": node.objective,
"prerequisites": list(node.prerequisites),
"exam_importance": node.exam_importance,
"typical_misconceptions": list(node.typical_misconceptions),
"practice_types": list(node.practice_types),
"estimated_minutes": node.estimated_minutes,
"resources": {
"lesson": node.resources.lesson,
"quiz": node.resources.quiz,
"flashcards": node.resources.flashcards,
"board_answer": node.resources.board_answer,
"numericals": node.resources.numericals,
"pyq": node.resources.pyq,
},
"availability": node.availability,
"textbook_reference": node.textbook_reference,
}
|