← domain #1

Source : alankara_varna.py

data/library/books/brihaddesi_sharma_1992/formal_grammar/alankara_varna.py · 1021 lines · 42772 bytes
"""Domain 1 — alaṅkāra / varṇa / svara ornamentation Synthèse 6c.3 du Brihaddesi (Sharma 1992, vols I & II) pour le domaine 1 depuis : - 161 règles génératives 6b - 321 affirmations sourcées - 132 concepts (domain_id=1) Anti-fabrication : chaque type, opération, contrainte et constante cite son evidence (rule_id 6b ou affirmation_id). Aucune valeur non sourcée n'est introduite. Concepts mentionnés sans rule_id pinnant un attribut → UNRESOLVED. Python 3.10+. Importable directement, sans dépendance externe. """ from __future__ import annotations from dataclasses import dataclass, field from enum import Enum from typing import Iterable # ============================================================================= # CONSTANTES — énumérations fermées du Brihaddesi # ============================================================================= # Types partagés avec melodic_derivations (domaine 3) — canonicalisés par 6c.4. try: from .melodic_derivations import Svara, SVARA_ORDER, Sthāna except ImportError: import os as _os, sys as _sys _sys.path.insert(0, _os.path.dirname(__file__)) from melodic_derivations import Svara, SVARA_ORDER, Sthāna # type: ignore # evidence: R_1697_vadi_mandala_seven_svaras (vādi-maṇḍala = [sa,ri,ga,ma,pa,dha,ni]) class Varṇa(str, Enum): """The four varṇas — patterns of melodic movement that underlie alaṅkāras. "Varṇas, that are verily four only — sthāyin (steady), sañcārin (circulatory), ārohin (ascending) and avarohin (descending)." evidence: aff#2529 (BD I.118), aff#2546 (jāti exemplars), aff#3111 (pada-rendering classification), varna_four_types, R_1835_001 (count==4) """ STHAYIN = "sthayin" # steady, evidence: BD_6_1_R001, aff#2537 SANCARIN = "sancarin" # circulatory, evidence: R_c89_sancarin, aff#2534 AROHIN = "arohin" # ascending, evidence: R_4p0_011, aff#2535 AVAROHIN = "avarohin" # descending, evidence: avarohin_definition, aff#2536 class SvaraRole(str, Enum): """The fourfold svara-classification of Brihaddesi. "svara is classified fourfold beginning with vādin" evidence: svara_definition_and_eternity (cluster 1325), vadi_mandala_seven_svaras """ VADIN = "vadin" # tonic / king. evidence: vadin_definition_speaker SAMVADIN = "samvadin" # consonant / minister. evidence: R_samvadin_def, R_1695_samvadin ANUVADIN = "anuvadin" # assonant / attendant. evidence: anuvadin_definition VIVADIN = "vivadin" # dissonant. evidence: vivadin_definition_and_exclusion, BD_6_1_R029 class Heptad(str, Enum): """The three vocal heptads (registers) — locus of certain alaṅkāras. Used by kampita (lower/chest), kuharita (middle/throat), recita (higher/head). evidence: kampita_definition_three_shruti_shake (chest = lower), R_558_kuharita_def (middle), recita_definition_higher_three_shruti_shake (higher) """ LOWER = "lower" # chest MIDDLE = "middle" # throat HIGHER = "higher" # head # Sthāna est importé depuis melodic_derivations (domaine 3) en tête de module. # evidence partagée : R_2049_01, R_520_sthana, prasannadi_definition_gradual_low_to_high class AlankāraKālaType(str, Enum): """Kalā-type classification used in the alaṅkāra definitions. evidence: R_189_hunkara_structure (ekakala), R_4p1_1857 (sampradāna: ekakala / dvikala variants), hasita_definition_dvikala_laughter (dvikala), kampita_definition_three_shruti_shake (3 kalā), sandhipracchadana_definition (catuṣkala = 4-kalā) """ EKAKALA = "ekakala" # 1 kalā unit DVIKALA = "dvikala" # 2 kalā units CATUSKALA = "catuskala" # 4 kalā units class MotionDirection(str, Enum): """Direction of svara motion within an alaṅkāra phrase. evidence: varna_definition_primary_unit (steadiness | circulation | ascent | descent), R_129_krama_definition (ascending), avarohin_definition (descending) """ ASCEND = "ascend" DESCEND = "descend" STEADY = "steady" CIRCULATE = "circulate" ASCEND_DESCEND = "ascend_descend" # composite — used by huṅkāra, parivartaka DESCEND_ASCEND = "descend_ascend" # composite — used by prasannamadhya # ----------------------------------------------------------------------------- # Alaṅkāra registry — names, varṇa anchor, kalā type, motion # ----------------------------------------------------------------------------- # evidence: # aff#2558 (count = 33 by name and application), # aff#2693 (33 described by Mātaṅga), # aff#3123 (33 same as NS XXIX), # alankara_count_33, R_069_alankara_structural_role. # The 33 are partitioned by varṇa-anchor: # ārohin → 13 (aff#2631), sañcārin → 11 (aff#2640), avarohin → 5 (aff#2642), # sthāyin → "alaṅkāras based on varṇas excepting sthāyi-varṇa" (aff#2650); # the 4 sthāyivarṇa members are deferred by Mātaṅga himself → UNRESOLVED. # Some alaṅkāras appear in two lists (bindu, kuhara, prenkholita appear in # both ārohin and sañcārin lists; vidhuta, udvāhita in ārohin and avarohin; # veṇu in sañcārin and avarohin); the "varṇa_anchors" field reflects this. # Total of 33 — Mātaṅga's count N_ALANKARAS_TOTAL: int = 33 # aff#2558, aff#2693, alankara_count_33 # Closed enumeration of ārohin-anchored alaṅkāras (13) # evidence: aff#2631, r_brd_567_arohin_def ALANKARAS_AROHIN: tuple[str, ...] = ( "niṣkūjita", # R_568_niskujita, R_5p3_1851 "kuhara", # R_558_kuharita_def, kuhara_within_alankara_lists "hasita", # hasita_definition_dvikala_laughter "bindu", # R_4p0_004 "prenkholita", # prenkholita_definition "ākṣipta", # R_1876_001 "vidhuta", # vidhuta_definition "udvāhita", # R_297_udvahita "hrādamāna", # R_559_hradamana_definition "sampradāna", # R_4p1_1857 "sandhipracchādana", # sandhipracchadana_definition "prasannādi", # prasannadi_definition_gradual_low_to_high "prasannānta", # R_048_prasannanta_definition ) # Closed enumeration of sañcārin-anchored alaṅkāras (11) # evidence: aff#2640, R_4p0_022 (R_c89_sancarin) ALANKARAS_SANCARIN: tuple[str, ...] = ( "mandratāraprasanna", # R_4p1_572, R_1849_mandratara_prasanna "bindu", # appears in both — R_4p0_004 "prenkholita", # appears in both — prenkholita_definition "tāramandraprasanna", # R_1848_taramandra_prasanna_def "nivṛttapravṛtta", # r_brd_1846_nivrttapravrtta_def "kuhara", # appears in both — R_558_kuharita_def "veṇu", # R_4p1_296 "ranjita", # R_4p0_008 "upalolaka", # R_298_upalolaka_def "āvartaka", # R_avartaka_def "paravarta", # UNRESOLVED — listed in aff#2640 but no isolated rule ) # Closed enumeration of avarohin-anchored alaṅkāras (5) # evidence: aff#2642 ALANKARAS_AVAROHIN: tuple[str, ...] = ( "vidhuta", # appears in both ārohin and avarohin "gātravarṇa", # UNRESOLVED — named in aff#2642 but no rule body "udgīta", # R_1858_udgita_def "udvāhita", # appears in both ārohin and avarohin "veṇu", # appears in both sañcārin and avarohin ) # Sthāyivarṇa-anchored alaṅkāras — Mātaṅga DEFERS the general definition. # "I shall speak out the definition of these (alaṅkāras based on varṇas) # excepting the sthāyi-varṇa." (aff#2650) # Two members are anchored explicitly: # - prastāra : "Prastāra and prasāda alaṅkāras based on sthāyivarṇa" # (R_5p3_1861, aff#2630) # - prasāda : same source # Two further members are inferred via the prastāra-position rules # (since prastāra is sthāyivarṇa-anchored, alaṅkāras pinned to specific # prastāra positions belong to sthāyivarṇa): # - kampita (position 9) : kampita_ninth_prastara_form # - recita (position 11): recita_eleventh_prastara_form # evidence: aff#2630, R_5p3_1861, R_571_sthayi_varna_exclusion, aff#2650, # kampita_ninth_prastara_form, recita_eleventh_prastara_form ALANKARAS_STHAYIN_KNOWN: tuple[str, ...] = ( "prastāra", # R_prastara_def, R_180_prastara_definition, R_5p3_1861 "prasāda", # R_prasada_def, R_1850_prasada_prastara, R_5p3_1861 "kampita", # kampita_ninth_prastara_form (via prastāra-position 9) "recita", # recita_eleventh_prastara_form (via prastāra-position 11) # prasannādi is also anchored at prastāra position 1 but is already # classified under ārohin-13 (aff#2631). ) # Multi-varṇa anchoring: an alaṅkāra may appear in >1 list # Built from the 3 closed enumerations above + sthāyin-known. # evidence: aff#2631, aff#2640, aff#2642, aff#2630 def _build_varna_anchors() -> dict[str, frozenset[Varṇa]]: anchors: dict[str, set[Varṇa]] = {} for n in ALANKARAS_AROHIN: anchors.setdefault(n, set()).add(Varṇa.AROHIN) for n in ALANKARAS_SANCARIN: anchors.setdefault(n, set()).add(Varṇa.SANCARIN) for n in ALANKARAS_AVAROHIN: anchors.setdefault(n, set()).add(Varṇa.AVAROHIN) for n in ALANKARAS_STHAYIN_KNOWN: anchors.setdefault(n, set()).add(Varṇa.STHAYIN) return {k: frozenset(v) for k, v in anchors.items()} ALANKARA_VARNA_ANCHORS: dict[str, frozenset[Varṇa]] = _build_varna_anchors() # ----------------------------------------------------------------------------- # Kalā durations — 1-6 range of an alaṅkāra phrase # ----------------------------------------------------------------------------- # evidence: R_069_alankara_structural_role # "phrase_duration IN [1..6] kalas; COUNT alankaras = 33; REQUIRED FOR giti" ALANKARA_KALA_MIN: int = 1 ALANKARA_KALA_MAX: int = 6 # ----------------------------------------------------------------------------- # Pre-pinned alaṅkāra durations (kalās) — extracted verbatim from rule bodies # ----------------------------------------------------------------------------- # evidence: per-rule citations on each entry. ALANKARA_KALAS: dict[str, int] = { "huṅkāra": 18, # R_189_hunkara_structure ("total_kalas = 18") "udghaṭṭita": 18, # R_1856_01 "parivartaka": 16, # R_1854_parivartaka "āvartaka": 8, # R_avartaka_def "kampita": 3, # kampita_definition_three_shruti_shake "recita": 3, # recita_definition_higher_three_shruti_shake "kuharita": 3, # R_558_kuharita_def "sandhipracchādana": 4, # sandhipracchadana_definition (catuṣkala) "sampradāna": 22, # R_4p1_1857 (dvikala variant: 22 kalās) # ḥrādamāna: "three kalās" per repetition — R_559_hradamana_definition "hrādamāna": 3, # R_559_hradamana_definition } # ----------------------------------------------------------------------------- # Alaṅkāra → kalā-type (ekakala / dvikala / catuṣkala) # ----------------------------------------------------------------------------- # evidence: per-rule citations on each entry ALANKARA_KALA_TYPE: dict[str, AlankāraKālaType] = { "huṅkāra": AlankāraKālaType.EKAKALA, # R_189_hunkara_structure "hasita": AlankāraKālaType.DVIKALA, # hasita_definition_dvikala_laughter "udvāhita": AlankāraKālaType.DVIKALA, # R_297_udvahita ("2 kalas each") "hrādamāna": AlankāraKālaType.DVIKALA, # R_559_hradamana_definition "sandhipracchādana": AlankāraKālaType.CATUSKALA, # sandhipracchadana_definition "sampradāna": AlankāraKālaType.DVIKALA, # R_4p1_1857 } # ----------------------------------------------------------------------------- # Alaṅkāra → motion direction # ----------------------------------------------------------------------------- # evidence: per-rule citations on each entry; ascend/descend/etc. parsed from rule body. ALANKARA_MOTION: dict[str, MotionDirection] = { "prasannādi": MotionDirection.ASCEND, # prasannadi_definition_gradual_low_to_high "prasannānta": MotionDirection.DESCEND, # R_048_prasannanta_definition "prastāra": MotionDirection.ASCEND, # R_prastara_def (ascending expansion) "prasāda": MotionDirection.DESCEND, # R_prasada_def (held back/descent) "krama": MotionDirection.ASCEND, # R_129_krama_definition "udgīta": MotionDirection.DESCEND, # R_1858_udgita_def "vidhuta": MotionDirection.ASCEND, # vidhuta_definition (2-svara ascending) "ākṣipta": MotionDirection.ASCEND, # R_1876_001 "ranjita": MotionDirection.ASCEND, # R_4p0_008 (with descent tail) "niṣkūjita": MotionDirection.ASCEND, # R_568_niskujita "huṅkāra": MotionDirection.ASCEND_DESCEND, # R_189_hunkara_structure "parivartaka": MotionDirection.ASCEND_DESCEND, # R_1854_parivartaka "sandhipracchādana": MotionDirection.ASCEND_DESCEND, # sandhipracchadana_definition "āvartaka": MotionDirection.ASCEND_DESCEND, # R_avartaka_def "veṇu": MotionDirection.ASCEND_DESCEND, # R_4p1_296 "udvāhita": MotionDirection.CIRCULATE, # R_297_udvahita (pair to-and-fro) "upalolaka": MotionDirection.CIRCULATE, # R_298_upalolaka_def (variant of udvāhita) "prenkholita": MotionDirection.CIRCULATE, # prenkholita_definition (paired ascent+descent) "prasannamadhya": MotionDirection.DESCEND_ASCEND, # prasannamadhya_definition "prasannādyanta": MotionDirection.ASCEND_DESCEND, # R_165_prasannadyanta_definition "bindu": MotionDirection.STEADY, # R_4p0_004 (stay-long pattern) "sama": MotionDirection.STEADY, # R_187_sama_definition "hrādamāna": MotionDirection.DESCEND, # R_559_hradamana_definition "mandratāraprasanna": MotionDirection.ASCEND, # R_4p1_572 "tāramandraprasanna": MotionDirection.DESCEND, # R_1848_taramandra_prasanna_def "kuharita": MotionDirection.ASCEND, # R_558_kuharita_def "nivṛttapravṛtta": MotionDirection.STEADY, # r_brd_1846_nivrttapravrtta_def (reverse bindu) } # ----------------------------------------------------------------------------- # Alaṅkāra heptad anchoring — chest/throat/head # ----------------------------------------------------------------------------- # evidence: per-rule citations. ALANKARA_HEPTAD: dict[str, Heptad] = { "kampita": Heptad.LOWER, # kampita_definition_three_shruti_shake (lower/chest) "kuharita": Heptad.MIDDLE, # R_558_kuharita_def (middle heptad) "recita": Heptad.HIGHER, # recita_definition_higher_three_shruti_shake (head) "kuhara": Heptad.MIDDLE, # kuhara_definition_throat_obstruction } # ----------------------------------------------------------------------------- # Alaṅkāra → rāga assignment (named-rāga membership) # ----------------------------------------------------------------------------- # evidence: per-rule citations. ALANKARA_RAGA_ASSIGNMENTS: dict[str, frozenset[str]] = { "prasannādi": frozenset({"bhinna-ṣaḍja", "bhinnatāna", "gaudakaiśika"}), # prasannadi_assigned_to_ragas "prasannamadhya": frozenset({"gaudapañcama", "gāndhārapañcama"}), # prasannamadhya_assigned_to_ragas, aff#702, aff#819 "prasannādyanta": frozenset({"pañcamaṣāḍava"}), # aff#831 "prasannamadhyama": frozenset({"mālavakaiśika"}), # R_243_prasannamadhyama_definition, aff#781, aff#789 (narta-rāga) } # ----------------------------------------------------------------------------- # Prastāra-position → alaṅkāra (the 3 anchored prastāra forms over the heptad) # ----------------------------------------------------------------------------- # Three specific prastāra positions over the sequence sa-ri-ga-ma-pa-dha-ni-sa # are tagged with a named alaṅkāra in the source. # evidence: prasannadi_first_prastara_form, kampita_ninth_prastara_form, # recita_eleventh_prastara_form PRASTARA_POSITION_TO_ALANKARA: dict[int, str] = { 1: "prasannādi", 9: "kampita", 11: "recita", } # ----------------------------------------------------------------------------- # Samvādin pairs (consonant svara relations) # ----------------------------------------------------------------------------- # Samvādin is defined by an interval of 9 or 13 śrutis. # Only ONE specific pair is concretely pinned by a rule: # "pañcama is samvādin of ṛṣabha (9 śrutis)" (R_samvadin_def) # Other pairs are NOT individually rule-pinned in domain 1 → UNRESOLVED. # evidence: R_samvadin_def, R_1695_samvadin, R_542_samvadin_def, samvada_definition_to_and_fro SAMVADIN_SRUTI_INTERVALS: frozenset[int] = frozenset({9, 13}) SAMVADIN_PAIRS_PINNED: tuple[frozenset[Svara], ...] = ( frozenset({Svara.RI, Svara.PA}), # R_samvadin_def — madhyamagrāma example frozenset({Svara.SA, Svara.PA}), # samvada_sadja_pancama_consonance ) # Vivāditva: state of dissonance, occurs at 2-śruti separation. # evidence: vivaditva_definition (cluster 1702), # vivadin_substitution_causes_loss VIVADITVA_SRUTI_INTERVAL: int = 2 # Anuvādin: svara that is one śruti less than its corresponding vādin. # Specifically: gāndhāra and niṣāda qualify as anuvādins. # evidence: anuvadin_one_sruti_less, R_823_shuddhashadava_anuvadins ANUVADIN_DEFAULT_SVARAS: frozenset[Svara] = frozenset({Svara.GA, Svara.NI}) # Named raga-specific anuvādin assignments ANUVADIN_RAGA_ASSIGNMENTS: dict[str, frozenset[Svara]] = { "śuddhaṣāḍava": frozenset({Svara.RI, Svara.PA}), # R_823_shuddhashadava_anuvadins } # Vādin svaras: all 7 are eligible (vādi-maṇḍala = cycle of 7 svaras). # evidence: R_1697_vadi_mandala_seven_svaras, vadin_svaras_seven VADIN_ELIGIBLE_SVARAS: frozenset[Svara] = frozenset(SVARA_ORDER) # Named raga vādin assignments VADIN_RAGA_ASSIGNMENTS: dict[str, Svara] = { "śuddhaṣāḍava": Svara.MA, # vadin_assignment_per_raga (madhyama is vādin) } # ============================================================================= # TYPES — dataclasses # ============================================================================= @dataclass(frozen=True) class VarṇaPattern: """A varṇa is a primary unit of melodic movement formed when svaras stretch the syllable (pada) via steadiness, circulation, ascent or descent. "Varṇa is the primary unit of melodic movement..." evidence: varna_definition_primary_unit, varna_four_types, R_1835_001 (count of four constructors), aff#2529, aff#3111 """ type: Varṇa motion: MotionDirection svaras: tuple[Svara, ...] # the svara sequence forming the pattern def __post_init__(self) -> None: # sthāyin: a single svara stays (1 distinct svara repeated) # evidence: BD_6_1_R001 (sthāyin_varna pattern = STEADY(s)) if self.type == Varṇa.STHAYIN: distinct = set(self.svaras) if len(distinct) != 1: raise ValueError( f"sthāyin varṇa must rest on one svara (BD_6_1_R001); " f"got {len(distinct)} distinct" ) if self.motion != MotionDirection.STEADY: raise ValueError( "sthāyin requires MotionDirection.STEADY " "(evidence: BD_6_1_R001)" ) # ārohin: strictly ascending or with gaps of 1-2 svaras # evidence: R_4p0_011, aff#2540 if self.type == Varṇa.AROHIN and self.motion not in ( MotionDirection.ASCEND, MotionDirection.ASCEND_DESCEND, ): raise ValueError( "ārohin varṇa requires ASCEND motion (R_4p0_011, aff#2535)" ) # avarohin: descending (gapless or gap 1-2) # evidence: avarohin_definition, aff#2536, aff#2541 if self.type == Varṇa.AVAROHIN and self.motion not in ( MotionDirection.DESCEND, MotionDirection.DESCEND_ASCEND, ): raise ValueError( "avarohin varṇa requires DESCEND motion (avarohin_definition, aff#2536)" ) @dataclass(frozen=True) class Alaṅkāra: """An alaṅkāra is an ornamental melodic phrase, member of the closed set of 33, classified by varṇa-anchor, kalā-type, motion and duration. "Alaṅkāras are ornaments adorning the singing that subsists in varṇas, making it delightful." evidence: alankara_definition (cluster 69), R_069_alankara_structural_role (33 forms, 1-6 kalās), alankara_built_from_varna (requires knowledge of varṇas), R_2203_001 (motif-governed by varṇas), aff#2558 (33 by name and application), aff#2693, aff#3123 """ name: str varna_anchors: frozenset[Varṇa] motion: MotionDirection | None = None kala_type: AlankāraKālaType | None = None n_kalas: int | None = None heptad: Heptad | None = None assigned_ragas: frozenset[str] = field(default_factory=frozenset) def __post_init__(self) -> None: # An alaṅkāra must be anchored to ≥1 varṇa (or explicitly unresolved). # evidence: aff#2650 ("alaṅkāras based on varṇas"), # alankara_built_from_varna if not self.varna_anchors: raise ValueError( f"alaṅkāra {self.name!r} must have ≥1 varṇa anchor " "(evidence: alankara_built_from_varna, aff#2650)" ) if self.n_kalas is not None: if not is_valid_kala_count(self.n_kalas): raise ValueError( f"alaṅkāra duration {self.n_kalas} outside " f"[{ALANKARA_KALA_MIN}, {ALANKARA_KALA_MAX}] " "(evidence: R_069_alankara_structural_role) — " "note: some alaṅkāras have higher kalā totals reported " "from kalā-unit aggregation; flag if relevant." ) @dataclass(frozen=True) class SvaraRelation: """A binary relation between two svaras: samvāda (consonant), anuvāda (assonant) or vivāda (dissonant). "Saṁvāda refers to to-and-fro mutual movement between two svaras." evidence: samvada_definition_to_and_fro (cluster 122), R_samvadin_def (interval criterion: 9 or 13 śrutis), anuvadin_definition (assonance), BD_6_1_R029 (vivāda = unpleasant), vivaditva_definition (2-śruti interval) """ a: Svara b: Svara role_of_b: SvaraRole # how b relates to a (b is samvādin / vivādin / ... of a) sruti_interval: int | None = None def __post_init__(self) -> None: if self.a == self.b: raise ValueError("Svara-relation must connect two distinct svaras") if self.role_of_b == SvaraRole.SAMVADIN and self.sruti_interval is not None: if self.sruti_interval not in SAMVADIN_SRUTI_INTERVALS: raise ValueError( f"samvādin requires interval ∈ {{9, 13}} śrutis " f"(R_samvadin_def); got {self.sruti_interval}" ) if self.role_of_b == SvaraRole.VIVADIN and self.sruti_interval is not None: if self.sruti_interval != VIVADITVA_SRUTI_INTERVAL: raise ValueError( f"vivādin requires 2-śruti separation " f"(vivaditva_definition); got {self.sruti_interval}" ) @dataclass(frozen=True) class Kalā: """A kalā is a time-unit: (1) equal to a guru = 2 mātrās of tāla (2) an action (kriyā) in tāla (sounded or unsounded) Used to measure the duration of articulation ornaments. evidence: kala_definition_time_unit (cluster 4), R_478_kalas_validation (must not be violated in proper composition) """ n_units: int meaning: str # "time_unit_guru_2_matras" | "kriya_sounded" | "kriya_unsounded" def __post_init__(self) -> None: if self.n_units < 1: raise ValueError("kalā count must be ≥ 1 (R_478_kalas_validation)") @dataclass(frozen=True) class Prastāra: """A prastāra is the systematic permutation/expansion of svaras. "Prastāra is a systematic phrase-expansion where successive phrases add svaras one-by-one." evidence: R_180_prastara_definition (cluster 18), R_prastara_def (in sthāyin & ārohin forms, 15th alaṅkāra), prastara_definition_expansion (cluster 109), R_5p3_1883 (organised from ṣadja), aff#2582 (two forms: sthāyin and ārohin), R_5p3_1861 (sthāyivarṇa-based) """ base_sequence: tuple[Svara, ...] expansion_scheme: str # e.g. "growing_window_with_return" varna_form: Varṇa # STHAYIN or AROHIN — evidence: aff#2582 position: int | None = None # 1-indexed position when known associated_alankara: str | None = None # e.g. "prasannādi" at pos 1 def __post_init__(self) -> None: if self.varna_form not in (Varṇa.STHAYIN, Varṇa.AROHIN): raise ValueError( f"prastāra has two forms only: sthāyin and ārohin " f"(aff#2582); got {self.varna_form}" ) if self.position is not None and self.associated_alankara is not None: expected = PRASTARA_POSITION_TO_ALANKARA.get(self.position) if expected is not None and expected != self.associated_alankara: raise ValueError( f"prastāra position {self.position} → " f"{expected} (rule), not {self.associated_alankara}" ) # ============================================================================= # OPÉRATIONS — dérivations exécutables # ============================================================================= def build_alankara(name: str) -> Alaṅkāra: """Construct an Alaṅkāra from the static registries. Returns an Alaṅkāra with all evidence-pinned attributes filled. Raises KeyError if the name has no varṇa anchor in any closed list. evidence: ALANKARA_VARNA_ANCHORS (built from aff#2631, aff#2640, aff#2642, aff#2630, R_5p3_1861) """ anchors = ALANKARA_VARNA_ANCHORS.get(name) if anchors is None: raise KeyError( f"alaṅkāra {name!r} not found in any of the 4 varṇa lists " "(aff#2631 / aff#2640 / aff#2642 / aff#2630); see UNRESOLVED" ) return Alaṅkāra( name=name, varna_anchors=anchors, motion=ALANKARA_MOTION.get(name), kala_type=ALANKARA_KALA_TYPE.get(name), n_kalas=ALANKARA_KALAS.get(name), heptad=ALANKARA_HEPTAD.get(name), assigned_ragas=ALANKARA_RAGA_ASSIGNMENTS.get(name, frozenset()), ) def enumerate_alankaras_by_varna(v: Varṇa) -> tuple[str, ...]: """Enumerate the alaṅkāras anchored to a given varṇa. evidence: aff#2631 (ārohin → 13), aff#2640 (sañcārin → 11), aff#2642 (avarohin → 5), aff#2650 (sthāyin → deferred) """ if v == Varṇa.AROHIN: return ALANKARAS_AROHIN if v == Varṇa.SANCARIN: return ALANKARAS_SANCARIN if v == Varṇa.AVAROHIN: return ALANKARAS_AVAROHIN if v == Varṇa.STHAYIN: return ALANKARAS_STHAYIN_KNOWN raise ValueError(f"unknown varṇa {v}") def prastara_alankara_at_position(position: int) -> str | None: """Return the alaṅkāra anchored to a given prastāra position over the heptad sa-ri-ga-ma-pa-dha-ni-sa. Returns None if the position has no pinning rule (only 1, 9, 11 are rule-pinned). evidence: prasannadi_first_prastara_form (pos 1), kampita_ninth_prastara_form (pos 9), recita_eleventh_prastara_form (pos 11) """ return PRASTARA_POSITION_TO_ALANKARA.get(position) def classify_svara_role_by_interval( interval_in_srutis: int, ) -> SvaraRole | None: """Classify a svara-pair relation by its śruti interval. 9 or 13 śrutis → samvādin (R_samvadin_def) 2 śrutis → vivādin (vivaditva_definition) Otherwise no canonical classification rule applies → None. Note: the anuvādin criterion ("one-śruti less than the corresponding vādin") is RELATIVE — it depends on the chosen vādin, not on a fixed interval — so it's not handled here. evidence: R_samvadin_def, vivaditva_definition, BD_6_1_R029 """ if interval_in_srutis in SAMVADIN_SRUTI_INTERVALS: return SvaraRole.SAMVADIN if interval_in_srutis == VIVADITVA_SRUTI_INTERVAL: return SvaraRole.VIVADIN return None def vivadin_substitution_invalidates(jati_or_raga_intact: bool) -> bool: """Substituting a vivādin svara at a non-vivādin position destroys the jāti / rāga integrity. Returns True if invalidated; False if the rāga/jāti remained intact. evidence: vivadin_substitution_causes_loss (cluster 1702) """ return not jati_or_raga_intact def derive_audavita_from_samvadin_omission( svaras: frozenset[Svara], samvadin_pair_to_omit: frozenset[Svara], ) -> frozenset[Svara]: """Audavita (pentatonic state) arises by the omission of two saṁvādin svaras. evidence: audavita_via_two_samvadin_omissions (cluster 111) """ if len(samvadin_pair_to_omit) != 2: raise ValueError( "audavita derivation requires exactly 2 svaras " "(audavita_via_two_samvadin_omissions)" ) if not samvadin_pair_to_omit.issubset(svaras): raise ValueError("omitted pair not a subset of input svaras") return svaras - samvadin_pair_to_omit def sadavita_from_seven_svara_jati( jati_n_svaras: int, ) -> int: """Transform a seven-svara jāti into ṣāḍavita (six-svara) form. evidence: R_692_sadavita_seven_to_six """ if jati_n_svaras != 7: raise ValueError( "R_692 ṣāḍavita transformation requires a 7-svara jāti input" ) return 6 # ============================================================================= # CONTRAINTES — validations # ============================================================================= def is_valid_alankara_name(name: str) -> bool: """True iff `name` is one of the 33 alaṅkāras present in any closed list. evidence: aff#2558 (33 enumerated by name), aff#2693, aff#3123, ALANKARA_VARNA_ANCHORS """ return name in ALANKARA_VARNA_ANCHORS def is_valid_kala_count(n: int) -> bool: """True iff `n` ∈ [1, 6] — the canonical alaṅkāra phrase-duration range. evidence: R_069_alankara_structural_role (phrase_duration IN [1..6] kalās) Note: some alaṅkāras report higher kalā TOTALS via repetition or aggregation (huṅkāra=18, sampradāna=22, parivartaka=16). Those are composite counts, not single-phrase durations. """ return ALANKARA_KALA_MIN <= n <= ALANKARA_KALA_MAX def varna_count_is_four(n: int) -> bool: """True iff n == 4. The Brihaddesi states varṇas are four only. evidence: aff#2529 ("verily four only"), aff#2546, aff#3111, varna_four_types, R_1835_001 (count == 4) """ return n == 4 def total_alankara_count_is_33(n: int) -> bool: """True iff n == 33 — the canonical total. evidence: aff#2558, aff#2693, aff#3123, alankara_count_33, R_069_alankara_structural_role """ return n == N_ALANKARAS_TOTAL def is_valid_varna_anchor_partition( arohin: Iterable[str], sancarin: Iterable[str], avarohin: Iterable[str], ) -> bool: """True iff the three closed lists have the canonical sizes (13/11/5). Note: the partition is NOT disjoint — some alaṅkāras (bindu, kuhara, prenkholita, vidhuta, udvāhita, veṇu) appear in 2 lists. This check is on sizes only. evidence: aff#2631 (13), aff#2640 (11), aff#2642 (5) """ return ( len(tuple(arohin)) == 13 and len(tuple(sancarin)) == 11 and len(tuple(avarohin)) == 5 ) def is_alankara_in_varna(name: str, v: Varṇa) -> bool: """True iff alaṅkāra `name` is anchored to varṇa `v` by an explicit list. evidence: ALANKARA_VARNA_ANCHORS (built from aff#2631/40/42/30) """ anchors = ALANKARA_VARNA_ANCHORS.get(name) return anchors is not None and v in anchors def krama_step_size_valid(step: int) -> bool: """True iff krama step size ∈ {1, 2, 3} svaras. evidence: R_129_krama_definition (step_size ∈ {1,2,3} AND no_gap) """ return step in (1, 2, 3) def avarohin_gap_valid(gap: int) -> bool: """True iff avarohin descent gap ∈ {0, 1, 2}. evidence: avarohin_definition (gap ∈ {0,1,2}), aff#2541 """ return gap in (0, 1, 2) def arohin_gap_valid(gap: int) -> bool: """True iff ārohin ascent gap ∈ {0, 1, 2}. evidence: R_4p0_011 (gap ∈ {1, 2}), aff#2540 (gapless or gap of 1 or 2) """ return gap in (0, 1, 2) def is_valid_samvadin_pair( pair: frozenset[Svara], interval_srutis: int, ) -> bool: """True iff pair has 2 distinct svaras and interval ∈ {9, 13} śrutis. evidence: R_samvadin_def """ return ( len(pair) == 2 and interval_srutis in SAMVADIN_SRUTI_INTERVALS ) # ============================================================================= # UNRESOLVED — concepts mentioned but not formally pinned by any 6b rule # ============================================================================= # Listed here for traceability; consumed by the JSON manifest. UNRESOLVED: tuple[dict[str, str], ...] = ( { "concept": "sthāyivarṇa-anchored alaṅkāras (residue)", "reason": "Mātaṅga explicitly defers: 'I shall speak out the " "definition of these (alaṅkāras based on varṇas) " "EXCEPTING the sthāyi-varṇa' (aff#2650). 4 sthāyivarṇa " "members are now anchored: prastāra/prasāda (R_5p3_1861) " "and kampita/recita (via prastāra-positions 9 and 11). " "Adding 13+11+5+4=33 matches aff#2693 — but the union of " "unique names is 26 due to multi-anchoring overlaps, so " "Mātaṅga's count of 33 is by SLOT, not by unique name.", }, { "concept": "huṅkāra, ākṣiptaka, sama, prasannamadhya, prasannādyanta, " "udghaṭṭita — varṇa anchor unknown", "reason": "These have isolated rule-definitions (R_189_hunkara_structure, " "R_c299_aksiptaka, R_187_sama_definition, " "prasannamadhya_definition, R_165_prasannadyanta_definition, " "R_1856_01) but no rule places them in any of the 4 closed " "varṇa-anchor lists (aff#2631/40/42/30 + R_5p3_1861). They " "may belong to the deferred sthāyivarṇa class (aff#2650) " "but anchoring is not directly evidenced.", }, { "concept": "paravarta", "reason": "Named in the sañcārin-11 list (aff#2640) but no rule " "isolates its structural definition in domain 1.", }, { "concept": "gātravarṇa", "reason": "Named in the avarohin-5 list (aff#2642) but no rule body " "captures its structural definition.", }, { "concept": "samvādin pairs beyond {ri,pa} and {sa,pa}", "reason": "Only two samvādin pairs are concretely pinned by rules " "(R_samvadin_def for ri-pa in madhyamagrāma, " "samvada_sadja_pancama_consonance for sa-pa). The general " "{9,13}-śruti criterion is given but a closed canonical " "list is not in the rule set.", }, { "concept": "tribhaṅgi / kūṭa-tāna alaṅkāras", "reason": "Mentioned in clusters 1365 (त्रिभङ्गि), 1796 (kūṭa-tānas: " "5033) but their relation to the 33-alaṅkāra closed list " "is not pinned by a varṇa-anchor rule.", }, { "concept": "ākṣiptaka vs ākṣipta distinction", "reason": "Cluster 299 (ākṣiptaka) and cluster 1876 (ākṣipta) both " "exist; only ākṣipta appears in the ārohin-13 list. The " "exact relation (variant? distinct?) is not pinned.", }, { "concept": "śuddhā states / Nāradīya Śikṣā cross-references", "reason": "Clusters 1392 (śuddhā states), 2146 (Nāradīya Śikṣā) are " "mentioned without operative definitions in domain 1.", }, { "concept": "raga-specific alaṅkāra assignments beyond pinned set", "reason": "Many rāgas mentioned without explicit alaṅkāra-assignment " "rules; only prasannādi, prasannamadhya, prasannādyanta, " "prasannamadhyama have a pinned ALANKARA_RAGA_ASSIGNMENTS " "entry.", }, ) # ============================================================================= # Self-test (executed at import-time only if __main__) # ============================================================================= if __name__ == "__main__": # Sanity: varṇa is 4 (aff#2529) assert varna_count_is_four(len(Varṇa)) assert len(Varṇa) == 4 # Sanity: 33-alaṅkāra count (aff#2558, aff#2693) assert total_alankara_count_is_33(N_ALANKARAS_TOTAL) # Sanity: list sizes match the canonical 13/11/5 (aff#2631, 2640, 2642) assert is_valid_varna_anchor_partition( ALANKARAS_AROHIN, ALANKARAS_SANCARIN, ALANKARAS_AVAROHIN, ) # Sanity: every alaṅkāra in the closed lists is "valid" for n in ALANKARAS_AROHIN + ALANKARAS_SANCARIN + ALANKARAS_AVAROHIN: assert is_valid_alankara_name(n), n # Sanity: multi-anchored alaṅkāras (overlap is explicit) assert Varṇa.AROHIN in ALANKARA_VARNA_ANCHORS["bindu"] assert Varṇa.SANCARIN in ALANKARA_VARNA_ANCHORS["bindu"] assert Varṇa.AROHIN in ALANKARA_VARNA_ANCHORS["vidhuta"] assert Varṇa.AVAROHIN in ALANKARA_VARNA_ANCHORS["vidhuta"] assert Varṇa.SANCARIN in ALANKARA_VARNA_ANCHORS["veṇu"] assert Varṇa.AVAROHIN in ALANKARA_VARNA_ANCHORS["veṇu"] # Sanity: enumerate assert len(enumerate_alankaras_by_varna(Varṇa.AROHIN)) == 13 assert len(enumerate_alankaras_by_varna(Varṇa.SANCARIN)) == 11 assert len(enumerate_alankaras_by_varna(Varṇa.AVAROHIN)) == 5 assert len(enumerate_alankaras_by_varna(Varṇa.STHAYIN)) == 4 # known only # Sanity: build a known alaṅkāra a = build_alankara("prasannādi") assert a.motion == MotionDirection.ASCEND assert "bhinna-ṣaḍja" in a.assigned_ragas a2 = build_alankara("kuhara") # kuhara appears in both ārohin and sañcārin lists assert {Varṇa.AROHIN, Varṇa.SANCARIN}.issubset(a2.varna_anchors) # udvāhita is anchored in BOTH ārohin-13 and avarohin-5 lists a3 = build_alankara("udvāhita") assert {Varṇa.AROHIN, Varṇa.AVAROHIN}.issubset(a3.varna_anchors) assert a3.kala_type == AlankāraKālaType.DVIKALA # kampita is anchored via prastāra-position 9 to sthāyivarṇa a4 = build_alankara("kampita") assert Varṇa.STHAYIN in a4.varna_anchors assert a4.n_kalas == 3 assert a4.heptad == Heptad.LOWER # Sanity: unknown alaṅkāra raises try: build_alankara("notarealalankara") assert False, "should have raised" except KeyError: pass # Sanity: prastāra-position mapping assert prastara_alankara_at_position(1) == "prasannādi" assert prastara_alankara_at_position(9) == "kampita" assert prastara_alankara_at_position(11) == "recita" assert prastara_alankara_at_position(2) is None # Sanity: kalā range assert is_valid_kala_count(1) assert is_valid_kala_count(6) assert not is_valid_kala_count(0) assert not is_valid_kala_count(7) # Sanity: svara role classification assert classify_svara_role_by_interval(9) == SvaraRole.SAMVADIN assert classify_svara_role_by_interval(13) == SvaraRole.SAMVADIN assert classify_svara_role_by_interval(2) == SvaraRole.VIVADIN assert classify_svara_role_by_interval(7) is None # Sanity: samvādin pair construction assert is_valid_samvadin_pair(frozenset({Svara.RI, Svara.PA}), 9) assert not is_valid_samvadin_pair(frozenset({Svara.RI, Svara.PA}), 7) # Sanity: SvaraRelation validates intervals SvaraRelation(Svara.RI, Svara.PA, SvaraRole.SAMVADIN, sruti_interval=9) try: SvaraRelation(Svara.RI, Svara.PA, SvaraRole.SAMVADIN, sruti_interval=7) assert False, "should have raised" except ValueError: pass # Sanity: vivādin substitution invalidates assert vivadin_substitution_invalidates(False) assert not vivadin_substitution_invalidates(True) # Sanity: audavita = sampurna minus 2 samvādins (cluster 111) full = frozenset(SVARA_ORDER) five = derive_audavita_from_samvadin_omission( full, frozenset({Svara.RI, Svara.PA}) ) assert len(five) == 5 assert Svara.RI not in five and Svara.PA not in five # Sanity: ṣāḍavita reduction assert sadavita_from_seven_svara_jati(7) == 6 try: sadavita_from_seven_svara_jati(6) assert False, "should have raised" except ValueError: pass # Sanity: VarṇaPattern construction sth = VarṇaPattern( type=Varṇa.STHAYIN, motion=MotionDirection.STEADY, svaras=(Svara.SA, Svara.SA, Svara.SA), ) assert sth.type == Varṇa.STHAYIN try: # sthāyin with 2 distinct svaras should fail VarṇaPattern(Varṇa.STHAYIN, MotionDirection.STEADY, (Svara.SA, Svara.RI)) assert False, "should have raised" except ValueError: pass # Sanity: Prastāra position vs alaṅkāra Prastāra( base_sequence=SVARA_ORDER, expansion_scheme="growing_window_with_return", varna_form=Varṇa.STHAYIN, position=1, associated_alankara="prasannādi", ) try: Prastāra( base_sequence=SVARA_ORDER, expansion_scheme="growing_window_with_return", varna_form=Varṇa.STHAYIN, position=1, associated_alankara="kampita", # wrong: pos 1 must be prasannādi ) assert False, "should have raised" except ValueError: pass # Sanity: krama step assert krama_step_size_valid(1) assert krama_step_size_valid(3) assert not krama_step_size_valid(4) # Sanity: avarohin / arohin gap assert avarohin_gap_valid(0) assert avarohin_gap_valid(2) assert not avarohin_gap_valid(3) assert arohin_gap_valid(0) assert arohin_gap_valid(2) assert not arohin_gap_valid(3) print("alankara_varna.py — all self-tests pass")