vyakarana

Derive a word through Pāṇini’s grammar, match a word-by-word analysis to the written line, and name the sandhi rule at a join

Vyākaraṇa is Sanskrit grammar, and its foundation is Pāṇini’s Aṣṭādhyāyī, about 4,000 rules (sūtras) numbered by book, chapter and rule, such as 3.1.68. vidyut includes a program that applies these rules step by step, and this module uses it to show how a written word is derived. The second section matches a word-by-word analysis of a line to the line’s written words, and the third names the sandhi rule at the join of two words.

Specs and derivations

A pada is an inflected word. To ask vidyut for one, you describe it in a single string called a spec. A spec is written in SLP1 with its fields separated by |, and the first field says what kind of word it is:

s|<stem>|<liṅga>|<vibhakti>|<vacana>            a noun or adjective from a plain stem (a compound's stem written joined)
a|<text>                                        an indeclinable (avyaya)
k|<dhātu>|<kṛt>|<liṅga>|<vibhakti>|<vacana>     a noun from a root and a kṛt suffix, such as a participle
ka|<dhātu>|<kṛt>                                an indeclinable kṛt form (ktvā, lyap, tumun)
t|<dhātu>|<prayoga>|<lakāra>|<puruṣa>|<vacana>[|P|A]  a finite verb; P or A asks for active or middle endings

Liṅga is gender, vibhakti case, vacana number, prayoga voice, lakāra tense or mood, and puruṣa person, each given by vidyut’s SLP1 name. s|rAma|puM|praTamA|eka is rāmaḥ, masculine nominative singular. A dhātu (verbal root) is written <aupadeśika>,<gaṇa>,<antargaṇa>,<sanādi+…>,<upasarga+…>: the root as the Dhātupāṭha lists it, its verb class, its sub-class, any suffixes that make a new root such as the causative, and any prefixes, with unused fields left empty. t|BU,BvAdi,,,|kartari|la~w|praTama|eka|P is bhavati. dhatu turns such a field into a vidyut Dhatu, and made gives the stem vidyut builds from it: the root alone, with its prefixes set apart, or with a kṛt suffix the kṛdanta’s stem. made('Ra\\Sa~,divAdi') is naS, made('A\\px~,svAdi,,,pra', 'kta') is prApta, and a field vidyut cannot read gives an empty string.

pada turns a spec into a vidyut Pada, its noun stem made by stem, which gives a feminine in -ā or -ī its feminine suffix so sītā declines as sītāyai, not sītaiḥ, and derive returns its derivation, a vidyut Prakriya. When you also pass the written word as surf, derive returns only a derivation that ends in that word, or None, so a wrong spec gives nothing rather than some other word. derives(spec, surf) asks the same question and returns True or False. steps lists a derivation’s steps as (source, rule, result), each rule at most once while the form stays the same, with the source A for the Aṣṭādhyāyī, D for the Dhātupāṭha, U for the Uṇādipāṭha and V for a vārttika. norm prepares a form for comparison with a derivation by removing accents and writing a final anusvāra as m.

To describe a spec in words, spec_words gives its grammar in English (given the written form, a doer-voice verb reads middle when only the ātmanepada endings reach it, as kurute), spec_lemma gives the headword it stands on, and spec_root gives the root with its prefixes, such as pra+Ap. KRT and GRAM_EN hold the English names behind spec_words.

vidyut is imported inside the functions that need it, and only vidyut.prakriya is loaded, never the kosha or the segmenter. vy holds the one Vyakarana object, set to record every step.


source

vy

def vy():

Return the shared vidyut Vyakarana, which records each step of a derivation.


source

dhatu

def dhatu(
    f
):

The vidyut Dhatu a dhātu field upadeśa,gaṇa,antargaṇa,sanādi,prefixes names: BU,BvAdi is bhū.

assert dhatu('BU,BvAdi').aupadeshika == 'BU' and _dhatu is dhatu   # the old private name still imports

source

stem

def stem(
    t, g
):

Return the Pratipadika of stem t in gender g, a feminine in -ā or -ī carrying its feminine suffix (sītā, nadī) so vidyut declines it as one.


source

pada

def pada(
    spec
):

Turn a spec into a vidyut Pada, or None if the spec cannot be read.


source

norm

def norm(
    slp
):

Prepare an SLP1 form for comparison with a derivation by removing accents and marks and writing a final anusvāra as m.


source

derive

def derive(
    spec, surf:NoneType=None
):

Derive the word a spec names, returning the derivation that ends in surf (SLP1) when given, else the first, or None.


source

derives

def derives(
    spec, surf
):

source

steps

def steps(
    p
):

List a derivation’s steps as (source, rule, result), the source being A (Aṣṭādhyāyī), D (Dhātupāṭha), U (Uṇādipāṭha), V (vārttika) or another source’s own name.


source

made

def made(
    f, krt:str=''
):

The stem vidyut builds from dhātu field f, alone or with kṛt krt, or an empty string when it cannot.

assert made('Ra\\Sa~,divAdi') == 'naS' and made('A\\px~,svAdi,,,pra', 'kta') == 'prApta' and made('BU,nogana') == '' and _made is made

source

spec_lemma

def spec_lemma(
    spec
):

Return the SLP1 headword a spec stands on: the stem, the indeclinable, a kṛt form’s stem (prApta), or a verb’s root with its prefixes (pra+Ap).


source

spec_root

def spec_root(
    spec
):

Return the root of a kṛt form or a verb with its prefixes (pra+Ap), or ’’ for a noun from a plain stem.


source

spec_words

def spec_words(
    spec, surf:NoneType=None
):

Describe a spec’s grammar in English words: the kṛt suffix’s meaning if there is one, then case, number and gender, or tense, person, number and voice.

The checks below use two words with well-known derivations. Deriving रामः applies rules 4.1.2, 8.2.66 and 8.3.15, and deriving विजयते applies 3.4.78, 3.1.68, 7.3.84 and 6.1.78. The spec for rāmaḥ checked against the written form rāmeṇa gives None.

def codes(spec, surf): return [c for s, c, _ in steps(derive(spec, surf)) if s == 'A']
assert derive('s|rAma|puM|praTamA|eka', 'rAmaH').text == 'rAmaH'
assert {'4.1.2', '8.2.66', '8.3.15'} <= set(codes('s|rAma|puM|praTamA|eka', 'rAmaH'))
assert {'3.4.78', '3.1.68', '7.3.84', '6.1.78'} <= set(codes('t|ji\\,BvAdi,,,vi|kartari|la~w|praTama|eka', 'vijayate'))
assert derive('s|rAma|puM|praTamA|eka', 'rAmeRa') is None
assert derive('t|BU,BvAdi,,,|kartari|la~w|praTama|eka|P').text == 'Bavati'
assert spec_words('s|rAma|puM|tftIyA|eka') == ['instrumental', 'singular', 'masculine']
assert spec_lemma('k|A\\px~,svAdi,,,pra|kta|puM|dvitIyA|eka') == 'prApta' and spec_root('k|A\\px~,svAdi,,,pra|kta|puM|dvitIyA|eka') == 'pra+Ap'
assert norm('rAmaM') == 'rAmam'
assert derive('s|sItA|strI|caturTI|eka', 'sItAyE') and derive('s|nadI|strI|tftIyA|eka', 'nadyA') and derive('s|lakzmI|strI|praTamA|eka', 'lakzmIH')
kf = 't|qukf\\Y,tanAdi,,,|kartari|la~w|praTama|eka'
assert spec_words(kf, 'kurute')[-1] == 'middle' and spec_words(kf, 'karoti')[-1] == 'active' and spec_words(kf)[-1] == 'active'
cs = [c for _, c, _ in steps(derive('s|rAma|puM|dvitIyA|eka', 'rAmam'))]
assert cs.count('1.1.73') == 1

Placing a word-by-word analysis on the written line

A word-by-word analysis lists a line’s words in reading order with sandhi undone, so it has more, shorter entries than the line has written tokens. स कालेनेह is two written tokens and three entries: सः, कालेन, इह. align works out which entries belong to which token. It takes the tokens and a list of entries, each an object with the word in .w, such as the records from etym_entries, and returns one list of entry positions per token.

The match keeps the order of both lists. A token may take up to four consecutive entries, and they fit when their joined sandhi skeleton is similar enough to the token’s: a difflib similarity ratio of at least 0.72, scored by the token’s length. An entry that no token fits is dropped at a cost of 0.5. That cost was tuned on Gītā 4.2. Any lower and कालेनेह loses इह; any higher and विदुः takes a सः that the analysis added but the line does not have.

sandhi_skeleton is what makes the comparison tolerant of sandhi. It removes marks, drops a final s, r, ḥ, m or ṃ, and makes long vowels short, since a word’s final sound and its vowel length are what sandhi most often changes.


source

sandhi_skeleton

def sandhi_skeleton(
    slp
):

Reduce an SLP1 word to a form that survives sandhi: marks removed, a final s, r, H, m or M dropped, and long vowels made short.


source

align

def align(
    tk, es
):

Match a line’s written tokens to its word-by-word entries in order, returning the list of entry positions each token takes.

from collections import namedtuple
E = namedtuple('E', 'w')
tk = 'स कालेनेह महता योगो नष्टः परन्तप'.split()
es = [E(x) for x in ('सः', 'कालेन', 'इह', 'महता', 'योगः', 'नष्टः', 'परन्तप')]
assert align(tk, es) == [[0], [1, 2], [3], [4], [5], [6]]
assert sandhi_skeleton('rAmaH') == 'rama'

The sandhi at a join

Sandhi is the set of sound changes where two words meet: ca and eva are written caiva, and rāmaḥ before gacchati becomes rāmo. join_rule takes the word before the join and the word after, both in SLP1 with sandhi undone, and names the rule that joins them. It returns (id, name, sūtra) from JOINS, such as ('vrddhi', 'vṛddhi', '6.1.88'), or None for a join that no class fits. JOINS describes 25 classes that cover the joins of classical verse, each with its Aṣṭādhyāyī sūtra and a one-line description, plus a none entry for words written apart.

Pass the written form as written when you have it. Two pairs of rules can only be told apart that way: whether the y or v of ayādi was kept or dropped (te ime written tayime or ta ime), and whether t before ś became cch (tacchrutvā) or only c.


source

join_rule

def join_rule(
    left, right, written:str=''
):

Name the sandhi between left and right (both SLP1, sandhi undone) as (id, name, sūtra) from JOINS, using the written form when given, or None if no class fits.

for l, r, w, k in [('BIma', 'arjuna', 'BImArjuna', '6.1.101'), ('mahA', 'izvAsAH', 'mahezvAsAH', '6.1.87'), ('ca', 'eva', 'cEva', '6.1.88'),
                   ('karmaRi', 'eva', 'karmaRyeva', '6.1.77'), ('karO', 'uBO', 'karAvuBO', '6.1.78'), ('te', 'api', "te'pi", '6.1.109'),
                   ('te', 'ime', 'ta ime', '8.3.19'), ('rAmaH', 'gacCati', 'rAmo gacCati', '6.1.114'), ('rAmaH', 'api', "rAmo'pi", '6.1.113'),
                   ('hariH', 'gacCati', 'harir gacCati', '8.2.66'), ('tam', 'vande', 'taM vande', '8.3.23'), ('vAk', 'ISa', 'vAgISa', '8.2.39'), ('tat', 'ca', 'tacca', '8.4.40')]:
    assert join_rule(l, r, w)[2] == k, (l, r, join_rule(l, r, w))
assert join_rule('', 'iti') is None

Joining two words

join_rule names a join that is already written. join writes it. Give it two words in SLP1 as each stands alone and it returns the left word as written, the right word as written, and the class from join_rule. When the two words merge into one, as tava and iti do in taveti, the right word comes back as None.

The joins come from vidyut’s table of 1,468 sandhi rules (ganapati/data/vidyut_sandhi_rules.csv, MIT licence, in ganapati/data/LICENSE-vidyut). vidyut’s Python binding has a splitter but no joiner, so ganapati reads the table itself. A final visarga is read as s, or as r in punar, antar and prātar, and saḥ and eṣaḥ drop their visarga before a consonant by 6.1.132.


source

rules

def rules(
    p:NoneType=None
):

vidyut’s joins from p (default RULES) as {(left end, right first sound): result}, a space in the result keeping the words apart.

len(rules()), rules()[('a', 'i')], rules()[('as', 'a')]

source

join

def join(
    left, right
):

Join left and right (SLP1, each as it stands alone) as (left written, right written or None if they merge, class from join_rule or None).

assert join('rAmaH', 'api') == ('rAmo', "'pi", ('o_a', '-aḥ to -o', '6.1.113'))
assert join('tava', 'iti') == ('taveti', None, ('guna', 'guṇa', '6.1.87'))
assert join('saH', 'gacCati')[:2] == ('sa', 'gacCati')
assert join('punaH', 'api')[0] == 'punar'
assert join('', 'iti') == ('', 'iti', None)
join('ca', 'eva'), join('tat', 'ca')

source

samhita

def samhita(
    ws
):

Words in order (SLP1) as one saṃhitā line: (line, [(k, class of the join between word k and k+1)]), merged words written as one.

line, js = samhita(['rAmaH', 'api', 'vanam', 'gacCati'])
assert line == "rAmo 'pi vanaM gacCati", line
assert [j[2] if j else None for _, j in js] == ['6.1.113', None, '8.3.23']
assert samhita([]) == ('', [])
line, js