| """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: str |
| mission_id: str |
| subject: str |
| title: str |
| objective: str |
| prerequisites: tuple[str, ...] |
| exam_importance: float |
| typical_misconceptions: tuple[str, ...] |
| practice_types: tuple[str, ...] |
| estimated_minutes: int |
| resources: ConceptResources = field(default_factory=ConceptResources) |
| availability: str = "curated" |
| 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: |
| 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, |
| } |
|
|