← domain #0
Source : jati_structure.py
data/library/books/brihaddesi_sharma_1992/formal_grammar/jati_structure.py · 1054 lines · 39627 bytes
"""Domaine 0 — jāti / svara-roles structural
Synthèse 6c.3 du Brihaddesi (Sharma 1992, vols I & II) depuis :
- 207 règles génératives 6b sur 170 concepts du domaine 0
- 488 affirmations sourcées
- partition Leiden domain_id=0
Concepts modelés (top-8 par poids opérationnel) :
- aṁśa (svara prédominant), graha (svara de départ), nyāsa (svara de conclusion),
apanyāsa (cadence intermédiaire), aniśa (non-aṁśa)
- jāti (abstraction mélodique, 18 jātis = 7 ṣaḍjagrāma + 11 madhyamagrāma)
- ṣāḍava (hexatonique par omission), auduvita (pentatonique par omission)
- kākalī (niṣāda altéré), antara (gāndhāra altéré)
- śuddhā / vikṛtā (formes pure / altérée)
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.
Les concepts mentionnés mais non formellement épinglés sont listés dans
UNRESOLVED en bas de fichier.
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 0)
# =============================================================================
# Types partagés avec melodic_derivations (domaine 3) — canonicalisés au niveau
# package par 6c.4. Import compat dual : relative quand chargé en tant que
# membre du package formal_grammar/, absolute quand le module est exécuté
# en standalone pour self-test.
try:
from .melodic_derivations import Svara, SVARA_ORDER, Grāma
except ImportError:
import os as _os, sys as _sys
_sys.path.insert(0, _os.path.dirname(__file__))
from melodic_derivations import Svara, SVARA_ORDER, Grāma # type: ignore
class SvaraRole(str, Enum):
"""Structural role of a svara within a jāti, bhāṣā or rāga.
Roles attested with rules in domain 0 :
- AMSA : svara prédominant (vādī, principal note)
- GRAHA : svara de départ de l'exécution
- NYASA : svara de conclusion
- APANYASA : cadence intermédiaire interne aux vidārīs
- ANISA : non-aṁśa, désignation explicite
- VINYASA / SAMNYASA : voir UNRESOLVED (mentionnés ailleurs mais
aucune règle 6b en domaine 0)
- PARYAYAMSA : aṁśa alternatif utilisé tour à tour
- KAKALI : niṣāda altéré (vikṛta)
- ANTARA : gāndhāra altéré (vikṛta), utilisé en ascension
- SADHARANA : svara "commun" (gāndhāra ou niṣāda)
evidence:
- AMSA : R_36_amsa_definition, R_4p0_012, amsa_definition_vadi_principal
- GRAHA : graha_definition, graha_63_varieties
- NYASA : R_006_nyasa_definition
- APANYASA : R_c17_apanyasa, R_4p0_005
- ANISA : R_4p1_151
- PARYAYAMSA : R_5p2_1417
- KAKALI : R_kakali_role, R_1853_001
- ANTARA : R_113_antara_identification
- SADHARANA : R_1789_sadharana_svaras
"""
AMSA = "amsa"
GRAHA = "graha"
NYASA = "nyasa"
APANYASA = "apanyasa"
ANISA = "anisa"
PARYAYAMSA = "paryayamsa"
KAKALI = "kakali"
ANTARA = "antara"
SADHARANA = "sadharana"
class JatiForm(str, Enum):
"""Two forms of a jāti, born by samavāya.
evidence: jati_suddha_vikrta_duality, R_42_jati_definition,
R_619_suddha_jati_definition, R_211_vikrta_transformation
"""
SUDDHA = "suddha" # pure / unaltered
VIKRTA = "vikrta" # altered / modified
class ScaleCardinality(str, Enum):
"""Density of a jāti's svara content.
evidence: r_brd_546_sampurna_def, R_shadava_def, R_5p3_1759,
shadava_state_lacks_rsabha, R_027_auduvita_definition
"""
SAMPURNA = "sampurna" # 7 svaras (complete)
SADAVA = "sadava" # 6 svaras (one omission)
AUDUVA = "auduva" # 5 svaras (two omissions)
# -----------------------------------------------------------------------------
# Comptages canoniques des jātis
# -----------------------------------------------------------------------------
# evidence: sadjagrama_seven_jatis, madhyamagrama_jati_basis, R_5p2_723
N_JATIS_PER_GRAMA: dict[Grāma, int] = {
Grāma.SADJA: 7, # sadjagrama_seven_jatis
Grāma.MADHYAMA: 11, # madhyamagrama_jati_basis, R_5p2_723
}
# 18 jātis total ; 63 aṁśas répartis ; 63 = nb total de variétés de jāti
# (1 variété par aṁśa-svara).
# evidence: R_1389_01 (18 jātis × amsa-svaras → 63 varietes),
# R_733_sixty_three_amsas (total amsas = 63),
# graha_63_varieties (graha has 63 varieties like aṁśa)
N_JATIS_TOTAL: int = 18 # R_1389_01
N_AMSA_TOTAL_ACROSS_JATIS: int = 63 # R_733_sixty_three_amsas, R_1389_01
N_GRAHA_VARIETIES: int = 63 # graha_63_varieties
N_SAMSARGAJA_VIKRTA: int = 11 # R_1416_samsargaja_vikrta_jatis
# -----------------------------------------------------------------------------
# Sets nominaux de jātis par grāma
# -----------------------------------------------------------------------------
# Bodies des règles 6b citent des sous-ensembles à 5 (jātis avec 5 svaras) ;
# nous gardons ces listes telles que citées, sans les promouvoir au statut
# de "liste complète des 7/11".
# evidence: R_214_raktagandhari_class (madhyamagrama 5-svara subset),
# sadjamadhyama_among_five_sadjagrama_jatis,
# nisadavati_arsabhi_five_svara_jatis
# 5 jātis basées dans ṣaḍjagrāma avec 5 svaras
# evidence: sadjamadhyama_among_five_sadjagrama_jatis,
# nisadavati_arsabhi_five_svara_jatis
SADJAGRAMA_FIVE_SVARA_JATIS: tuple[str, ...] = (
"niṣādavatī", "ārṣabhī", "dhaivatī", "ṣaḍjamadhyamā", "ṣaḍjodīcyavatī",
)
# 5 jātis basées dans madhyamagrāma (sous-ensemble cité par R_214)
# evidence: R_214_raktagandhari_class (body: madhyamagrama_jatis :=
# [gandhari, raktagandhari, madhyama, pancami, kaisiki])
MADHYAMAGRAMA_NAMED_JATIS: tuple[str, ...] = (
"gāndhārī", "raktagāndhārī", "madhyamā", "pañcamī", "kaiśikī",
)
# Sapta-svara-jātis : 7 jātis homonymes des 7 svaras (chacune avec son
# svara comme nyāsa). Deux formes : śuddhā et vikṛtā.
# evidence: svara_jati_nyasa_naming (svara_jati.count = 7, forms =
# {suddha, vikrta} ; nyāsa = own svara)
SVARA_JATI_COUNT: int = 7
SVARA_JATI_FORMS: tuple[JatiForm, ...] = (JatiForm.SUDDHA, JatiForm.VIKRTA)
# 3 jātis "sādhāraṇakṛtā" (utilisant antara + kākalī)
# evidence: sadharanakrta_three_jatis,
# sadharanakrta_uses_antara_kakali, R_4p1_419
SADHARANAKRTA_JATIS: tuple[str, ...] = ("madhyamā", "pañcamī", "ṣaḍjamadhyamā")
# -----------------------------------------------------------------------------
# Svaras altérés (sādhāraṇa svaras)
# -----------------------------------------------------------------------------
# evidence: R_1789_sadharana_svaras (sadharana_svaras := {gandhara, nisada})
SADHARANA_SVARAS: frozenset[Svara] = frozenset({Svara.GA, Svara.NI})
# Kākalī = niṣāda altéré ; Antara = gāndhāra altéré
# evidence: R_kakali_role, R_1853_001, R_113_antara_identification,
# gandhara_identified_as_antara_vikrta
KAKALI_BASE_SVARA: Svara = Svara.NI # R_kakali_role
ANTARA_BASE_SVARA: Svara = Svara.GA # R_113_antara_identification
# -----------------------------------------------------------------------------
# Spécifications des jātis (extraites verbatim des bodies de règles 6b)
# -----------------------------------------------------------------------------
# Pour chaque jāti, nous encodons UNIQUEMENT ce que les règles affirment
# explicitement. Les champs absents (None / set vide) sont à interpréter
# comme "non spécifié par une règle 6b", pas comme "vide".
@dataclass(frozen=True)
class JatiSpec:
"""Specification structurelle d'une jāti.
Chaque champ est sourcé par une règle 6b citée dans `evidence`.
"""
name: str
grāma: Grāma | None # None si transgrāma / disputé
amsas: frozenset[Svara] # peut être tous svaras
nyasas: frozenset[Svara] # 1 ou 2 svaras
apanyasas: frozenset[Svara]
grahas: frozenset[Svara]
anisas: frozenset[Svara] # non-aṁśas explicites
forbid_for_sadava: frozenset[Svara] # svaras non-omissibles pour ṣāḍava
forbid_for_auduva: frozenset[Svara] # svaras non-omissibles pour auduva
cardinality: ScaleCardinality | None
parents: frozenset[str] # jātis-mères en cas de dérivation
n_amsa_varieties: int | None # ex : ārṣabhī = 10, dhaivatī = 7
evidence: tuple[str, ...] # rule_ids
# Spécifications fournies UNIQUEMENT pour les jātis dont les bodies de règles
# donnent assez de détail (>= 2 champs structuraux explicites).
JATI_SPECS: dict[str, JatiSpec] = {
"kaiśikī": JatiSpec(
name="kaiśikī",
grāma=Grāma.MADHYAMA,
amsas=frozenset({Svara.MA}),
nyasas=frozenset({Svara.PA}),
# apanyāsas = seven svaras OR six excluding ṛṣabha (R_4p0_005)
apanyasas=frozenset(SVARA_ORDER) - {Svara.RI},
grahas=frozenset(),
anisas=frozenset({Svara.RI}),
forbid_for_sadava=frozenset(),
forbid_for_auduva=frozenset({Svara.DHA}),
cardinality=None,
parents=frozenset(),
n_amsa_varieties=None,
evidence=("R_4p0_002", "R_364_amsas"),
),
"gāndhārī": JatiSpec(
name="gāndhārī",
grāma=Grāma.MADHYAMA,
amsas=frozenset({Svara.GA}),
nyasas=frozenset({Svara.MA}),
apanyasas=frozenset(),
grahas=frozenset(),
anisas=frozenset({Svara.RI, Svara.DHA}),
forbid_for_sadava=frozenset(),
forbid_for_auduva=frozenset(),
cardinality=None,
parents=frozenset(),
n_amsa_varieties=None,
evidence=("R_4p0_003",),
),
"pañcamī": JatiSpec(
name="pañcamī",
grāma=Grāma.MADHYAMA,
amsas=frozenset(), # règle ne nomme pas l'aṁśa positif
nyasas=frozenset(),
apanyasas=frozenset({Svara.RI, Svara.PA, Svara.NI}),
grahas=frozenset(),
anisas=frozenset({Svara.RI, Svara.PA}),
forbid_for_sadava=frozenset(),
forbid_for_auduva=frozenset({Svara.RI}),
cardinality=None,
parents=frozenset(),
n_amsa_varieties=None,
evidence=("R_4p0_007",),
),
"madhyamā": JatiSpec(
name="madhyamā",
grāma=Grāma.MADHYAMA,
amsas=frozenset({Svara.PA}),
nyasas=frozenset(),
apanyasas=frozenset(),
grahas=frozenset(),
anisas=frozenset(),
forbid_for_sadava=frozenset(),
forbid_for_auduva=frozenset(),
cardinality=ScaleCardinality.SAMPURNA,
parents=frozenset(),
n_amsa_varieties=5, # R_4p0_012 : madhyamā count=5
evidence=("BD_6_1_R023", "R_4p0_012"),
),
"ṣaḍjamadhyamā": JatiSpec(
name="ṣaḍjamadhyamā",
grāma=Grāma.SADJA,
# "all svaras as aṁśa" — R_720_sadja_madhyama
amsas=frozenset(SVARA_ORDER),
nyasas=frozenset({Svara.SA, Svara.MA}),
apanyasas=frozenset(),
grahas=frozenset(),
anisas=frozenset(),
forbid_for_sadava=frozenset(),
forbid_for_auduva=frozenset(),
cardinality=None,
parents=frozenset(),
n_amsa_varieties=None,
evidence=("R_720_sadja_madhyama", "amsa_constraint_sadjamadhyama_sadava"),
),
"ṣāḍjī": JatiSpec(
name="ṣāḍjī",
grāma=Grāma.SADJA,
amsas=frozenset({Svara.SA}), # śuddhā jāti named after its aṁśa
nyasas=frozenset({Svara.SA}), # svara-jāti -> nyāsa = own svara
apanyasas=frozenset(),
grahas=frozenset({Svara.SA}),
anisas=frozenset(),
forbid_for_sadava=frozenset(),
forbid_for_auduva=frozenset(),
cardinality=None,
parents=frozenset(),
n_amsa_varieties=10, # R_4p0_012 : 1 śuddha + 5 vikṛta + 4 ṣāḍava
evidence=(
"sadji_shuddha_amsa_self_named",
"svara_jati_nyasa_naming",
"R_4p0_012",
),
),
"dhaivatī": JatiSpec(
name="dhaivatī",
grāma=Grāma.SADJA,
amsas=frozenset({Svara.RI, Svara.DHA}),
nyasas=frozenset({Svara.DHA}), # svara-jāti
apanyasas=frozenset({Svara.RI, Svara.DHA, Svara.MA}),
grahas=frozenset({Svara.RI, Svara.DHA}),
anisas=frozenset(),
forbid_for_sadava=frozenset(),
forbid_for_auduva=frozenset(),
cardinality=None,
parents=frozenset(),
n_amsa_varieties=7, # dhaivati_sevenfold_amsa_structure
evidence=(
"dhaivati_amsa_set",
"dhaivati_sevenfold_amsa_structure",
"dhaivati_grahas_amsas_structural",
"svara_jati_nyasa_naming",
),
),
"naiṣādī": JatiSpec(
name="naiṣādī",
grāma=None, # non spécifié par la règle
amsas=frozenset(),
nyasas=frozenset({Svara.NI}),
apanyasas=frozenset({Svara.NI, Svara.GA, Svara.RI}),
grahas=frozenset(),
anisas=frozenset(),
forbid_for_sadava=frozenset(),
forbid_for_auduva=frozenset(),
cardinality=None,
parents=frozenset(),
n_amsa_varieties=None,
evidence=("R_4p1_356",),
),
"ārṣabhī": JatiSpec(
name="ārṣabhī",
grāma=Grāma.SADJA,
amsas=frozenset({Svara.RI}), # svara-jāti (nyāsa = ṛṣabha)
nyasas=frozenset({Svara.RI}),
apanyasas=frozenset(),
grahas=frozenset(),
anisas=frozenset(),
forbid_for_sadava=frozenset(),
forbid_for_auduva=frozenset(),
cardinality=None,
parents=frozenset(),
n_amsa_varieties=10, # R_c752_arsabhi
evidence=("R_c752_arsabhi", "svara_jati_nyasa_naming"),
),
"raktagāndhārī": JatiSpec(
name="raktagāndhārī",
grāma=Grāma.MADHYAMA,
amsas=frozenset(SVARA_ORDER) - {Svara.DHA, Svara.RI},
nyasas=frozenset(),
apanyasas=frozenset({Svara.MA}),
grahas=frozenset(),
anisas=frozenset(),
forbid_for_sadava=frozenset({Svara.RI}), # ṣāḍava omits ṛṣabha
forbid_for_auduva=frozenset({Svara.RI, Svara.DHA}),
cardinality=None,
parents=frozenset({"gāndhārī", "madhyamā", "pañcamī", "naiṣādī"}),
n_amsa_varieties=5, # R_4p0_012 raktagāndhārī count=5
evidence=(
"R_raktagandhari_jati",
"R_214_raktagandhari_class",
"R_4p0_012",
),
),
"āndhrī": JatiSpec(
name="āndhrī",
grāma=None,
amsas=frozenset(),
nyasas=frozenset(),
apanyasas=frozenset(),
grahas=frozenset(),
anisas=frozenset({Svara.SA, Svara.MA, Svara.DHA}),
forbid_for_sadava=frozenset(),
forbid_for_auduva=frozenset(),
cardinality=None,
parents=frozenset({"ārṣabhī", "gāndhārī"}),
n_amsa_varieties=None,
evidence=("R_4p0_026",),
),
"kārmāravī": JatiSpec(
name="kārmāravī",
grāma=Grāma.MADHYAMA,
amsas=frozenset(),
nyasas=frozenset({Svara.PA}),
apanyasas=frozenset(),
grahas=frozenset(),
anisas=frozenset({Svara.SA, Svara.GA, Svara.MA}),
forbid_for_sadava=frozenset(),
forbid_for_auduva=frozenset(),
cardinality=ScaleCardinality.SAMPURNA,
parents=frozenset({"ārṣabhī", "gāndhārī", "pañcamī"}),
n_amsa_varieties=None,
evidence=(
"karmaravi_formation",
"karmaravi_non_amsas_and_nyasa",
"R_337_karmaravi_struct",
),
),
"nandayantī": JatiSpec(
name="nandayantī",
grāma=None,
amsas=frozenset({Svara.PA}),
nyasas=frozenset({Svara.GA}),
apanyasas=frozenset({Svara.MA, Svara.PA}),
grahas=frozenset({Svara.GA}),
anisas=frozenset(),
forbid_for_sadava=frozenset(),
forbid_for_auduva=frozenset(),
cardinality=None,
parents=frozenset({"ārṣabhī", "gāndhārī", "pañcamī"}),
n_amsa_varieties=None,
evidence=("R_224_nandayanti_structural", "R_c83_nandayanti"),
),
"sadjakaiśikī": JatiSpec(
name="sadjakaiśikī",
grāma=Grāma.SADJA,
amsas=frozenset({Svara.SA, Svara.GA, Svara.PA}),
nyasas=frozenset(),
apanyasas=frozenset(),
grahas=frozenset({Svara.SA, Svara.GA, Svara.PA}),
anisas=frozenset(),
forbid_for_sadava=frozenset(),
forbid_for_auduva=frozenset(),
cardinality=ScaleCardinality.SAMPURNA,
parents=frozenset(),
n_amsa_varieties=None,
evidence=("sadjakaisiki_structure",),
),
}
# =============================================================================
# TYPES — dataclasses opérationnelles
# =============================================================================
@dataclass(frozen=True)
class SvaraAssignment:
"""Affecte un rôle structural à un svara dans le contexte d'une jāti / bhāṣā.
evidence: amsa_assignment_per_bhasa (chaque bhāṣā/rāga a un aṁśa assigné,
distinct de son nyāsa)
"""
svara: Svara
role: SvaraRole
@dataclass(frozen=True)
class JatiInstance:
"""Instance concrète d'une jāti, avec ses svaras et leurs rôles.
Une jāti est l'abstraction mélodique fondamentale (R_42_jati_definition).
Elle a deux formes : śuddhā (où graha=apanyāsa=aniśa, nyāsa en mandra,
completion) et vikṛtā (dérivée par altération).
evidence:
- R_42_jati_definition (jāti = melodic_abstraction, kinds {suddha,vikrta})
- R_619_suddha_jati_definition (śuddhā = identical graha/apanyāsa/aniśa)
- R_211_vikrta_transformation (vikṛtā dérivée de śuddhā par altération
de aṁśa/apanyāsa/completeness ; INVARIANTS = nyāsa, alpatva/bahutva)
- R_1419_alpatva_bahutva_invariant
"""
name: str
form: JatiForm
grāma: Grāma | None
svaras: frozenset[Svara] # svaras présents
assignments: tuple[SvaraAssignment, ...]
cardinality: ScaleCardinality
def __post_init__(self) -> None:
n = len(self.svaras)
if n not in (5, 6, 7):
raise ValueError(
f"jāti {self.name}: svara count must be 5/6/7, got {n}"
)
if n == 7 and self.cardinality != ScaleCardinality.SAMPURNA:
raise ValueError(
"7 svaras → cardinality must be SAMPURNA "
"— see r_brd_546_sampurna_def"
)
if n == 6 and self.cardinality != ScaleCardinality.SADAVA:
raise ValueError(
"6 svaras → cardinality must be SADAVA — see R_shadava_def"
)
if n == 5 and self.cardinality != ScaleCardinality.AUDUVA:
raise ValueError(
"5 svaras → cardinality must be AUDUVA — see R_5p3_1759"
)
for a in self.assignments:
if a.svara not in self.svaras:
raise ValueError(
f"assignment for {a.svara.value} not in jāti svaras "
f"— role {a.role.value} requires svara presence"
)
# ----- accesseurs par rôle -----
def svaras_with_role(self, role: SvaraRole) -> frozenset[Svara]:
return frozenset(a.svara for a in self.assignments if a.role == role)
@property
def amsas(self) -> frozenset[Svara]:
"""evidence: R_36_amsa_definition, R_4p0_012"""
return self.svaras_with_role(SvaraRole.AMSA)
@property
def grahas(self) -> frozenset[Svara]:
"""evidence: graha_definition"""
return self.svaras_with_role(SvaraRole.GRAHA)
@property
def nyasas(self) -> frozenset[Svara]:
"""evidence: R_006_nyasa_definition"""
return self.svaras_with_role(SvaraRole.NYASA)
@property
def apanyasas(self) -> frozenset[Svara]:
"""evidence: R_c17_apanyasa, R_4p0_005"""
return self.svaras_with_role(SvaraRole.APANYASA)
@property
def anisas(self) -> frozenset[Svara]:
"""evidence: R_4p1_151"""
return self.svaras_with_role(SvaraRole.ANISA)
# =============================================================================
# OPÉRATIONS — fonctions pures
# =============================================================================
def cardinality_of(n_svaras: int) -> ScaleCardinality:
"""Map a svara count to its cardinality category.
evidence: r_brd_546_sampurna_def (sampūrṇa = 7), R_shadava_def (6),
R_5p3_1759 (auduva = 5)
"""
if n_svaras == 7:
return ScaleCardinality.SAMPURNA
if n_svaras == 6:
return ScaleCardinality.SADAVA
if n_svaras == 5:
return ScaleCardinality.AUDUVA
raise ValueError(
f"cardinality undefined for {n_svaras} svaras "
"(only 5/6/7 attested) — see R_shadava_def, R_5p3_1759, "
"r_brd_546_sampurna_def"
)
def is_suddha_jati(j: JatiInstance) -> bool:
"""Test si une jāti est śuddhā.
Condition (R_619_suddha_jati_definition): graha == apanyāsa == aniśa
(identité des rôles structuraux) ET nyāsa en registre mandra ET
`is_complete` (= sampūrṇa).
Nous testons la partie observable depuis la JatiInstance : completion
sampūrṇa + identité graha/apanyāsa/aniśa (lorsque ces sets sont définis).
evidence: R_619_suddha_jati_definition, suddha_definition_and_giti_rank,
R_42_jati_definition
"""
if j.cardinality != ScaleCardinality.SAMPURNA:
return False
grahas, apanyas, anisas = j.grahas, j.apanyasas, j.anisas
if grahas and apanyas and anisas:
# all three sets must coincide
return grahas == apanyas == anisas
# If any role is not assigned in this instance, we cannot positively
# confirm śuddhā — return False conservatively.
return False
def is_valid_omission_for_sadava(jati_name: str, omit: Svara) -> bool:
"""Test si l'omission d'un svara pour former ṣāḍava est admise dans
cette jāti.
Une jāti admet une omission ssi le svara n'est pas dans `forbid_for_sadava`
et n'est pas un aṁśa (R_36_amsa_murchana_relation : l'aṁśa ne s'omet
pas, par construction).
evidence: R_c25_sadava (samvādī de l'aṁśa non omissible),
amsa_samvadi_cannot_be_omitted,
R_42_jati_destruction_vivadin
"""
spec = JATI_SPECS.get(jati_name)
if spec is None:
raise KeyError(
f"jāti {jati_name} unspecified — see JATI_SPECS and UNRESOLVED"
)
if omit in spec.forbid_for_sadava:
return False
if omit in spec.amsas:
return False
return True
def is_valid_omission_for_auduva(
jati_name: str, omit_pair: frozenset[Svara]
) -> bool:
"""Idem pour auduva : aucune des deux svaras omises ne doit être
dans `forbid_for_auduva` ni dans les aṁśas.
evidence: R_027_auduvita_definition (auduva = devoid of ṛṣabha+dhaivata
when prescribed), R_143_auduvita_state
"""
spec = JATI_SPECS.get(jati_name)
if spec is None:
raise KeyError(
f"jāti {jati_name} unspecified — see JATI_SPECS and UNRESOLVED"
)
if len(omit_pair) != 2:
raise ValueError("auduva omission must be a pair (size 2)")
if omit_pair & spec.forbid_for_auduva:
return False
if omit_pair & spec.amsas:
return False
return True
def derive_sadava_form(jati_name: str, omit: Svara) -> frozenset[Svara]:
"""Forme ṣāḍava (6 svaras) d'une jāti par omission d'un svara.
Lève ValueError si l'omission n'est pas valide.
evidence: R_c25_sadava (forme hexatonique par omission, contrainte
non-omission saṃvādī aṁśa), R_shadava_def,
shadava_state_lacks_rsabha
"""
if not is_valid_omission_for_sadava(jati_name, omit):
raise ValueError(
f"omission de {omit.value} interdite pour {jati_name} "
"— voir R_c25_sadava, amsa_samvadi_cannot_be_omitted"
)
return frozenset(SVARA_ORDER) - {omit}
def derive_auduva_form(
jati_name: str, omit_pair: frozenset[Svara]
) -> frozenset[Svara]:
"""Forme auduva (5 svaras) par omission d'une paire de svaras.
evidence: R_027_auduvita_definition, R_143_auduvita_state,
R_763_auduva_dhaivati (auduva of dhaivatī omits pa+sa)
"""
if not is_valid_omission_for_auduva(jati_name, omit_pair):
raise ValueError(
f"omission {{{', '.join(s.value for s in omit_pair)}}} "
f"interdite pour {jati_name} "
"— voir R_027_auduvita_definition, R_143_auduvita_state"
)
result = frozenset(SVARA_ORDER) - omit_pair
if len(result) != 5:
raise ValueError("auduva form must yield exactly 5 svaras")
return result
def assign_kakali_role(jati_name: str, svara: Svara) -> SvaraRole | None:
"""Détermine le rôle "altéré" (kākalī ou antara) d'un svara dans une jāti
sādhāraṇakṛtā.
- niṣāda → KAKALI (R_kakali_role, R_1853_001)
- gāndhāra → ANTARA (R_113_antara_identification)
- autres → None
Note : la liste explicite des rāgas où niṣāda devient kākalī (R_kakali_role)
n'est pas reproduite ici car ce sont des rāgas, pas des jātis.
evidence: R_kakali_role, R_1853_001, R_113_antara_identification,
sadharanakrta_uses_antara_kakali, R_4p1_419
"""
if jati_name not in SADHARANAKRTA_JATIS:
return None
if svara == KAKALI_BASE_SVARA:
return SvaraRole.KAKALI
if svara == ANTARA_BASE_SVARA:
return SvaraRole.ANTARA
return None
def amsa_predecessor(amsa: Svara) -> Svara:
"""Le svara immédiatement en dessous d'un aṁśa est le mandra de cette jāti.
evidence: amsa_predecessor_is_mandra, amsa_role_and_mandra_relation
"""
idx = SVARA_ORDER.index(amsa)
return SVARA_ORDER[(idx - 1) % len(SVARA_ORDER)]
def total_amsas_across_jatis() -> int:
"""Total canonique des aṁśas à travers les 18 jātis = 63.
evidence: R_733_sixty_three_amsas, R_1389_01
"""
return N_AMSA_TOTAL_ACROSS_JATIS
# =============================================================================
# CONTRAINTES — validations
# =============================================================================
def validate_sadjamadhyama_amsa(amsa: Svara, n_svaras: int) -> None:
"""Ṣaḍjamadhyamā ne peut être hexatonique (6 svaras) quand niṣāda est aṁśa.
evidence: amsa_constraint_sadjamadhyama_sadava (aff#317)
"""
if amsa == Svara.NI and n_svaras == 6:
raise ValueError(
"Ṣaḍjamadhyamā interdite en 6 svaras quand niṣāda est aṁśa "
"— voir amsa_constraint_sadjamadhyama_sadava (aff#317)"
)
def validate_amsa_samvadi_present(
amsa: Svara, samvadi: Svara, present: frozenset[Svara]
) -> None:
"""Le saṃvādī d'un aṁśa ne peut pas être omis d'une jāti.
evidence: amsa_samvadi_cannot_be_omitted (R_c25_sadava recouvrant)
"""
if amsa not in present:
raise ValueError(
f"aṁśa {amsa.value} doit être présent — voir R_36_amsa_definition"
)
if samvadi not in present:
raise ValueError(
f"saṃvādī de l'aṁśa ({samvadi.value}) ne peut être omis "
"— voir amsa_samvadi_cannot_be_omitted"
)
def validate_jati_count_per_grama(grāma: Grāma, count: int) -> bool:
"""Vérifie qu'un comptage de jātis pour un grāma respecte les totaux
canoniques.
evidence: sadjagrama_seven_jatis (7), madhyamagrama_jati_basis (11),
R_5p2_723 (eleven madhyama jātis)
"""
return count == N_JATIS_PER_GRAMA[grāma]
def validate_vikrta_invariants(
suddha: JatiInstance, vikrta_candidate: JatiInstance
) -> None:
"""Une jāti vikṛtā dérive d'une śuddhā par altération de
aṁśa/apanyāsa/completeness, MAIS alpatva-bahutva et nyāsa sont
invariants.
evidence: R_211_vikrta_transformation, R_1419_alpatva_bahutva_invariant
"""
if vikrta_candidate.form != JatiForm.VIKRTA:
raise ValueError(
"candidat must have form=VIKRTA — voir R_211_vikrta_transformation"
)
if suddha.form != JatiForm.SUDDHA:
raise ValueError("source must have form=SUDDHA")
# nyāsa invariant
if suddha.nyasas and vikrta_candidate.nyasas:
if suddha.nyasas != vikrta_candidate.nyasas:
raise ValueError(
f"nyāsa doit être invariant dans la dérivation vikṛtā "
f"({suddha.nyasas} → {vikrta_candidate.nyasas}) "
"— voir R_1419_alpatva_bahutva_invariant"
)
# =============================================================================
# UNRESOLVED — concepts cités mais non formalisés par règle 6b en domaine 0
# =============================================================================
UNRESOLVED: tuple[dict[str, str], ...] = (
{
"concept": "saṁnyāsa / vinyāsa",
"reason": "rôles structuraux mentionnés dans la tradition (avec "
"graha/aṁśa/nyāsa/apanyāsa) mais aucune règle 6b en "
"domaine 0 ne les pose explicitement ; conservés dans "
"SvaraRole comme placeholder commenté mais sans "
"assignation opérationnelle.",
},
{
"concept": "liste complète des 7 jātis ṣaḍjagrāma",
"reason": "sadjagrama_seven_jatis affirme n=7 ; seuls 5 sont "
"nommés (sadjamadhyama_among_five_sadjagrama_jatis) ; "
"les 2 autres ne sont pas isolés par une règle 6b.",
},
{
"concept": "liste complète des 11 jātis madhyamagrāma",
"reason": "madhyamagrama_jati_basis + R_5p2_723 donnent n=11 ; "
"R_214 nomme 5 (gandhari/raktagandhari/madhyama/"
"pancami/kaisiki) ; les 6 autres (saṁsargajā vikṛtā) "
"ne sont pas individuellement isolées par règle 6b.",
},
{
"concept": "bahutva / alpatva exacts par svara",
"reason": "R_1419 affirme l'invariance, sadjakaisiki_structure et "
"R_143_auduvita_state donnent des cas, mais aucune "
"règle ne calcule alpatva/bahutva opérationnellement.",
},
{
"concept": "antaramārga (parcours intermédiaire)",
"reason": "mentionné par R_1416 (saṁsargajā vikṛtā jātis liés à "
"antara-mārga) et sadji_nishada_rsabha_not_paryayamsas, "
"mais aucune définition opérationnelle en domaine 0.",
},
{
"concept": "sangati exhaustif",
"reason": "sangati_attested_pairs liste des paires attestées mais "
"sangati_definition_concert dit qu'il est melodic_pur "
"et indépendant de samvāda — non-exhaustif.",
},
{
"concept": "śuddhakaiśika-madhyama grāma assignment",
"reason": "Disputé en domaine 3 (R_486) ; ici R_846_kaishiki_parent "
"dit né de kaiśikī + ṣaḍjamadhyamā, sans grāma fixe.",
},
)
# =============================================================================
# Self-test
# =============================================================================
if __name__ == "__main__":
# ------- Sanity : énumérations -------
assert len(SVARA_ORDER) == 7
assert N_JATIS_PER_GRAMA[Grāma.SADJA] + N_JATIS_PER_GRAMA[Grāma.MADHYAMA] \
== N_JATIS_TOTAL
assert SADHARANA_SVARAS == {Svara.GA, Svara.NI}
assert KAKALI_BASE_SVARA == Svara.NI
assert ANTARA_BASE_SVARA == Svara.GA
# ------- Sanity : cardinality -------
assert cardinality_of(7) == ScaleCardinality.SAMPURNA
assert cardinality_of(6) == ScaleCardinality.SADAVA
assert cardinality_of(5) == ScaleCardinality.AUDUVA
try:
cardinality_of(4)
except ValueError:
pass
else:
raise AssertionError("cardinality_of(4) should raise")
# ------- Sanity : amsa predecessor (mandra) -------
assert amsa_predecessor(Svara.MA) == Svara.GA
assert amsa_predecessor(Svara.SA) == Svara.NI # wrap-around
# ------- Sanity : kākalī / antara -------
assert assign_kakali_role("madhyamā", Svara.NI) == SvaraRole.KAKALI
assert assign_kakali_role("madhyamā", Svara.GA) == SvaraRole.ANTARA
assert assign_kakali_role("madhyamā", Svara.SA) is None
# non-sādhāraṇakṛtā → no altered roles
assert assign_kakali_role("kaiśikī", Svara.NI) is None
# ------- Sanity : validate_sadjamadhyama_amsa -------
validate_sadjamadhyama_amsa(Svara.SA, 6) # OK
validate_sadjamadhyama_amsa(Svara.NI, 7) # OK
try:
validate_sadjamadhyama_amsa(Svara.NI, 6)
except ValueError:
pass
else:
raise AssertionError("should reject Ṣaḍjamadhyamā 6sv with NI as aṁśa")
# ------- Sanity : validate_amsa_samvadi_present -------
validate_amsa_samvadi_present(
Svara.SA, Svara.PA, frozenset({Svara.SA, Svara.PA, Svara.MA})
)
try:
validate_amsa_samvadi_present(
Svara.SA, Svara.PA, frozenset({Svara.SA, Svara.MA})
)
except ValueError:
pass
else:
raise AssertionError("missing saṃvādī should raise")
# ------- Sanity : validate_jati_count_per_grama -------
assert validate_jati_count_per_grama(Grāma.SADJA, 7)
assert validate_jati_count_per_grama(Grāma.MADHYAMA, 11)
assert not validate_jati_count_per_grama(Grāma.SADJA, 6)
# ------- Sanity : derive_sadava_form / derive_auduva_form -------
# raktagāndhārī : aṁśas exclude {DHA, RI}, forbid_for_sadava = {RI}
# → can omit DHA (not aṁśa, not forbidden)
form = derive_sadava_form("raktagāndhārī", Svara.DHA)
assert len(form) == 6
assert Svara.DHA not in form
# → cannot omit RI (forbidden) or any of the amsas
try:
derive_sadava_form("raktagāndhārī", Svara.RI)
except ValueError:
pass
else:
raise AssertionError("RI omission in raktagāndhārī should fail")
try:
derive_sadava_form("raktagāndhārī", Svara.SA) # SA is aṁśa
except ValueError:
pass
else:
raise AssertionError("omitting aṁśa SA should fail")
# auduva pair {RI, DHA} : RI is forbidden for raktagāndhārī sādhāva,
# and forbid_for_auduva = {RI, DHA} both forbidden; expect failure
try:
derive_auduva_form("raktagāndhārī", frozenset({Svara.RI, Svara.DHA}))
except ValueError:
pass
else:
raise AssertionError("auduva {RI,DHA} in raktagāndhārī should fail "
"(both forbidden)")
# But pañcamī : amsas = ∅ (unset), forbid_for_auduva = {RI}, so {NI, MA}
# should pass (neither RI nor an aṁśa).
pair = frozenset({Svara.NI, Svara.MA})
assert is_valid_omission_for_auduva("pañcamī", pair)
f5 = derive_auduva_form("pañcamī", pair)
assert len(f5) == 5
# ------- Sanity : JatiInstance construction & role accessors -------
inst = JatiInstance(
name="ṣaḍjamadhyamā-instance",
form=JatiForm.SUDDHA,
grāma=Grāma.SADJA,
svaras=frozenset(SVARA_ORDER),
assignments=(
SvaraAssignment(Svara.SA, SvaraRole.NYASA),
SvaraAssignment(Svara.MA, SvaraRole.NYASA),
SvaraAssignment(Svara.SA, SvaraRole.AMSA),
SvaraAssignment(Svara.RI, SvaraRole.AMSA),
SvaraAssignment(Svara.GA, SvaraRole.AMSA),
SvaraAssignment(Svara.MA, SvaraRole.AMSA),
SvaraAssignment(Svara.PA, SvaraRole.AMSA),
SvaraAssignment(Svara.DHA, SvaraRole.AMSA),
SvaraAssignment(Svara.NI, SvaraRole.AMSA),
),
cardinality=ScaleCardinality.SAMPURNA,
)
assert inst.nyasas == {Svara.SA, Svara.MA}
assert inst.amsas == frozenset(SVARA_ORDER)
# ------- Sanity : JatiInstance bad construction -------
try:
JatiInstance(
name="bad", form=JatiForm.SUDDHA, grāma=None,
svaras=frozenset({Svara.SA, Svara.RI, Svara.GA}), # 3 → invalid
assignments=(),
cardinality=ScaleCardinality.AUDUVA,
)
except ValueError:
pass
else:
raise AssertionError("3-svara jāti should fail validation")
try:
JatiInstance(
name="mismatch",
form=JatiForm.SUDDHA, grāma=None,
svaras=frozenset(SVARA_ORDER),
assignments=(),
cardinality=ScaleCardinality.SADAVA, # mismatch with 7 svaras
)
except ValueError:
pass
else:
raise AssertionError("cardinality/count mismatch should fail")
# assignment for absent svara
try:
JatiInstance(
name="absent",
form=JatiForm.SUDDHA, grāma=None,
svaras=frozenset({Svara.SA, Svara.RI, Svara.GA, Svara.MA, Svara.PA}),
assignments=(SvaraAssignment(Svara.NI, SvaraRole.AMSA),),
cardinality=ScaleCardinality.AUDUVA,
)
except ValueError:
pass
else:
raise AssertionError("assignment for absent svara should fail")
# ------- Sanity : vikṛtā invariants -------
suddha = JatiInstance(
name="ṣāḍjī-śuddhā", form=JatiForm.SUDDHA, grāma=Grāma.SADJA,
svaras=frozenset(SVARA_ORDER),
assignments=(SvaraAssignment(Svara.SA, SvaraRole.NYASA),),
cardinality=ScaleCardinality.SAMPURNA,
)
vikrta_ok = JatiInstance(
name="ṣāḍjī-vikṛtā", form=JatiForm.VIKRTA, grāma=Grāma.SADJA,
svaras=frozenset(SVARA_ORDER) - {Svara.NI},
assignments=(SvaraAssignment(Svara.SA, SvaraRole.NYASA),),
cardinality=ScaleCardinality.SADAVA,
)
validate_vikrta_invariants(suddha, vikrta_ok)
vikrta_bad = JatiInstance(
name="ṣāḍjī-bad-vikṛtā", form=JatiForm.VIKRTA, grāma=Grāma.SADJA,
svaras=frozenset(SVARA_ORDER) - {Svara.NI},
assignments=(SvaraAssignment(Svara.MA, SvaraRole.NYASA),), # changed
cardinality=ScaleCardinality.SADAVA,
)
try:
validate_vikrta_invariants(suddha, vikrta_bad)
except ValueError:
pass
else:
raise AssertionError("nyāsa-changed vikṛtā should fail")
# ------- Sanity : is_suddha_jati -------
# ṣaḍja-madhyamā with graha=apanyāsa=aniśa all equal triggers SUDDHA test
inst2 = JatiInstance(
name="test-suddha", form=JatiForm.SUDDHA, grāma=Grāma.SADJA,
svaras=frozenset(SVARA_ORDER),
assignments=(
SvaraAssignment(Svara.SA, SvaraRole.GRAHA),
SvaraAssignment(Svara.SA, SvaraRole.APANYASA),
SvaraAssignment(Svara.SA, SvaraRole.ANISA),
SvaraAssignment(Svara.NI, SvaraRole.NYASA),
),
cardinality=ScaleCardinality.SAMPURNA,
)
assert is_suddha_jati(inst2)
# mismatched sets → not śuddhā
inst3 = JatiInstance(
name="test-not-suddha", form=JatiForm.SUDDHA, grāma=Grāma.SADJA,
svaras=frozenset(SVARA_ORDER),
assignments=(
SvaraAssignment(Svara.SA, SvaraRole.GRAHA),
SvaraAssignment(Svara.MA, SvaraRole.APANYASA), # differs
SvaraAssignment(Svara.SA, SvaraRole.ANISA),
),
cardinality=ScaleCardinality.SAMPURNA,
)
assert not is_suddha_jati(inst3)
# ------- Sanity : total_amsas -------
assert total_amsas_across_jatis() == 63
assert N_GRAHA_VARIETIES == 63
# ------- Sanity : JATI_SPECS coverage -------
assert "kaiśikī" in JATI_SPECS
assert JATI_SPECS["raktagāndhārī"].n_amsa_varieties == 5
assert JATI_SPECS["ārṣabhī"].n_amsa_varieties == 10
# āndhrī's parents are {ārṣabhī, gāndhārī} (R_4p0_026)
assert JATI_SPECS["āndhrī"].parents == {"ārṣabhī", "gāndhārī"}
print("jati_structure — all self-tests pass")