← domain #5
Source : tala_prabandha.py
data/library/books/brihaddesi_sharma_1992/formal_grammar/tala_prabandha.py · 816 lines · 31135 bytes
"""Domaine 5 — tāla, prabandha, rhythmic compositions
Synthèse 6c.3 du Brihaddesi (Sharma 1992, vols I & II) depuis :
- 100 règles génératives 6b (cluster_id ∈ domain_id=5)
- 172 affirmations sourcées
- 91 concepts, 73 avec règles
Domaine = formes de chant (prabandhas), leurs composants structurels (birudas,
pāṭas, tenakas, padas, svaras), distinction gandharva/gāna, gamakas au niveau
rythmique, kanda comme structure sectionnelle, cycles tāla.
Anti-fabrication stricte : chaque type, constante, opération et contrainte
cite ≥1 evidence (rule_id ou affirmation_id). Tensions épistémiques (ex. 7 vs
14 varnailā) préservées explicitement, jamais arbitrées par effacement.
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, Literal
# =============================================================================
# CONSTANTES — énumérations fermées du Brihaddesi
# =============================================================================
class Kala(str, Enum):
"""Time-division magnification of a tāla.
Caccatpuṭa (and other tālas) operate in three kala-grades:
ekakala (×1), dvikala (×2), catuṣkala (×4). Each kala pairs with a mārga.
evidence: R_40_caccatputa_kala_marga (aff#393),
R_gandharva_def (5 tālas × 3 mārgas)
"""
EKAKALA = "ekakala"
DVIKALA = "dvikala"
CATUSKALA = "catuskala"
class Marga(str, Enum):
"""The three mārgas (tempo/style classes) of the gāndharva system,
paired one-per-kala on caccatpuṭa-type tālas.
evidence: R_gandharva_def (aff#3155: "Each of the five tālas of gāndharva
have three mārgas"), R_40_caccatputa_kala_marga (aff#393)
"""
CITRA = "citra" # paired with ekakala
VARTIKA = "vartika" # paired with dvikala
DAKSINA = "daksina" # paired with catuṣkala
class Giti(str, Enum):
"""Gīti style coupled to (kala, mārga) pair under caccatpuṭa.
evidence: R_40_caccatputa_kala_marga (aff#393, "in citra mārga there is
māgadhī gīti; in vārtika mārga there is sambhāvitā gīti")
"""
MAGADHI = "magadhi" # citra
SAMBHAVITA = "sambhavita" # vārtika
# daksina/catuṣkala : no specific gīti named in the rule — UNRESOLVED
class CompositionType(str, Enum):
"""The two macro-classes of rhythmic music.
Gāndharva = mārga (Vedic/canonical), 5 tālas × 3 mārgas, upaveda of
Sāmaveda. Deśī = regional/local — "innumerable as deśī music".
evidence: R_gandharva_def (aff#2902 upaveda, aff#3155 5×3),
prabandhas_chapter_origin_innumerable (aff#1306, aff#1317),
desi_gita_uses_vastu (aff for desī gītas)
"""
GANDHARVA = "gandharva"
DESI = "desi"
class Sthana(str, Enum):
"""Tempo grades for a gamaka.
evidence: R_1008_sphurita_tempos ("Sphurita gamaka is performed in fast,
medium and slow tempos"), gamaka_sthita_in_ela (aff#1449)
"""
DRUTA = "druta" # fast
MADHYA = "madhya" # medium
VILAMBITA = "vilambita" # slow / sthita (in elā)
# -----------------------------------------------------------------------------
# Caccatpuṭa — the paradigm tāla of the system
# -----------------------------------------------------------------------------
# Caccatpuṭa is the FIRST among the 5 tālas of Nāṭyaśāstra XXXI; the only
# caturasra ("square") tāla; canonical shape = 4 kalās = 8 mātrās.
# A 16-kalā variant is attested for the Kārmāravī rāga.
# evidence: R_c47_caccatputa (aff#445, aff#487, aff#488, aff#1909)
CACCATPUTA_RANK_IN_NS_XXXI: int = 1 # aff#1909
CACCATPUTA_SHAPE: str = "caturasra" # aff#1909
CACCATPUTA_KALAS_DEFAULT: int = 4 # aff#1909
CACCATPUTA_MATRAS_DEFAULT: int = 8 # aff#1909
CACCATPUTA_KALAS_KARMARAVI_VARIANT: int = 16 # aff#487, aff#488
# Kala × Mārga × Gīti table for caccatpuṭa.
# evidence: R_40_caccatputa_kala_marga (aff#393)
# Each row: (kala, mārga, gīti) — third entry is None where source silent.
CACCATPUTA_KALA_MARGA_GITI: tuple[tuple[Kala, Marga, Giti | None], ...] = (
(Kala.EKAKALA, Marga.CITRA, Giti.MAGADHI),
(Kala.DVIKALA, Marga.VARTIKA, Giti.SAMBHAVITA),
(Kala.CATUSKALA, Marga.DAKSINA, None), # gīti not named — UNRESOLVED
)
# -----------------------------------------------------------------------------
# Gāndharva system structural numbers
# -----------------------------------------------------------------------------
# evidence: R_gandharva_def (aff#2902, aff#3155)
GANDHARVA_N_TALAS: int = 5
GANDHARVA_N_MARGAS_PER_TALA: int = 3
GANDHARVA_UPAVEDA_OF: str = "samaveda"
# -----------------------------------------------------------------------------
# Prabandha ordinals — pinned positions in the 48-list
# -----------------------------------------------------------------------------
# The Brihaddesi enumerates "48 (eight above forty) varieties of beautiful
# prabandhas" (aff#1519). Not all 48 positions are individually rule-pinned;
# we list ONLY those with a sourced ordinal.
# evidence per entry below.
PRABANDHA_ORDINAL: dict[str, int] = {
"kanda": 1, # R_172_kanda_definition (aff#1307 "ādyaḥ prabandhaḥ")
"vrtta": 2, # R_vrtta_prabandha (rule body "2nd prabandha")
"catuspadi": 11, # R_5p2_1083 (aff#1309)
"dodhaka": 12, # R_c173_dodhaka (aff#1310)
"totaka": 13, # R_174_totaka_definition (aff#1311)
"vastu": 14, # R_vastu_prabandha (aff#1318)
"hainsapat": 16, # R_1085_hainsapat_16th_prabandha (aff#1312)
"asvalila": 18, # R_1086_asvalila_eighteenth (aff#1313)
"simhalila": 20, # simhalila_twentieth_prabandha (aff#1314)
"shukacancu": 22, # shukacancu_position_22
"vicitra": 23, # vicitra_definition_and_rank
"caturanga": 25, # r_brd_440_caturanga_def (aff#1323)
"simhavikranta": 28, # R_1092_01 (aff)
"kalahamsa": 29, # R_1093_kalahamsa_ordinal_29 (aff#1333)
"ghata": 30, # R_4p1_442 (aff#1327)
"cakravala": 31, # R_443_cakravala
"tripadi": 32, # BD_6_1_R026
"patakarana": 34, # patakarana_thirty_fourth (aff#1330)
"kaivata": 35, # R_5p2_1096 (aff#1331)
"karana": 40, # R_1101_karana_40th_combined_5
"matrika": 43, # prabandha_enumeration_48_varieties (aff#1332)
"varnasvara": 46, # R_204_varnasvara_definition (aff#1515)
"muktavali": 47, # R_muktavali_def
"pratapavardhana": 48, # R_c206_pratapavardhana (aff#1338)
}
# Total prabandha varieties (closed enumeration claim).
# evidence: prabandha_enumeration_48_varieties (aff#1519 "eight above forty")
PRABANDHA_VARIETIES_TOTAL: int = 48
# Epistemic tension: also "innumerable as deśī music".
# evidence: prabandhas_chapter_origin_innumerable (aff#1306, aff#1317)
# Both claims are preserved here — they are NOT mutually exclusive (48 =
# "beautiful" canonical forms; deśī forms are unbounded).
# -----------------------------------------------------------------------------
# Tāla assignments to prabandhas (closed mapping where rule-sourced)
# -----------------------------------------------------------------------------
# evidence per entry below.
PRABANDHA_TALA: dict[str, str | None] = {
"kanda": None, # R_172_kanda_definition: "bereft of tāla"
"kalahamsa": "jhampa", # R_5p2_1135 / jhampatala_kalahamsaka
"kalahamsaka": "jhampata", # R_1367_kalahamsaka_composition
"hayalila": "turagalila", # R_1122_turagalila_hayalila (aff#1375)
"gajalila": "gajalila", # R_1124_gajalila_definition
"simhavikranta": "aditala", # R_1131_aditala_simhavikranta
"krauncapada": "pratitala", # R_1115 / R_446 (aff#)
"bandhakarana": "karana", # R_1141_bandhakarana_definition
}
# Tālas in which varṇailā is performed (open set, ⊇ this list).
# evidence: varnaila_talas_set (rule body), R_4p1_473 (aff)
VARNAILA_TALAS: tuple[str, ...] = (
"mantha", "dvitiya", "kankala", "dmatitala",
)
# Equivalent listing in R_4p1_473: kankāla is one of {maṇṭha, dvitīya,
# kankāla, jhaṁpatitāla}. The two enumerations co-exist (jhaṁpatitāla vs
# ḍmatitāla = OCR/transliteration variants of the same fourth tāla).
# -----------------------------------------------------------------------------
# Varṇailā — seven syllabically-regulated varieties
# -----------------------------------------------------------------------------
# evidence: varnaila_seven_varieties (aff#1452), varnaila_seven_count (aff#1471)
VARNAILA_VARIETIES_SEVEN: tuple[str, ...] = (
"madanavati", "shashilekha", "prabhavati", "malati",
"lalita", "hemavati", "kusumavati",
)
# Syllable count per foot, position-indexed:
# evidence: varnaila_syllable_count_progression (aff#1456)
# "Beginning with eleven syllables and ending with seventeen"
VARNAILA_MIN_SYLLABLES: int = 11
VARNAILA_MAX_SYLLABLES: int = 17
# Epistemic tension preserved: R_608_varnaila_14_varieties (aff#1497) asserts
# 14 varieties on a different page. Both numbers are sourced; the seven-list
# is operative for syllable-count derivation, the fourteen-count is an
# alternative enumeration not paired with explicit names. See UNRESOLVED.
VARNAILA_VARIETIES_FOURTEEN_COUNT: int = 14 # R_608 (aff#1497)
# -----------------------------------------------------------------------------
# Composition elements — building blocks of prabandhas
# -----------------------------------------------------------------------------
class CompositionElement(str, Enum):
"""The seven structural elements that combine to build a prabandha.
Compiled from explicit composition/relation rules across the domain.
Each element below has at least one rule body that names it as a
constituent of a sourced composition.
evidence: R_204_varnasvara_definition (svara, pada, pata, tenaka, hand_pata),
R_172_kanda_definition (pata, biruda),
R_c94_birudas (biruda),
BD_6_1_R028 (tenaka),
R_446_pratitala_rel (pada, svara),
R_1132_01 (pada, svara, pata),
gamaka_in_shukacancu_opening (gamaka)
"""
SVARA = "svara" # note/tone — passim
PADA = "pada" # textual unit — pada_one_of_four_gita_components
PATA = "pata" # drum/hand syllable — BD_6_1_R005, BD_6_1_R028 by analogy
TENAKA = "tenaka" # "tenna/tena" syllable — BD_6_1_R028
BIRUDA = "biruda" # eulogistic Sanskrit compound — R_472_biruda
GAMAKA = "gamaka" # vocal ornament — R_378_gamakas
HAND_PATA = "hand_pata" # drum syllable on the hands — R_204_varnasvara
# Eulogy themes that birudas express.
# evidence: R_c94_birudas
BIRUDA_THEMES: tuple[str, ...] = (
"plenitude", "renoncement", "bien-etre", "prosperite",
)
# Compositions explicitly listed as biruda-bearing.
# evidence: R_c94_birudas (rule body), aff#1377, aff#1386, aff#1396, aff#1522
BIRUDA_HOSTS: frozenset[str] = frozenset({
"kanda", "hayalila_second_half", "simhalila",
"vicitra", "tripadi", "satpadi", "pratapavardhana",
})
# -----------------------------------------------------------------------------
# Prabandha defects
# -----------------------------------------------------------------------------
# evidence: prabandha_defects (aff#1524)
class PrabandhaDefect(str, Enum):
LOKA_DEFECTIVE = "loka_defective"
SASTRA_DEFECTIVE = "sastra_defective"
REPETITIVE = "repetitive"
METRE_DEFECTIVE = "metre_defective"
VULGAR = "vulgar"
ORDER_DEVIATION = "order_deviation"
# =============================================================================
# TYPES — dataclasses
# =============================================================================
@dataclass(frozen=True)
class TalaConfig:
"""A tāla instance under a specific kala / mārga selection.
evidence: R_40_caccatputa_kala_marga, R_c47_caccatputa, R_gandharva_def
"""
name: str
shape: str | None # "caturasra" for caccatpuṭa, else None unless sourced
kalas: int # mātrās = kalas × 2 for caccatpuṭa default
kala_grade: Kala | None
marga: Marga | None
giti: Giti | None = None # only sourced for ekakala/dvikala caccatpuṭa
@dataclass(frozen=True)
class Prabandha:
"""A prabandha = a song-form. The Brihaddesi describes 48 (aff#1519);
deśī instances are innumerable (aff#1306).
evidence: prabandha_chapter_and_authority, prabandha_enumeration_48_varieties,
R_172_kanda_definition (template for fields)
"""
name: str
ordinal: int | None # position in the 48-list, if pinned
tala: str | None # explicit tāla, or None if "bereft of tāla"
elements: frozenset[CompositionElement]
language: str | None = None # e.g. "karpata" for kanda
n_halves: int | None = None # 2 for dodhaka, hayalīlā…
n_pada: int | None = None # e.g. 8 for śarabhalīla
n_raga: int | None = None
n_tala: int | None = None
def __post_init__(self) -> None:
if self.ordinal is not None and not (1 <= self.ordinal <= PRABANDHA_VARIETIES_TOTAL):
raise ValueError(
f"prabandha ordinal must be in [1, {PRABANDHA_VARIETIES_TOTAL}], "
f"got {self.ordinal} (evidence: prabandha_enumeration_48_varieties)"
)
if not self.elements:
raise ValueError(
"A prabandha must combine at least one composition element "
"(R_204_varnasvara_definition, R_172_kanda_definition)"
)
@dataclass(frozen=True)
class Varnaila:
"""A varṇailā = a syllable-regulated elā variety. Seven varieties named
in aff#1452, with syllable count 11..17 per foot (aff#1456).
Varṇailā is "regulated by syllable count alone, with no regulation of
tāla, rasa, or rāga" (aff#1460) — but is sung in tālas {maṇṭha, dvitīya,
kaṅkāla, ḍmatitāla} (aff#1462). The two statements are reconciled by
reading "no regulation" as "no metre-derived obligation".
evidence: varnaila_seven_varieties, varnaila_seven_count,
varnaila_syllable_count_progression, varnaila_regulated_by_syllables_only,
varnaila_talas_set
"""
name: str # must be in VARNAILA_VARIETIES_SEVEN
syllables_per_foot: int
talas: tuple[str, ...] # ⊇ VARNAILA_TALAS in practice
description_theme: str = "qualities_glory_enthusiasm_steadfastness"
# evidence for theme: varnaila_definition_quality_descriptions (aff#1461)
def __post_init__(self) -> None:
if self.name not in VARNAILA_VARIETIES_SEVEN:
raise ValueError(
f"Unknown varṇailā variety {self.name!r}; allowed = "
f"{VARNAILA_VARIETIES_SEVEN} (varnaila_seven_varieties, aff#1452)"
)
if not (VARNAILA_MIN_SYLLABLES <= self.syllables_per_foot <= VARNAILA_MAX_SYLLABLES):
raise ValueError(
f"varṇailā syllables_per_foot must be in "
f"[{VARNAILA_MIN_SYLLABLES}, {VARNAILA_MAX_SYLLABLES}], got "
f"{self.syllables_per_foot} (varnaila_syllable_count_progression, aff#1456)"
)
@dataclass(frozen=True)
class GamakaApplication:
"""A gamaka rendered in a specific compositional context.
Constraints:
- In elā, gamaka must be sthita (slow/steady) — gamaka_sthita_in_ela (aff#1449)
- In śukacañcu, gamaka opens the composition combined with tāla —
gamaka_in_shukacancu_opening (aff#1378)
- Gamaka is not restricted to "shake" — gamaka_definition_vocal_ornament_inc_shakes (aff#1897)
evidence: R_378_gamakas, gamaka_sthita_in_ela, gamaka_in_shukacancu_opening,
gamaka_definition_vocal_ornament_inc_shakes
"""
host_form: str # composition name (ela, sukacancu, vibhasha…)
tempo: Sthana
position: Literal["opening", "any"] = "any"
@dataclass(frozen=True)
class Biruda:
"""A panegyrical Sanskrit compound phrase.
evidence: R_472_biruda (aff#1983, aff#1467), R_c94_birudas
"""
theme: str # one of BIRUDA_THEMES (or compound-eulogy)
host_form: str # must be in BIRUDA_HOSTS
placement: Literal["pada_end", "second_half", "general"] = "general"
def __post_init__(self) -> None:
if self.host_form not in BIRUDA_HOSTS:
raise ValueError(
f"biruda host {self.host_form!r} not in sourced set "
f"{set(BIRUDA_HOSTS)} (R_c94_birudas)"
)
# =============================================================================
# OPÉRATIONS — dérivations exécutables
# =============================================================================
def caccatputa_config(kala: Kala) -> TalaConfig:
"""Return the caccatpuṭa configuration for a given kala-grade, with
the sourced (mārga, gīti) pairing.
evidence: R_c47_caccatputa (shape/kalas/matras),
R_40_caccatputa_kala_marga (kala→mārga→gīti row)
"""
for k, marga, giti in CACCATPUTA_KALA_MARGA_GITI:
if k == kala:
return TalaConfig(
name="caccatputa",
shape=CACCATPUTA_SHAPE,
kalas=CACCATPUTA_KALAS_DEFAULT,
kala_grade=kala,
marga=marga,
giti=giti,
)
raise ValueError(
f"No sourced row for kala={kala} in caccatpuṭa table "
f"(R_40_caccatputa_kala_marga)"
)
def caccatputa_karmaravi_variant() -> TalaConfig:
"""The 16-kalā variant of caccatpuṭa attested for the Kārmāravī rāga.
evidence: R_c47_caccatputa (aff#487, aff#488 "sixteen kalās")
"""
return TalaConfig(
name="caccatputa_karmaravi",
shape=CACCATPUTA_SHAPE,
kalas=CACCATPUTA_KALAS_KARMARAVI_VARIANT,
kala_grade=None,
marga=None,
giti=None,
)
def varnaila_at(index: int) -> Varnaila:
"""Build the k-th varṇailā (0..6) with its sourced syllable count.
The k-th variety has 11+k syllables per foot, by the progression rule.
evidence: varnaila_seven_varieties (aff#1452),
varnaila_syllable_count_progression (aff#1456),
varnaila_talas_set
"""
if not (0 <= index < len(VARNAILA_VARIETIES_SEVEN)):
raise IndexError(
f"varṇailā index must be in [0, {len(VARNAILA_VARIETIES_SEVEN)}); "
f"got {index}"
)
return Varnaila(
name=VARNAILA_VARIETIES_SEVEN[index],
syllables_per_foot=VARNAILA_MIN_SYLLABLES + index,
talas=VARNAILA_TALAS,
)
def all_varnailas() -> list[Varnaila]:
"""Enumerate the 7 sourced varṇailā varieties.
evidence: varnaila_seven_varieties, varnaila_seven_count
"""
return [varnaila_at(k) for k in range(len(VARNAILA_VARIETIES_SEVEN))]
def kanda() -> Prabandha:
"""Build kanda — the 1st prabandha, sans tāla, combined with pāṭas and
birudas, mixed with Karpāṭa-language pada.
evidence: R_172_kanda_definition (aff#1307, aff#1339, aff#1340)
"""
return Prabandha(
name="kanda",
ordinal=PRABANDHA_ORDINAL["kanda"],
tala=None,
elements=frozenset({
CompositionElement.PATA,
CompositionElement.BIRUDA,
CompositionElement.PADA,
}),
language="karpata",
)
def hayalila() -> Prabandha:
"""Build hayalīlā — composition where first half is sung with svaras,
second half with birudas, in turagalīla tāla.
evidence: R_1123_01, R_4p1_1362, R_1122_turagalila_hayalila (aff#1375)
"""
return Prabandha(
name="hayalila",
ordinal=None, # no ordinal pinned in 6b rule set
tala="turagalila",
elements=frozenset({
CompositionElement.SVARA,
CompositionElement.BIRUDA,
}),
n_halves=2,
)
def sarabhalila() -> Prabandha:
"""Build śarabhalīla — 8 pādas, each with a distinct rāga and distinct
tāla, composed of svaras + pāṭas.
evidence: R_1127_sarabhalila_eight_feet, BD_6_1_R013, BD_6_1_R005
"""
return Prabandha(
name="sarabhalila",
ordinal=None,
tala=None, # 8 distinct tālas — not a single value
elements=frozenset({CompositionElement.SVARA, CompositionElement.PATA}),
n_pada=8,
n_raga=8,
n_tala=8,
)
def kalahamsa() -> Prabandha:
"""Build kalahaṁsa — 29th prabandha; one pada sung first, then svaras,
combined with jhampā tāla.
evidence: R_1093_kalahamsa_ordinal_29 (aff#1333), R_5p2_1135,
R_1134_kalahamsa_definition
"""
return Prabandha(
name="kalahamsa",
ordinal=PRABANDHA_ORDINAL["kalahamsa"],
tala=PRABANDHA_TALA["kalahamsa"],
elements=frozenset({CompositionElement.PADA, CompositionElement.SVARA}),
)
def prabandha_by_ordinal(n: int) -> str | None:
"""Look up the prabandha name at the n-th canonical position.
evidence: PRABANDHA_ORDINAL (each entry sourced individually).
Returns None if no rule pins that position.
"""
if not (1 <= n <= PRABANDHA_VARIETIES_TOTAL):
raise ValueError(
f"ordinal must be in [1, {PRABANDHA_VARIETIES_TOTAL}], got {n}"
)
for name, ord_ in PRABANDHA_ORDINAL.items():
if ord_ == n:
return name
return None
def gamaka_in_ela(host_form: str) -> GamakaApplication:
"""Construct a gamaka constrained for elā context: must be sthita.
evidence: gamaka_sthita_in_ela (aff#1449),
R_378_gamakas (gamakas as melodic ornaments)
"""
if "ela" not in host_form.lower():
raise ValueError(
f"gamaka_in_ela expects an elā-class host; got {host_form!r} "
f"(gamaka_sthita_in_ela, aff#1449)"
)
return GamakaApplication(
host_form=host_form,
tempo=Sthana.VILAMBITA,
position="any",
)
def gamaka_in_sukacancu() -> GamakaApplication:
"""Opening gamaka for śukacañcu, combined with tāla.
evidence: gamaka_in_shukacancu_opening (aff#1378),
shukacancu_definition_svaras_patas_regional
"""
return GamakaApplication(
host_form="sukacancu",
tempo=Sthana.MADHYA, # not pinned — placeholder default
position="opening",
)
def biruda_for(host_form: str, theme: str = "eulogy") -> Biruda:
"""Place a biruda in a sourced host form.
evidence: R_c94_birudas, R_472_biruda (aff#1983)
"""
placement: Literal["pada_end", "second_half", "general"] = "general"
if host_form == "hayalila_second_half":
placement = "second_half"
elif host_form == "kanda":
placement = "general"
return Biruda(theme=theme, host_form=host_form, placement=placement)
# =============================================================================
# CONTRAINTES — validations
# =============================================================================
def is_valid_prabandha_ordinal(n: int) -> bool:
"""A prabandha ordinal must lie in the closed 48-range.
evidence: prabandha_enumeration_48_varieties (aff#1519)
"""
return 1 <= n <= PRABANDHA_VARIETIES_TOTAL
def is_valid_caccatputa_kala_marga(kala: Kala, marga: Marga) -> bool:
"""A (kala, mārga) pair is valid iff present in the sourced table.
evidence: R_40_caccatputa_kala_marga (aff#393)
"""
for k, m, _ in CACCATPUTA_KALA_MARGA_GITI:
if k == kala and m == marga:
return True
return False
def is_valid_biruda_host(host_form: str) -> bool:
"""Birudas may appear only in the rule-attested host set.
evidence: R_c94_birudas (aff#1345, aff#1377, aff#1386, aff#1396, aff#1522)
"""
return host_form in BIRUDA_HOSTS
def is_valid_varnaila_syllable_count(n: int) -> bool:
"""Varṇailā syllable count per foot lies in [11, 17].
evidence: varnaila_syllable_count_progression (aff#1456)
"""
return VARNAILA_MIN_SYLLABLES <= n <= VARNAILA_MAX_SYLLABLES
def is_valid_gamaka_in_ela(g: GamakaApplication) -> bool:
"""In elā, gamaka must be sthita (= vilambita).
evidence: gamaka_sthita_in_ela (aff#1449)
"""
if "ela" not in g.host_form.lower():
return True # constraint only applies to elā contexts
return g.tempo == Sthana.VILAMBITA
# =============================================================================
# UNRESOLVED — concepts mentioned but not pinned by 6b rules
# =============================================================================
UNRESOLVED: tuple[dict[str, str], ...] = (
{
"concept": "dhātu / udgrāha / dhruva / antara / ābhoga sectional structure",
"reason": "later Sangīta-Ratnākara codifies prabandha into 4-section "
"dhātus, but Brihaddesi's 6b rule set does not isolate them. "
"Kanda's 'sectional' character is alluded (R_172_kanda_definition) "
"without explicit dhātu vocabulary.",
},
{
"concept": "varṇailā variety count (7 vs 14)",
"reason": "varnaila_seven_count (aff#1471, vol_II_p124) and "
"varnaila_seven_varieties (aff#1452, vol_II_p123) give 7 named; "
"R_608_varnaila_14_varieties (aff#1497, vol_II_p127) gives 14. "
"Both preserved; the 14-list is not enumerated by name in 6b.",
},
{
"concept": "catuṣkala/dakṣiṇa gīti of caccatpuṭa",
"reason": "R_40_caccatputa_kala_marga gives gīti for ekakala (māgadhī) "
"and dvikala (sambhāvitā) but the rule body trails off for "
"catuṣkala/dakṣiṇa.",
},
{
"concept": "ordinals of prabandhas 3-10, 15, 17, 19, 21, 24, 26-27, 33, "
"36-39, 41-42, 44-45",
"reason": "PRABANDHA_VARIETIES_TOTAL=48 (aff#1519) but only 24 ordinals "
"are individually rule-pinned in 6b. Source enumeration "
"uddesa-list 'does not match described forms exactly' "
"(prabandha_chapter_and_authority).",
},
{
"concept": "biruda placement / yati-gamaka coupling in elā",
"reason": "R_472_biruda mentions 'gamaka_on_yati' after a biruda at "
"pada_end in elā; no rule isolates yati structure here.",
},
{
"concept": "gandharva-deśī formal boundary",
"reason": "gāndharva = 5 tālas × 3 mārgas (R_gandharva_def); deśī = "
"'innumerable' (aff#1306). The crossover (e.g. caccatpuṭa "
"used in deśī rāgas) is attested but not formally typed in 6b.",
},
{
"concept": "dvipathaka 4 subforms",
"reason": "R_c256_dvipathaka asserts 4 subforms with/without tāla; "
"individual subforms are not enumerated by name.",
},
)
# =============================================================================
# Self-test (executed only when __main__)
# =============================================================================
if __name__ == "__main__":
# caccatpuṭa configurations
c1 = caccatputa_config(Kala.EKAKALA)
assert c1.marga == Marga.CITRA and c1.giti == Giti.MAGADHI
c2 = caccatputa_config(Kala.DVIKALA)
assert c2.marga == Marga.VARTIKA and c2.giti == Giti.SAMBHAVITA
c3 = caccatputa_config(Kala.CATUSKALA)
assert c3.marga == Marga.DAKSINA and c3.giti is None # UNRESOLVED gīti
assert c1.kalas == 4 and c1.shape == "caturasra"
cv = caccatputa_karmaravi_variant()
assert cv.kalas == 16
# kala/mārga validation
assert is_valid_caccatputa_kala_marga(Kala.EKAKALA, Marga.CITRA)
assert not is_valid_caccatputa_kala_marga(Kala.EKAKALA, Marga.DAKSINA)
# varṇailā enumeration
vs = all_varnailas()
assert len(vs) == 7
assert vs[0].name == "madanavati" and vs[0].syllables_per_foot == 11
assert vs[6].name == "kusumavati" and vs[6].syllables_per_foot == 17
assert is_valid_varnaila_syllable_count(11)
assert is_valid_varnaila_syllable_count(17)
assert not is_valid_varnaila_syllable_count(10)
assert not is_valid_varnaila_syllable_count(18)
# unknown variety rejected
try:
Varnaila(name="unknown", syllables_per_foot=12, talas=VARNAILA_TALAS)
assert False, "should have raised"
except ValueError:
pass
# prabandha constructors
k = kanda()
assert k.ordinal == 1
assert k.tala is None
assert CompositionElement.BIRUDA in k.elements
h = hayalila()
assert h.tala == "turagalila" and h.n_halves == 2
s = sarabhalila()
assert s.n_pada == 8 and s.n_raga == 8 and s.n_tala == 8
kh = kalahamsa()
assert kh.ordinal == 29 and kh.tala == "jhampa"
# ordinal lookup
assert prabandha_by_ordinal(1) == "kanda"
assert prabandha_by_ordinal(48) == "pratapavardhana"
assert prabandha_by_ordinal(13) == "totaka"
assert prabandha_by_ordinal(5) is None # not pinned in 6b
assert is_valid_prabandha_ordinal(1)
assert is_valid_prabandha_ordinal(48)
assert not is_valid_prabandha_ordinal(0)
assert not is_valid_prabandha_ordinal(49)
try:
prabandha_by_ordinal(49)
assert False
except ValueError:
pass
# gamaka in elā must be sthita / vilambita
g_ela = gamaka_in_ela("lalita_ela")
assert g_ela.tempo == Sthana.VILAMBITA
assert is_valid_gamaka_in_ela(g_ela)
g_bad = GamakaApplication(host_form="lalita_ela", tempo=Sthana.DRUTA)
assert not is_valid_gamaka_in_ela(g_bad)
try:
gamaka_in_ela("kanda")
assert False
except ValueError:
pass
# gamaka in śukacañcu = opening
g_su = gamaka_in_sukacancu()
assert g_su.position == "opening"
# birudas: host validation
assert is_valid_biruda_host("kanda")
assert is_valid_biruda_host("simhalila")
assert not is_valid_biruda_host("varnaila")
b = biruda_for("hayalila_second_half", theme="plenitude")
assert b.placement == "second_half"
try:
biruda_for("random_form")
assert False
except ValueError:
pass
# prabandha defect enum
assert PrabandhaDefect.REPETITIVE.value == "repetitive"
# gāndharva structural numbers
assert GANDHARVA_N_TALAS == 5 and GANDHARVA_N_MARGAS_PER_TALA == 3
assert GANDHARVA_UPAVEDA_OF == "samaveda"
# prabandha total / coverage
assert PRABANDHA_VARIETIES_TOTAL == 48
pinned = sorted(PRABANDHA_ORDINAL.values())
assert pinned[0] == 1 and pinned[-1] == 48
assert len(set(pinned)) == len(pinned) # ordinals unique
# Prabandha invalid-ordinal raises
try:
Prabandha(name="x", ordinal=49, tala=None,
elements=frozenset({CompositionElement.SVARA}))
assert False
except ValueError:
pass
# Empty-elements Prabandha rejected
try:
Prabandha(name="x", ordinal=1, tala=None, elements=frozenset())
assert False
except ValueError:
pass
print("tala_prabandha.py — all self-tests pass")