← domain #3

Source : melodic_derivations.py

data/library/books/brihaddesi_sharma_1992/formal_grammar/melodic_derivations.py · 577 lines · 22717 bytes
"""Domaine 3 — mélodique modal (mūrchanā, grāma, tāna, svara) Synthèse 6c.3 du Brihaddesi (Sharma 1992, vols I & II) depuis : - 114 règles génératives 6b - 241 affirmations sourcées - structures 4e (tables mūrchanā-maṇḍala p.037, mūrchanā nommées p.051, tables tāna p.040) - partition Leiden domain_id=3 (104 concepts) Anti-fabrication : chaque type, opération et contrainte cite son evidence (rule_id 6b ou affirmation_id). Aucune valeur non sourcée n'est introduite. 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 Svara(str, Enum): """The seven svaras of the gāmut. evidence: R_c57_grama, R_c39_murchana_devanagari, structures 4e p.037 """ SA = "sa" # ṣaḍja RI = "ri" # ṛṣabha GA = "ga" # gāndhāra MA = "ma" # madhyama PA = "pa" # pañcama DHA = "dha" # dhaivata NI = "ni" # niṣāda SVARA_ORDER: tuple[Svara, ...] = ( Svara.SA, Svara.RI, Svara.GA, Svara.MA, Svara.PA, Svara.DHA, Svara.NI, ) class Grāma(str, Enum): """The two operative grāmas in Brihaddesi. Brihaddesi treats only ṣaḍja-grāma and madhyama-grāma as live frames. A third (gāndhāra-grāma) is alluded to in tradition but no rule states its svara content here — see UNRESOLVED. evidence: R_c57_grama, R_319_gramas (differentiation by pañcama position) """ SADJA = "sadjagrama" MADHYAMA = "madhyamagrama" class ScaleType(str, Enum): """Density of a svara collection. evidence: R_147_auduva_definition (auduva=5), R_1266_sadjamadhyama_struct (purna=7, sadava=6, auduva=5) """ SAMPURNA = "sampurna" # full 7-svara set SADAVA = "sadava" # hexatonic (one omission) AUDUVA = "auduva" # pentatonic (two omissions) class Sthāna(str, Enum): """The three vocal registers — objective of tānas. evidence: R_2049_01, R_520_sthana, R_271_kaku_def """ MANDRA = "mandra" # low MADHYA = "madhya" # middle TARA = "tara" # high # ----------------------------------------------------------------------------- # Mūrchanā-maṇḍala — tables 4e p.037 (Sharma vol. I) # ----------------------------------------------------------------------------- # Each row is one mūrchanā = a 7-svara rotation of the grāma frame. # These tables are extracted verbatim from the OCR'd Brihaddesi diagrams, # not derived theoretically. # evidence: structures 4e p.037 (concept=mūrchanā-maṇḍala), confidence=0.95 # corroborated by R_c57_grama (murchanas_start_order) MURCHANA_MANDALA_SADJA: tuple[tuple[Svara, ...], ...] = ( # Header row of source diagram: ni-dha-pa-ma-ga-ri-sa (Svara.NI, Svara.DHA, Svara.PA, Svara.MA, Svara.GA, Svara.RI, Svara.SA), (Svara.DHA, Svara.PA, Svara.MA, Svara.GA, Svara.RI, Svara.SA, Svara.NI), (Svara.PA, Svara.MA, Svara.GA, Svara.RI, Svara.SA, Svara.NI, Svara.DHA), (Svara.MA, Svara.GA, Svara.RI, Svara.SA, Svara.NI, Svara.DHA, Svara.PA), (Svara.GA, Svara.RI, Svara.SA, Svara.NI, Svara.DHA, Svara.PA, Svara.MA), (Svara.RI, Svara.SA, Svara.NI, Svara.DHA, Svara.PA, Svara.MA, Svara.GA), (Svara.SA, Svara.NI, Svara.DHA, Svara.PA, Svara.MA, Svara.GA, Svara.RI), ) MURCHANA_MANDALA_MADHYAMA: tuple[tuple[Svara, ...], ...] = ( # Header row of source diagram: ga-ri-sa-ni-dha-pa-ma (Svara.GA, Svara.RI, Svara.SA, Svara.NI, Svara.DHA, Svara.PA, Svara.MA), (Svara.RI, Svara.SA, Svara.NI, Svara.DHA, Svara.PA, Svara.MA, Svara.GA), (Svara.SA, Svara.NI, Svara.DHA, Svara.PA, Svara.MA, Svara.GA, Svara.RI), (Svara.NI, Svara.DHA, Svara.PA, Svara.MA, Svara.GA, Svara.RI, Svara.SA), (Svara.DHA, Svara.PA, Svara.MA, Svara.GA, Svara.RI, Svara.SA, Svara.NI), (Svara.PA, Svara.MA, Svara.GA, Svara.RI, Svara.SA, Svara.NI, Svara.DHA), (Svara.MA, Svara.GA, Svara.RI, Svara.SA, Svara.NI, Svara.DHA, Svara.PA), ) MURCHANA_MANDALA: dict[Grāma, tuple[tuple[Svara, ...], ...]] = { Grāma.SADJA: MURCHANA_MANDALA_SADJA, Grāma.MADHYAMA: MURCHANA_MANDALA_MADHYAMA, } # Named mūrchanās — Bharata enumeration cited by Mātaṅga (Sharma vol. I p.051) # 14 names, 7 per grāma. # evidence: structures 4e p.051, R_550_uttaramandra_murchana, # R_1818_asvakranta_murchana, R_5p3_1819, R_c1821_sauviri_murchana, # R_1822_001, kalopanata_madhyamagrama_twelve_svara, R_1824_01, # R_1825_margi_murchana_madhyamagrama, R_1826_pauravi_murchana_sequence, # R_1827_hrsyaka_murchana, R_1748_rajani_nisada NAMED_MURCHANAS_SADJA: tuple[str, ...] = ( "uttaramandrā", "rajanī", "uttarāyatā", "śuddhaṣaḍjā", "matsarīkṛtā", "aśvakrāntā", "abhirudgatā", ) NAMED_MURCHANAS_MADHYAMA: tuple[str, ...] = ( "sauvīrī", "hariṇāśvā", "kalopanatā", "śuddhamadhyā", "mārgavī", "pauravī", "hṛṣyakā", ) # Starting-svara mapping for named mūrchanās — extracted from rule bodies # that give concrete svara sequences (R_1818..R_1827) and R_550, R_1748. # evidence per entry below. NAMED_MURCHANA_START: dict[str, tuple[Grāma, Svara]] = { "uttaramandrā": (Grāma.SADJA, Svara.SA), # R_550_uttaramandra_murchana "rajanī": (Grāma.SADJA, Svara.NI), # R_1748_rajani_nisada "aśvakrāntā": (Grāma.SADJA, Svara.MA), # R_1818_asvakranta_murchana "abhirudgatā": (Grāma.SADJA, Svara.PA), # R_5p3_1819 "sauvīrī": (Grāma.MADHYAMA, Svara.NI), # R_c1821_sauviri_murchana "hariṇāśvā": (Grāma.MADHYAMA, Svara.SA), # R_1822_001 "kalopanatā": (Grāma.MADHYAMA, Svara.RI), # kalopanata_..._twelve_svara "śuddhamadhyā": (Grāma.MADHYAMA, Svara.GA), # R_1824_01 "mārgavī": (Grāma.MADHYAMA, Svara.MA), # R_1825_margi_murchana "pauravī": (Grāma.MADHYAMA, Svara.PA), # R_1826_pauravi_murchana "hṛṣyakā": (Grāma.MADHYAMA, Svara.DHA), # R_1827_hrsyaka_murchana # uttarāyatā, śuddhaṣaḍjā, matsarīkṛtā : named in p.051 list, no individual # starting-svara rule found in 6b — see UNRESOLVED. } # ----------------------------------------------------------------------------- # Inomissibility — which svaras cannot be dropped to form ṣāḍava / auduva # ----------------------------------------------------------------------------- # evidence: madhyamagrama_rsabha_pancama_inomissible (aff#612, aff#1935), # sadjagrama_inomissible_and_vivadi (aff#1924, aff#2294), # sadjagrama_sadava_murchana_formation (aff#290, aff#2408) INOMISSIBLE_SVARAS: dict[Grāma, frozenset[Svara]] = { # ṣaḍja-grāma: dhaivata is inomissible; gāndhāra-omission is "not # prescribed" for ṣāḍava (aff#290). madhyama is always inomissible. Grāma.SADJA: frozenset({Svara.DHA, Svara.MA}), # madhyama-grāma: ṛṣabha and pañcama are inomissible (aff#612); # pañcama specifically "inomissible according to Datti" (aff#1935). Grāma.MADHYAMA: frozenset({Svara.RI, Svara.PA, Svara.MA}), } # Permissible svaras for ṣāḍava omission per grāma (closed enumeration). # evidence: sadjagrama_sadava_murchana_formation (rule body explicitly lists # the 4 allowed omissions); for madhyama-grāma we derive by complement of # inomissible set (no positive enumeration rule found). SADAVA_OMISSION_CANDIDATES: dict[Grāma, frozenset[Svara]] = { Grāma.SADJA: frozenset({Svara.SA, Svara.RI, Svara.PA, Svara.NI}), # Madhyama: SA, GA, DHA, NI allowed (by complement of inomissible {RI,PA,MA}) Grāma.MADHYAMA: frozenset({Svara.SA, Svara.GA, Svara.DHA, Svara.NI}), } # ----------------------------------------------------------------------------- # Auduva (pentatonic) sub-categories — closed pairs of co-omitted svaras # ----------------------------------------------------------------------------- # evidence: R_147_auduva_definition (subtypes={devoid_niga, devoid_pani}), # R_1769_nigahina_auduva, R_5p3_1770, R_1786_01 AUDUVA_OMISSION_PAIRS: tuple[frozenset[Svara], ...] = ( frozenset({Svara.NI, Svara.GA}), # nigahīnā frozenset({Svara.PA, Svara.NI}), # auduva-parihīna ) # ----------------------------------------------------------------------------- # Tāna counts — enumerative facts from the Brihaddesi # ----------------------------------------------------------------------------- # evidence: BD_6_1_R007 (21 ṣāḍava tānas in madhyama), # R_4p0_025 (14 auduvita in madhyama, 35 total auduvita, # 84 = ṣāḍava+auduvita across both grāmas), # audavita_tanas_count_sadjagrama (21 audavita in ṣaḍja) TANA_COUNTS: dict[tuple[Grāma, ScaleType], int] = { (Grāma.MADHYAMA, ScaleType.SADAVA): 21, # BD_6_1_R007 (Grāma.MADHYAMA, ScaleType.AUDUVA): 14, # R_4p0_025 (Grāma.SADJA, ScaleType.AUDUVA): 21, # audavita_tanas_count_sadjagrama # SADJA × SADAVA: no direct count rule isolated to ṣaḍja alone (see UNRESOLVED) } TANAS_AUDUVA_BOTH_GRAMAS: int = 35 # R_4p0_025 TANAS_SADAVA_PLUS_AUDUVA_BOTH: int = 84 # R_4p0_025 # ----------------------------------------------------------------------------- # Named auduvita tānas devoid of ni and ga, in madhyama-grāma # ----------------------------------------------------------------------------- # evidence: R_1786_01 (full enumeration), R_1787_moksada_seventh_auduvita AUDUVITA_TANAS_NIGAHINA_MADHYAMA: tuple[str, ...] = ( "bhairava", "kāmada", "avabhṛtha", "aṣṭakapālaka", "sviṣṭakṛt", "vaṣaṭkāra", "mokṣada", ) # ============================================================================= # TYPES — dataclasses # ============================================================================= @dataclass(frozen=True) class Mūrchanā: """A mūrchanā is a 7-svara cyclic rotation of a grāma scale, starting on one specific svara — the "vehicle of rāga growth" (etymology mūrch + samucchrāya). A second sense, "12-svara mūrchanā", appears in performance context for attaining the three sthānas (see Mūrchanā12). evidence: R_c39_murchana_devanagari (definition + start_svara), murchana_definition_heptad_ascent_descent, murchana_twofold_seven_or_twelve_svaras """ grāma: Grāma starting_svara: Svara sequence: tuple[Svara, ...] # length 7 name: str | None = None # canonical name if known (uttaramandrā, etc.) def __post_init__(self) -> None: if len(self.sequence) != 7: raise ValueError( f"Mūrchanā must have 7 svaras, got {len(self.sequence)}" ) if self.sequence[0] != self.starting_svara: raise ValueError( f"sequence[0]={self.sequence[0]} != starting_svara={self.starting_svara}" ) if set(self.sequence) != set(SVARA_ORDER): raise ValueError("Mūrchanā sequence must permute all 7 svaras") @dataclass(frozen=True) class Mūrchanā12: """A twelve-svara mūrchanā used in rāga performance to attain the three sthānas. Subsumed within ṣaḍja-grāma mūrchanās during performance. evidence: murchana_twofold_seven_or_twelve_svaras (aff#2369, aff#2507), murchana_twelve_svara_for_three_sthanas, R_549_gramamurchana """ grāma: Grāma sequence: tuple[Svara, ...] # length 12 name: str | None = None def __post_init__(self) -> None: if len(self.sequence) != 12: raise ValueError( f"Mūrchanā12 must have 12 svaras, got {len(self.sequence)}" ) @dataclass(frozen=True) class Tāna: """A tāna is the result of a svara-omission operation on a mūrchanā / scale, producing a ṣāḍava (6-note) or auduva (5-note) collection. "Omission of svaras is operative only in mūrchanās and not in grāma" (aff#1925). evidence: tana_definition (aff#3097), R_275_murchana_omission (aff#1925), tana_definition_cyclic_permutations """ parent_grāma: Grāma omitted: frozenset[Svara] svaras: frozenset[Svara] scale_type: ScaleType name: str | None = None def __post_init__(self) -> None: n = len(self.svaras) if n == 5 and self.scale_type != ScaleType.AUDUVA: raise ValueError("5-svara tāna must be ScaleType.AUDUVA") if n == 6 and self.scale_type != ScaleType.SADAVA: raise ValueError("6-svara tāna must be ScaleType.SADAVA") if n not in (5, 6): raise ValueError(f"Tāna must have 5 or 6 svaras, got {n}") if self.svaras & self.omitted: raise ValueError("svaras and omitted must be disjoint") if self.svaras | self.omitted != set(SVARA_ORDER): raise ValueError("svaras + omitted must cover all 7 svaras") @dataclass(frozen=True) class GrāmaSpec: """Static specification of a grāma: its inomissible svaras, ordered cyclic frame, and structural properties. evidence: R_c57_grama, R_319_gramas, sadjagrama_inomissible_and_vivadi, madhyamagrama_rsabha_pancama_inomissible """ name: Grāma inomissible: frozenset[Svara] n_murchanas: int # 7 — same for both grāmas n_jatis: int # 7 for ṣaḍja-grāma, 11 for madhyama-grāma GRAMA_SPECS: dict[Grāma, GrāmaSpec] = { Grāma.SADJA: GrāmaSpec( name=Grāma.SADJA, inomissible=INOMISSIBLE_SVARAS[Grāma.SADJA], n_murchanas=7, n_jatis=7, # sadjagrama_seven_jatis (aff#...) ), Grāma.MADHYAMA: GrāmaSpec( name=Grāma.MADHYAMA, inomissible=INOMISSIBLE_SVARAS[Grāma.MADHYAMA], n_murchanas=7, n_jatis=11, # madhyamagrama_jati_basis ), } # ============================================================================= # OPÉRATIONS — dérivations exécutables # ============================================================================= def murchana_at(grāma: Grāma, k: int) -> Mūrchanā: """Return the k-th mūrchanā of a grāma from the mūrchanā-maṇḍala table. The mūrchanā-maṇḍala (Sharma vol. I p.037) is the canonical source: each row is one of the 7 rotations. We do NOT compute the rotation from SVARA_ORDER — we read it from the source diagram. evidence: structures 4e p.037 (mūrchanā-maṇḍala), R_597_murchana_shift (sadja-position theorem), R_c39_murchana_devanagari (etymology + structure) """ table = MURCHANA_MANDALA[grāma] if not (0 <= k < len(table)): raise IndexError(f"k must be in [0, {len(table)}); got {k}") seq = table[k] return Mūrchanā( grāma=grāma, starting_svara=seq[0], sequence=seq, ) def all_murchanas(grāma: Grāma) -> list[Mūrchanā]: """Enumerate the 7 mūrchanās of a grāma. evidence: R_c57_grama, structures 4e p.037 """ return [murchana_at(grāma, k) for k in range(len(MURCHANA_MANDALA[grāma]))] def named_murchana(name: str) -> Mūrchanā | None: """Build a named mūrchanā if its starting svara is sourced. evidence: NAMED_MURCHANA_START dict (each entry has its own rule_id) Returns None if name is in the canonical 14-list but its start svara is not pinned by a rule (see UNRESOLVED). """ if name not in NAMED_MURCHANA_START: return None grāma, start = NAMED_MURCHANA_START[name] table = MURCHANA_MANDALA[grāma] for seq in table: if seq[0] == start: return Mūrchanā( grāma=grāma, starting_svara=start, sequence=seq, name=name, ) return None def sadava_from_sampurna(grāma: Grāma, omit: Svara) -> Tāna: """Derive a ṣāḍava (hexatonic) tāna by omitting one svara. Raises if `omit` is inomissible in `grāma`. evidence: sadjagrama_sadava_murchana_formation (aff#290, aff#2408), R_275_murchana_omission (aff#1925), madhyamagrama_rsabha_pancama_inomissible (aff#612, aff#1935) """ if not is_valid_omission(grāma, frozenset({omit})): raise ValueError( f"Omission of {omit.value} not permitted in {grāma.value}" ) svaras = frozenset(SVARA_ORDER) - {omit} return Tāna( parent_grāma=grāma, omitted=frozenset({omit}), svaras=svaras, scale_type=ScaleType.SADAVA, ) def auduva_from_sampurna( grāma: Grāma, omit_pair: frozenset[Svara] ) -> Tāna: """Derive an auduva (pentatonic) tāna by omitting a pair of svaras. Brihaddesi defines exactly 2 closed auduva sub-categories: devoid of ni+ga, and devoid of pa+ni. Other pairs are out of scope here. evidence: R_147_auduva_definition (aff#2309, aff#2426), R_1769_nigahina_auduva, R_5p3_1770, R_1786_01 """ if omit_pair not in AUDUVA_OMISSION_PAIRS: raise ValueError( f"Auduva omission pair {set(omit_pair)} not canonical " f"({{ni,ga}} or {{pa,ni}} only — R_147)" ) if not is_valid_omission(grāma, omit_pair): raise ValueError( f"At least one of {set(omit_pair)} is inomissible in {grāma.value}" ) svaras = frozenset(SVARA_ORDER) - omit_pair return Tāna( parent_grāma=grāma, omitted=omit_pair, svaras=svaras, scale_type=ScaleType.AUDUVA, ) def substitute_madhyama_for_sadja( phrase: list[Svara], position: int, placed_by_murchana: bool ) -> list[Svara]: """Where madhyama is placed in a phrase by virtue of the mūrchanā principle, ṣaḍja may replace it without destroying the jāti or rāga. evidence: murchana_substitution_madhyama_for_sadja (aff#2270, aff#2277) """ if not placed_by_murchana: return list(phrase) if phrase[position] != Svara.MA: return list(phrase) out = list(phrase) out[position] = Svara.SA return out # ============================================================================= # CONTRAINTES — validations # ============================================================================= def is_valid_omission(grāma: Grāma, omit: frozenset[Svara]) -> bool: """True iff none of the svaras in `omit` is inomissible in `grāma`. evidence: INOMISSIBLE_SVARAS (see rule citations on that dict) """ return not (omit & INOMISSIBLE_SVARAS[grāma]) def is_valid_mūrchanā(seq: Iterable[Svara]) -> bool: """A valid mūrchanā is a 7-svara permutation of the full svara set. evidence: R_c39_murchana_devanagari (length=7), murchana_definition_heptad_ascent_descent """ seq = tuple(seq) return len(seq) == 7 and set(seq) == set(SVARA_ORDER) def is_valid_tana_count(grāma: Grāma, scale: ScaleType, count: int) -> bool: """Cross-check a tāna count against the Brihaddesi enumerative facts. evidence: TANA_COUNTS dict (BD_6_1_R007, R_4p0_025, audavita_tanas_count_sadjagrama) Returns True if no count is known (under-constrained) — see UNRESOLVED. """ expected = TANA_COUNTS.get((grāma, scale)) if expected is None: return True return count == expected def grama_differentiated_by_pancama( g1: Grāma, g2: Grāma ) -> bool: """The two grāmas are differentiated on the basis of pañcama alone (specifically: pañcama's śruti position differs by one). evidence: R_319_gramas, madhyama_grama_mandalas_and_pancama_sruti """ return g1 != g2 and {g1, g2} == {Grāma.SADJA, Grāma.MADHYAMA} # ============================================================================= # 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": "gāndhāra-grāma", "reason": "alluded to in Indic tradition but no Brihaddesi 6b rule " "pins its svaras or operative role — only SADJA + MADHYAMA " "are modelled (R_c57_grama).", }, { "concept": "uttarāyatā / śuddhaṣaḍjā / matsarīkṛtā", "reason": "named in canonical 14-list (structures 4e p.051) but no " "individual rule isolates their starting svara.", }, { "concept": "ṣāḍava count in ṣaḍja-grāma", "reason": "BD_6_1_R007 gives 21 for madhyama-grāma; no isolated " "count for ṣaḍja-grāma ṣāḍava in the rule set.", }, { "concept": "jāti → mūrchanā mapping", "reason": "R_275_murchana_omission explicitly states: 'No rule is " "prescribed for assigning a particular mūrchanā to a " "given jāti' (aff#1933) — authority-based, not derivable.", }, { "concept": "shuddhakaisikamadhyama grāma assignment", "reason": "Disputed (R_486_shuddhakaisikamadhyama_grama_disputed); " "not modelled.", }, ) # ============================================================================= # Self-test (executed at import-time only if __main__) # ============================================================================= if __name__ == "__main__": # Sanity: every mūrchanā in the maṇḍala is a valid permutation for g in Grāma: for k in range(len(MURCHANA_MANDALA[g])): m = murchana_at(g, k) assert is_valid_mūrchanā(m.sequence), (g, k, m.sequence) assert len(all_murchanas(g)) == 7 # Sanity: named lookup m = named_murchana("aśvakrāntā") assert m is not None and m.starting_svara == Svara.MA assert m.grāma == Grāma.SADJA # Sanity: omission constraints assert is_valid_omission(Grāma.MADHYAMA, frozenset({Svara.SA})) assert not is_valid_omission(Grāma.MADHYAMA, frozenset({Svara.PA})) assert not is_valid_omission(Grāma.SADJA, frozenset({Svara.DHA})) # Sanity: sadava derivation t = sadava_from_sampurna(Grāma.SADJA, Svara.SA) assert t.scale_type == ScaleType.SADAVA assert Svara.SA not in t.svaras # Sanity: auduva derivation t2 = auduva_from_sampurna(Grāma.SADJA, frozenset({Svara.NI, Svara.GA})) assert t2.scale_type == ScaleType.AUDUVA assert {Svara.NI, Svara.GA}.isdisjoint(t2.svaras) # Sanity: tāna counts assert is_valid_tana_count(Grāma.MADHYAMA, ScaleType.SADAVA, 21) assert not is_valid_tana_count(Grāma.MADHYAMA, ScaleType.SADAVA, 22) # Sanity: substitution phrase = [Svara.SA, Svara.RI, Svara.MA, Svara.PA] out = substitute_madhyama_for_sadja(phrase, 2, placed_by_murchana=True) assert out[2] == Svara.SA print("melodic_derivations.py — all self-tests pass")