← domain #8

Source : dhruva_natya.py

data/library/books/brihaddesi_sharma_1992/formal_grammar/dhruva_natya.py · 747 lines · 27822 bytes
"""Domaine 8 — dhruvā / gītaka / pūrvaranga (musique théâtrale) Synthèse 6c.3 du Brihaddesi (Sharma 1992, vols I & II) depuis : - 27 règles génératives 6b (domain_id=8) - 45 affirmations sourcées - 35 concepts du cluster Leiden domain_id=8 Domaine = la couche théâtrale / dramatique du Brihaddesi : ce que Mātaṅga emprunte au Nāṭyaśāstra de Bharata pour articuler le chant (dhruvā), les sept gītakas, le pūrvaranga (prélude rituel) et leurs applications (viniyoga) à des actes dramatiques précis. 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 # ============================================================================= # CONSTANTES — énumérations fermées du Brihaddesi (domaine 8) # ============================================================================= class DhruvaType(str, Enum): """The five canonical types of dhruvā in dramatic music. The list is closed: Mātaṅga cites it from Bharata (NS). evidence: dhruvas_five_types (aff#1889), R_759_naiskramiki_exit_dhruva """ PRAVESIKI = "praveśikī" # entry-song PRASADIKI = "prāsādikī" # calming AKSEPIKI = "ākṣepikī" # interrupting SANTARA = "sāntarā" # with-interval NAISKRAMIKI = "naiṣkrāmikī" # exit-song class DhruvaScale(str, Enum): """Fourfold density of svara-content available to a dhruvā. "Fourfold usage of dhruvās (viz. complete, hexatonic, pentatonic, quadratonic)" — aff#192 (vol_II_p018). evidence: dhruva_fourfold_usage (aff#192) """ SAMPURNA = "sampurna" # full 7 svaras SADAVA = "sadava" # 6 svaras (hexatonic) AUDUVA = "auduva" # 5 svaras (pentatonic) QUADRATONIC = "quadratonic" # 4 svaras class GitakaType(str, Enum): """The seven gītakas (NS XXXI) — closed enumeration. evidence: R_2220_gitakas (aff#3137), R_495_gitaka (aff#2649) """ MADRAKA = "madraka" APARANTAKA = "aparāntaka" ULLOPYAKA = "ullopyaka" PRAKARI = "prakarī" OVENAKA = "oveṇaka" ROVINDAKA = "rovindaka" UTTARA = "uttara" class AsaritaType(str, Enum): """The three āsārita sub-types (a pūrvaranga genre outside the seven gītakas, important for its tāla patterns and dance). evidence: r_brd_226_asarita_types (aff#1881), aff#3144 (NS-external importance) """ JYESTHA = "jyeṣṭha" # major MADHYA = "madhya" # middle KANISTHA = "kaniṣṭha" # minor class PurvarangaForm(str, Enum): """Performance forms classed as part of pūrvaranga (preliminary rite) but distinct from the seven gītakas. Mātaṅga isolates āsārita and vardhamāna as "outside the seven gītakas but very important due to special tāla patterns and association with dance" (aff#3144, aff#3145). evidence: R_227_vardhamāna (aff#288), r_brd_226_asarita_types (aff#1881), gitakas_purvaranga_alankara_stretching (aff#3138) """ SEVEN_GITAKAS = "seven_gitakas" ASARITA = "asarita" VARDHAMANA = "vardhamana" class PurvarangaJati(str, Enum): """Jāti-class prescribed inside pūrvaranga. "śuddhā jātis should be used by song organizers" (aff#286); "śuddhaṣāḍava should be used in the pūrvaraṅga" (aff#593); "In pūrvaraṅga, the śuddhā should be there" (aff#854). evidence: R_225_purvaranga_jati_use (aff#286, aff#593, aff#854) """ SUDDHA = "suddha" # pure jāti SUDDHASADAVA = "suddhasadava" # pure hexatonic class Trideva(str, Enum): """The three deities of the Trideva, each tied to a phase of cosmic manifestation and to a chapter-role in the Nāṭyaśāstra. evidence: trideva_three_aspects_of_reality (aff#2911), R_2009_01 (aff#2922), R_1391_natyaveda_attribution (aff#1820) """ BRAHMA = "brahma" # creation; NS chapter I (creates Nāṭyaveda) VISHNU = "vishnu" # preservation; NS chapter XX (four vṛttis) MAHESHVARA = "maheshvara" # destruction; via Taṇḍu, dance — NS chapter IV # ----------------------------------------------------------------------------- # Closed list of named dhruvās by dramatic situation (viniyoga) # ----------------------------------------------------------------------------- # Mapping dhruvā-type → primary dramatic-situation tag, sourced verbatim. # evidence: R_759_naiskramiki_exit_dhruva (aff#365), # praveshiki_bhinnashadja_application (aff#645), # dhruvas_five_types (aff#1889 — enumeration only). DHRUVA_SITUATION: dict[DhruvaType, str] = { DhruvaType.PRAVESIKI: "hero_entry", # aff#645 DhruvaType.NAISKRAMIKI: "exit_first_scene", # aff#365 # prāsādikī / ākṣepikī / sāntarā: named in aff#1889 but no individual # situation-rule found in the Brihaddesi 6b set — see UNRESOLVED. } # ----------------------------------------------------------------------------- # Closed list of jāti → dramatic-act assignments (Mātaṅga's viniyoga rules) # ----------------------------------------------------------------------------- # All three Brihaddesi rules of this form found by 6b. Each row cites one # affirmation_id. # evidence: dhruva_jati_act_assignment (aff#419, aff#456, aff#466), # R_782_01 (aff#447) JATI_ACT_VINIYOGA: dict[str, int] = { "gāndhārī": 3, # aff#419 — "ध्रुवागाने तृतीयप्रेक्षणके विनियोगः" "raktagāndhārī": 3, # aff#456 — "the third act" "kaiśikī": 5, # aff#466 — "the fifth act" "gāndhārodīcyavā": 4, # aff#447 — "the dhruvā song of the fourth act" } # ----------------------------------------------------------------------------- # Specific jāti-prescription for prāveśikī dhruvā # ----------------------------------------------------------------------------- # evidence: praveshiki_bhinnashadja_application (aff#645) PRAVESIKI_JATI_REQUIRED: str = "bhinna-ṣaḍja" # ----------------------------------------------------------------------------- # Alaṅkāras explicitly forbidden in dhruvā performance # ----------------------------------------------------------------------------- # "Syena, bindu and similar ornaments involving stretching should not be # used in dhruvā performance according to their own measure" (aff#2645). # "Excessive stretching is avowedly undesirable in dhruvās" (aff#2862). # evidence: dhruva_alankara_restrictions (aff#2645), # dhruvas_no_excessive_stretch (aff#2862, aff#3139), # dhruva_alankara_restrictions (aff#2646 — prefer ascending svaras) FORBIDDEN_ALANKARAS_IN_DHRUVA: frozenset[str] = frozenset({ "syena", "bindu", }) DHRUVA_PREFERRED_DIRECTION: str = "ascending" # aff#2646 # ----------------------------------------------------------------------------- # Pūrvaranga structural components # ----------------------------------------------------------------------------- # evidence: purvaranga_definition (aff#1899) PURVARANGA_COMPONENTS: tuple[str, ...] = ( "instrumental", "song", "dance", "dialogue", ) PURVARANGA_VALUE_DIMENSIONS: tuple[str, ...] = ( "ritualistic", "psychological", ) # ----------------------------------------------------------------------------- # brahma-granthi — fixed cosmological-physiological constant # ----------------------------------------------------------------------------- # "Brahma-granthi is the name of an energy centre in the human body # situated below the navel" (aff#2917). # "vital air is propelled from the seat of energy in the body known as # brahmagranthi … producing low and high sounds" (aff#2881). # evidence: R_523_brahma_granthi_def (aff#2112, aff#2917), # R_548_brahmagranthi (aff#2881) BRAHMA_GRANTHI_LOCATION: str = "below_navel" BRAHMA_GRANTHI_FUNCTION: str = "propels_vital_air_producing_sounds" # ----------------------------------------------------------------------------- # Nāṭyaśāstra → chapter / deity attribution # ----------------------------------------------------------------------------- # evidence: R_2009_01 (aff#2922), R_1391_natyaveda_attribution (aff#1820) # Three datapoints, each citing its own affirmation. NS_CHAPTER_ATTRIBUTION: dict[int, tuple[Trideva, str]] = { 1: (Trideva.BRAHMA, "creates_natyaveda"), # aff#2922 4: (Trideva.MAHESHVARA, "dance_added_via_tandu"), # aff#2922 20: (Trideva.VISHNU, "acts_in_four_vrittis"), # aff#2922 } # Nāṭya-Veda authorship + division of labour (Śiva = dance ; Brahmā = vāk). # evidence: R_1391_natyaveda_attribution (aff#1819, aff#1820) NATYAVEDA_CONTRIBUTION: dict[str, str] = { "brahma": "vak_based_elaboration", # aff#1820 "siva": "dance_added_to_theatre", # aff#1820 } # ============================================================================= # TYPES — dataclasses # ============================================================================= @dataclass(frozen=True) class Dhruva: """A dhruvā: metrical text performed in pūrvaranga and within drama, designed to communicate textual meaning to the listener. "Dhruvā is a metrical text that is performed in pūrvaranga" (aff#1928); "dhruvā that is formed in accordance with the (desired) meaning" (aff#2647); "dhruvā is the sounded one" (aff#2741). evidence: dhruva_definition_metrical_text (aff#1928, aff#2647, aff#2741), dhruvas_five_types (aff#1889), dhruva_fourfold_usage (aff#192) """ type: DhruvaType scale: DhruvaScale language: str # "sanskrit" / "prakrta" / "prescribed_syllables" text: str # the metrical text content (opaque here) is_avakrsta: bool = False # avakṛṣṭā sub-form (aff#189, aff#1861) alankaras_used: tuple[str, ...] = () def __post_init__(self) -> None: # evidence: dhruva_definition_metrical_text — language constraint if self.language not in ("sanskrit", "prakrta", "prescribed_syllables"): raise ValueError( f"dhruvā language must be sanskrit/prakrta/prescribed_syllables " f"(R_dhruva_definition_metrical_text aff#1928), got {self.language!r}" ) # evidence: dhruva_alankara_restrictions (aff#2645) bad = set(self.alankaras_used) & FORBIDDEN_ALANKARAS_IN_DHRUVA if bad: raise ValueError( f"forbidden alaṅkāras in dhruvā: {bad} " f"(dhruva_alankara_restrictions aff#2645)" ) @dataclass(frozen=True) class AvakrstaDhruva: """A sub-form of dhruvā explicitly described in NS V (aff#1861). Special because svarāntara applies *only* here (aff#189). evidence: R_712_svarantara_scope (aff#189), avakrsta_described_in_NS_V (aff#1861), dhruva_avakrsta_treatment_reference (aff#190) """ base: Dhruva has_svarantara: bool = True def __post_init__(self) -> None: if not self.base.is_avakrsta: raise ValueError( "AvakrstaDhruva requires base.is_avakrsta=True " "(R_712_svarantara_scope aff#189)" ) @dataclass(frozen=True) class Gitaka: """One of the seven gītakas prescribed in pūrvaranga. Each gītaka stretches text-units through alaṅkāras (aff#3138, aff#2649). evidence: R_2220_gitakas (aff#3137, aff#3138), R_495_gitaka (aff#2649) """ type: GitakaType in_purvaranga: bool = True # aff#3138 — all seven are prescribed there @dataclass(frozen=True) class Asarita: """A pūrvaranga form outside the seven gītakas, with three sub-types. evidence: r_brd_226_asarita_types (aff#1881), aff#3144 (status: NS-external but important; dance-associated) """ type: AsaritaType associated_with_dance: bool = True # aff#3144 inside_seven_gitakas: bool = False # aff#3144 ("outside the seven gitakas") @dataclass(frozen=True) class Vardhamana: """Pūrvaranga form (also NS-external) where the formation/variation of aṁśas of svaras in jātis is to be performed. evidence: R_227_vardhamana (aff#288), aff#3145 (NS-external importance) """ performs_amsa_kalpana: bool = True associated_with_dance: bool = True # aff#3145 inside_seven_gitakas: bool = False # aff#3145 @dataclass(frozen=True) class Purvaranga: """The preliminary ritual section before the drama proper: a composite of instrumental rendering, song, dance and dialogue, carrying both ritualistic and psychological value (aff#1899). evidence: purvaranga_definition (aff#1899), R_225_purvaranga_jati_use (aff#286, aff#593, aff#854), R_2220_gitakas (aff#3138 — seven gītakas prescribed inside) """ components: tuple[str, ...] = PURVARANGA_COMPONENTS value_dimensions: tuple[str, ...] = PURVARANGA_VALUE_DIMENSIONS jati_class_used: PurvarangaJati = PurvarangaJati.SUDDHA forms_present: tuple[PurvarangaForm, ...] = ( PurvarangaForm.SEVEN_GITAKAS, PurvarangaForm.ASARITA, PurvarangaForm.VARDHAMANA, ) def __post_init__(self) -> None: # evidence: R_225_purvaranga_jati_use (aff#286, aff#593, aff#854) if self.jati_class_used not in ( PurvarangaJati.SUDDHA, PurvarangaJati.SUDDHASADAVA ): raise ValueError( f"pūrvaranga jāti must be śuddhā/śuddhaṣāḍava " f"(R_225_purvaranga_jati_use), got {self.jati_class_used}" ) @dataclass(frozen=True) class Viniyoga: """A "viniyoga" — the application of a musical element to a dramatic situation. Term borrowed from Pūrva-Mīmāmsā (Vedic mantra application). "Viniyoga is a term from Pūrva-Mīmāmsā" (aff#1888); "mention of a rasa in the viniyoga of a bhāṣā is a rare phenomenon" (aff#1965). evidence: R_c155_viniyoga (aff#1888, aff#1965), dhruva_jati_act_assignment (aff#419, aff#456, aff#466), R_782_01 (aff#447) """ musical_element: str # e.g. "gāndhārī", "bhinna-ṣaḍja" dramatic_context: str # e.g. "act_3", "hero_entry" rasa: str | None = None # rare; aff#1965 def __post_init__(self) -> None: if self.rasa is not None: # documented as "rare" but admissible — aff#1965 pass @dataclass(frozen=True) class BrahmaGranthi: """The brahma-granthi: energy centre situated below the navel, from which vital air is propelled to produce the speech-sounds that underlie melody (vāk). evidence: R_523_brahma_granthi_def (aff#2112, aff#2917), R_548_brahmagranthi (aff#2881), BD_6_1_R021 (aff#1821 — melody ← vāk) """ location: str = BRAHMA_GRANTHI_LOCATION function: str = BRAHMA_GRANTHI_FUNCTION # ============================================================================= # OPÉRATIONS — dérivations exécutables # ============================================================================= def select_dhruva_for_scene( *, is_first_scene: bool, is_exit: bool, is_hero_entry: bool ) -> DhruvaType | None: """Choose which dhruvā-type to deploy given a dramatic situation tag. Two situation-rules are sourced: - naiṣkrāmikī for the dhruvā of the *first scene* tied to exit (R_759_naiskramiki_exit_dhruva, aff#365) - prāveśikī for the hero's entry (praveshiki_bhinnashadja_application, aff#645) Returns None if no sourced rule matches — never invent a mapping for prāsādikī / ākṣepikī / sāntarā (UNRESOLVED). """ if is_first_scene and is_exit: return DhruvaType.NAISKRAMIKI # aff#365 if is_hero_entry: return DhruvaType.PRAVESIKI # aff#645 return None def jati_for_act(jati_name: str) -> int | None: """Return the dramatic-act in which a given jāti is prescribed for dhruvā singing, or None if no rule. evidence: dhruva_jati_act_assignment (aff#419, aff#456, aff#466), R_782_01 (aff#447) """ return JATI_ACT_VINIYOGA.get(jati_name) def jati_for_pravesiki() -> str: """Return the jāti prescribed for prāveśikī dhruvā at hero entry. evidence: praveshiki_bhinnashadja_application (aff#645) """ return PRAVESIKI_JATI_REQUIRED def enumerate_gitakas() -> list[Gitaka]: """Return the seven gītakas in canonical NS XXXI order. evidence: R_2220_gitakas (aff#3137) """ order = ( GitakaType.MADRAKA, GitakaType.APARANTAKA, GitakaType.ULLOPYAKA, GitakaType.PRAKARI, GitakaType.OVENAKA, GitakaType.ROVINDAKA, GitakaType.UTTARA, ) return [Gitaka(type=g, in_purvaranga=True) for g in order] def enumerate_dhruva_types() -> list[DhruvaType]: """Return the five canonical dhruvā types (closed list). evidence: dhruvas_five_types (aff#1889) """ return [ DhruvaType.PRAVESIKI, DhruvaType.PRASADIKI, DhruvaType.AKSEPIKI, DhruvaType.SANTARA, DhruvaType.NAISKRAMIKI, ] def enumerate_asarita_types() -> list[AsaritaType]: """Three āsārita sub-types. evidence: r_brd_226_asarita_types (aff#1881) """ return [AsaritaType.JYESTHA, AsaritaType.MADHYA, AsaritaType.KANISTHA] def ns_chapter_for_deity(deity: Trideva) -> tuple[int, str] | None: """Return (chapter_number, role) for a Trideva entry in the Nāṭyaśāstra. Only sourced rows are returned. evidence: R_2009_01 (aff#2922) """ for ch, (d, role) in NS_CHAPTER_ATTRIBUTION.items(): if d == deity: return ch, role return None def melody_origin() -> dict[str, str]: """Ontological fact: melody is a manifestation of the tonal aspect of vāk; vāk in turn is propelled from the brahma-granthi. evidence: BD_6_1_R021 (aff#1821), R_548_brahmagranthi (aff#2881) """ return { "melody": "manifestation_of(vak.tonal_aspect)", # aff#1821 "vak_source": f"brahma_granthi({BRAHMA_GRANTHI_LOCATION})", # aff#2881 } # ============================================================================= # CONTRAINTES — validations # ============================================================================= def is_valid_dhruva_alankara_set(alankaras: Iterable[str]) -> bool: """A dhruvā may not use any alaṅkāra in the forbidden set. evidence: dhruva_alankara_restrictions (aff#2645), dhruvas_no_excessive_stretch (aff#2862, aff#3139) """ return not (set(alankaras) & FORBIDDEN_ALANKARAS_IN_DHRUVA) def is_valid_purvaranga_jati(jati_class: PurvarangaJati) -> bool: """In pūrvaranga only śuddhā / śuddhaṣāḍava jātis are permitted. evidence: R_225_purvaranga_jati_use (aff#286, aff#593, aff#854) """ return jati_class in (PurvarangaJati.SUDDHA, PurvarangaJati.SUDDHASADAVA) def is_svarantara_admissible(d: Dhruva) -> bool: """svarāntara applies in avakṛṣṭā dhruvās *alone*. evidence: R_712_svarantara_scope (aff#189) """ return d.is_avakrsta def is_canonical_gitaka(name: str) -> bool: """A gītaka name is canonical iff present in the seven-member list. evidence: R_2220_gitakas (aff#3137) """ return name in {g.value for g in GitakaType} def is_canonical_dhruva_type(name: str) -> bool: """A dhruvā type is canonical iff present in the five-member list. evidence: dhruvas_five_types (aff#1889) """ return name in {d.value for d in DhruvaType} def is_consistent_viniyoga(v: Viniyoga) -> bool: """A viniyoga is structurally consistent if its musical element is one we have a sourced rule for, or its dramatic context tag is one of the sourced situation tags. Note: deliberately permissive — Mātaṅga's viniyoga set is open (aff#1888); we only check that *if* the element is in our closed jāti→act table, the act matches. evidence: R_c155_viniyoga (aff#1888), dhruva_jati_act_assignment (aff#419, aff#456, aff#466) """ expected_act = JATI_ACT_VINIYOGA.get(v.musical_element) if expected_act is None: return True # element outside our closed set — under-constrained return v.dramatic_context == f"act_{expected_act}" # ============================================================================= # UNRESOLVED — concepts mentionnés mais non formellement épinglés par 6b # ============================================================================= # Listés ici pour traçabilité ; consommés par le manifest JSON. UNRESOLVED: tuple[dict[str, str], ...] = ( { "concept": "prāsādikī / ākṣepikī / sāntarā situation-tag", "reason": "named in dhruvas_five_types (aff#1889) but no Brihaddesi " "6b rule isolates the specific dramatic situation for " "these three dhruvā types — only PRAVESIKI (aff#645) and " "NAISKRAMIKI (aff#365) have sourced situation rules.", }, { "concept": "dhruvā treatment chapter", "reason": "aff#1555 (vol_II_p133) states 'the treatment of dhruvā is " "lost in the text, leaving an unfulfilled announcement by " "the author' — i.e. the dedicated chapter is missing.", }, { "concept": "viniyoga of jātis in NS", "reason": "R_4p1_496 (aff#1894): the Nāṭyaśāstra itself does not " "explicitly discuss the viniyoga of jātis in dramatic " "music — we cannot derive a closed jāti→situation map " "from NS directly.", }, { "concept": "rasa in viniyoga of bhāṣā", "reason": "aff#1965 marks this as 'a rare phenomenon' — no closed " "rasa→bhāṣā table can be derived.", }, { "concept": "ध्रुवागाने / Gāndhārodīcyavā jāti category", "reason": "aff#447 places its viniyoga in the 4th-act dhruvā song " "but no rule classifies it among the standard śuddhā " "jātis — left as a free-form jāti name in " "JATI_ACT_VINIYOGA.", }, { "concept": "Mālavī kāku operative role", "reason": "R_1456_malavi_kaku_definition (aff#1910) defines it as a " "regional accent of Mālavā but no rule integrates it into " "the dhruvā/gītaka derivation set.", }, ) # ============================================================================= # Self-test (executed at import-time only if __main__) # ============================================================================= if __name__ == "__main__": # Sanity: closed enumerations assert len(enumerate_dhruva_types()) == 5 assert len(enumerate_gitakas()) == 7 assert len(enumerate_asarita_types()) == 3 # Sanity: each enum value is canonical for d in DhruvaType: assert is_canonical_dhruva_type(d.value) for g in GitakaType: assert is_canonical_gitaka(g.value) assert not is_canonical_dhruva_type("invented-type") assert not is_canonical_gitaka("invented-gitaka") # Sanity: dhruvā construction + alankāra validation ok = Dhruva( type=DhruvaType.PRAVESIKI, scale=DhruvaScale.SAMPURNA, language="sanskrit", text="hero enters", alankaras_used=(), ) assert ok.type == DhruvaType.PRAVESIKI # Sanity: forbidden alankāras raise try: Dhruva( type=DhruvaType.PRAVESIKI, scale=DhruvaScale.SAMPURNA, language="sanskrit", text="x", alankaras_used=("syena",), ) except ValueError: pass else: # pragma: no cover raise AssertionError("expected ValueError for forbidden alaṅkāra") # Sanity: illegal language raises try: Dhruva( type=DhruvaType.NAISKRAMIKI, scale=DhruvaScale.AUDUVA, language="english", text="x", ) except ValueError: pass else: # pragma: no cover raise AssertionError("expected ValueError for illegal language") # Sanity: alankāra set validation function assert is_valid_dhruva_alankara_set([]) assert is_valid_dhruva_alankara_set(["kampita"]) assert not is_valid_dhruva_alankara_set(["bindu"]) assert not is_valid_dhruva_alankara_set(["kampita", "syena"]) # Sanity: scene selection — sourced cases only assert select_dhruva_for_scene( is_first_scene=True, is_exit=True, is_hero_entry=False ) == DhruvaType.NAISKRAMIKI assert select_dhruva_for_scene( is_first_scene=False, is_exit=False, is_hero_entry=True ) == DhruvaType.PRAVESIKI # No fabrication for unsourced situations assert select_dhruva_for_scene( is_first_scene=False, is_exit=False, is_hero_entry=False ) is None # Sanity: jāti → act table assert jati_for_act("gāndhārī") == 3 assert jati_for_act("raktagāndhārī") == 3 assert jati_for_act("kaiśikī") == 5 assert jati_for_act("gāndhārodīcyavā") == 4 assert jati_for_act("nonexistent") is None # Sanity: prāveśikī jāti assert jati_for_pravesiki() == "bhinna-ṣaḍja" # Sanity: pūrvaranga jāti constraint assert is_valid_purvaranga_jati(PurvarangaJati.SUDDHA) assert is_valid_purvaranga_jati(PurvarangaJati.SUDDHASADAVA) # Sanity: pūrvaranga construction pr = Purvaranga() assert pr.jati_class_used == PurvarangaJati.SUDDHA assert "song" in pr.components and "dance" in pr.components assert PurvarangaForm.SEVEN_GITAKAS in pr.forms_present # Sanity: avakṛṣṭā / svarāntara base_ok = Dhruva( type=DhruvaType.SANTARA, scale=DhruvaScale.AUDUVA, language="prakrta", text="x", is_avakrsta=True, ) av = AvakrstaDhruva(base=base_ok) assert is_svarantara_admissible(av.base) base_not_av = Dhruva( type=DhruvaType.SANTARA, scale=DhruvaScale.AUDUVA, language="prakrta", text="x", is_avakrsta=False, ) assert not is_svarantara_admissible(base_not_av) try: AvakrstaDhruva(base=base_not_av) except ValueError: pass else: # pragma: no cover raise AssertionError("AvakrstaDhruva must require is_avakrsta=True") # Sanity: NS chapter / deity attribution assert ns_chapter_for_deity(Trideva.BRAHMA) == (1, "creates_natyaveda") assert ns_chapter_for_deity(Trideva.MAHESHVARA) == (4, "dance_added_via_tandu") assert ns_chapter_for_deity(Trideva.VISHNU) == (20, "acts_in_four_vrittis") # Sanity: melody origin mo = melody_origin() assert "vak" in mo["melody"] assert "below_navel" in mo["vak_source"] # Sanity: brahma-granthi defaults bg = BrahmaGranthi() assert bg.location == "below_navel" # Sanity: viniyoga consistency v_good = Viniyoga(musical_element="gāndhārī", dramatic_context="act_3") assert is_consistent_viniyoga(v_good) v_bad = Viniyoga(musical_element="gāndhārī", dramatic_context="act_5") assert not is_consistent_viniyoga(v_bad) # Open viniyoga (element not in closed table) → permissive v_open = Viniyoga(musical_element="some-bhāṣā", dramatic_context="any_ctx") assert is_consistent_viniyoga(v_open) # Sanity: gitaka enumeration order is canonical gs = enumerate_gitakas() assert gs[0].type == GitakaType.MADRAKA assert gs[-1].type == GitakaType.UTTARA assert all(g.in_purvaranga for g in gs) # Sanity: āsārita is NS-external (aff#3144) a = Asarita(type=AsaritaType.JYESTHA) assert a.associated_with_dance and not a.inside_seven_gitakas # Sanity: vardhamāna performs aṁśa-kalpanā (aff#288) vm = Vardhamana() assert vm.performs_amsa_kalpana and not vm.inside_seven_gitakas print("dhruva_natya.py — all self-tests pass")