← 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")