From 018a0d5a2fd328c22a883e8c044f5b53551de8c2 Mon Sep 17 00:00:00 2001 From: Isin Demirsahin Date: Tue, 28 Jan 2025 04:51:33 -0800 Subject: [PATCH] [natural_translit] Atomic functions for PhonologicalObject.Inventory - or_from_suppl: Add Or expressions of symbol lists as a supplement to the atomics inventory to make it easier to write expressions like (ph.t >> ph.d, preceding=ph.vowel, following=ph.vowel) - sync_atomics: Sync atomic inventory with symbol inventory. - Make sure atomic features match symbol features. - Optionally make or update Or expressions of a list of supplements. PiperOrigin-RevId: 720529974 --- .../phonology/inventories/x_mul.py | 2 +- .../phonology/phonological_symbol.py | 44 +++++++++++++++++-- .../phonology/phonological_symbol_test.py | 10 ++++- .../natural_translit/utils/expression.py | 2 +- .../scripts/natural_translit/utils/symbol.py | 8 +++- .../natural_translit/utils/symbol_test.py | 10 +++++ 6 files changed, 69 insertions(+), 7 deletions(-) diff --git a/nisaba/scripts/natural_translit/phonology/inventories/x_mul.py b/nisaba/scripts/natural_translit/phonology/inventories/x_mul.py index c1781360..5e218728 100644 --- a/nisaba/scripts/natural_translit/phonology/inventories/x_mul.py +++ b/nisaba/scripts/natural_translit/phonology/inventories/x_mul.py @@ -437,7 +437,7 @@ def _build_inventory() -> po.Phon.Inventory: labialized_consonant.update_descriptives(phf.labialization.labialized) for palatalized_consonant in [ph.sj]: palatalized_consonant.update_descriptives(phf.palatalization.heavy) - return ph + return ph.sync_atomics() INVENTORY = _build_inventory() diff --git a/nisaba/scripts/natural_translit/phonology/phonological_symbol.py b/nisaba/scripts/natural_translit/phonology/phonological_symbol.py index 65d4a870..2ccaa530 100644 --- a/nisaba/scripts/natural_translit/phonology/phonological_symbol.py +++ b/nisaba/scripts/natural_translit/phonology/phonological_symbol.py @@ -30,13 +30,13 @@ class PhonologicalSymbol(sym.Symbol): def __init__( self, alias: str = '', - text: str = '', raw: str = '', index: ty.IntOrNothing = ty.UNSPECIFIED, name: str = '', features: ft.Feature.ITERABLE = ty.UNSPECIFIED, ): - super().__init__(alias, text, raw, index, name) + super().__init__(alias, raw=raw, index=index, name=name) + self.text = raw if raw else self.alias self.features.new_profile( ft.Feature.Profile(self.PH_DESCRIPTIVE_FEATURES, 'new') ) @@ -54,6 +54,44 @@ def _add_symbol_and_atomic(self, symbol: 'PhonologicalSymbol') -> bool: exp.Atomic.get_instance(symbol) ) + def or_from_suppl(self, suppl: ty.IterableThing) -> bool: + """Makes an Or from a supplement and adds it to the atomics.""" + return self.atomics.add_suppl(exp.Or(*suppl, alias=suppl.alias)) + + def sync_atomics( + self, or_suppls: ty.ListOrNothing = ty.UNSPECIFIED + ) -> 'PhonologicalSymbol.Inventory': + """Syncs the atomic inventory with the symbol inventory. + + Updates the features of the atomic instance of each phonological symbol + to match the features of the symbol. Optionally updates the members of + Or supplements in atomics to include all members of the given supplements + in the list. For example, if an `inventory.vowel` iterable and a + corresponding `inventory.atomics.vowel` Or were initiated as `[a, e, i]` + and `(a | e | i)`, and later `[o, u]` was added to `inventory.vowel`, + this function will update `inventory.atomics.vowel` to + `(a | e | i | o | u)`. + + Updates the atomic inventory with the supplements. + + Args: + or_suppls: Optional list iterable supplements. If specified, the + corresponding Or supplement of the given supplements will be updated to + include all symbols. + + Returns: + The inventory. + + """ + for atomic in self.atomics: + for profile in atomic.features: + profile.update(atomic.symbol.features.get(profile.inventory)) + for suppl in ty.type_check(or_suppls, []): + if suppl.alias not in self.atomics.suppl_aliases: + self.or_from_suppl(suppl) + self.atomics.get(suppl.alias).add(*suppl) + return self + class Phon(PhonologicalSymbol): """Class for representing phonemes, phones, and phonological modifiers.""" @@ -128,5 +166,5 @@ def add_phonemes( ) -> list['Phon']: phs = [ph for ph in phonemes if self._add_phoneme(ph)] if list_alias: - self.make_suppl(list_alias, phs) + self.make_iterable_suppl(list_alias, *phs) return phs diff --git a/nisaba/scripts/natural_translit/phonology/phonological_symbol_test.py b/nisaba/scripts/natural_translit/phonology/phonological_symbol_test.py index 8ab491b5..0693fb50 100644 --- a/nisaba/scripts/natural_translit/phonology/phonological_symbol_test.py +++ b/nisaba/scripts/natural_translit/phonology/phonological_symbol_test.py @@ -43,7 +43,10 @@ def _test_inventory() -> po.Phon.Inventory: ph_inv.i.update_descriptives( phf.height.close, phf.backness.front, phf.labialization.unrounded ) - return ph_inv + ph_inv.make_iterable_suppl('close_like', ph_inv.e) + ph_inv.or_from_suppl(ph_inv.close_like) + ph_inv.close_like.add(ph_inv.i) + return ph_inv.sync_atomics(or_suppls=[ph_inv.vowel, ph_inv.close_like]) _TEST = _test_inventory() @@ -146,5 +149,10 @@ def test_phon_update_descriptives(self): '| syllabicity | syllabic |\n', ) + def test_sync_atomics(self): + self.AssertStrEqual(_TEST.atomics.a.features, _TEST.a.features) + self.AssertStrEqual(_TEST.atomics.vowel, '(a | e | i)') + self.AssertStrEqual(_TEST.atomics.close_like, '(e | i)') + if __name__ == '__main__': absltest.main() diff --git a/nisaba/scripts/natural_translit/utils/expression.py b/nisaba/scripts/natural_translit/utils/expression.py index f270e7a4..c0814b9f 100644 --- a/nisaba/scripts/natural_translit/utils/expression.py +++ b/nisaba/scripts/natural_translit/utils/expression.py @@ -148,11 +148,11 @@ def __init__(self, symbol: sym.Symbol, alias: str = ''): symbol.raw, symbol.index, symbol.name, - symbol.features.copy(), ) self._item_type = Atomic self._items = [self] self.symbol = symbol.symbol if isinstance(symbol, Atomic) else symbol + self.features = symbol.features.copy() @classmethod def get_instance(cls, symbol: sym.Symbol) -> 'Atomic': diff --git a/nisaba/scripts/natural_translit/utils/symbol.py b/nisaba/scripts/natural_translit/utils/symbol.py index 9c3eba69..8a34a2db 100644 --- a/nisaba/scripts/natural_translit/utils/symbol.py +++ b/nisaba/scripts/natural_translit/utils/symbol.py @@ -591,6 +591,12 @@ def _add_symbol(self, sym: 'Symbol') -> bool: self._add_to_dicts(sym) return True + def make_iterable_suppl(self, alias: str = '', *items) -> bool: + """Makes an iterable supplement with the given symbols.""" + return self.add_suppl( + ty.IterableThing(*items, alias=alias) + ) + def add_symbols(self, *symbols, list_alias: str = '') -> list['Symbol']: """Adds multiple symbols to the inventory. @@ -604,7 +610,7 @@ def add_symbols(self, *symbols, list_alias: str = '') -> list['Symbol']: """ syms = list(filter(self._add_symbol, [sym for sym in symbols])) if list_alias: - self.make_suppl(list_alias, syms) + self.make_iterable_suppl(list_alias, *syms) return syms def lookup( diff --git a/nisaba/scripts/natural_translit/utils/symbol_test.py b/nisaba/scripts/natural_translit/utils/symbol_test.py index fcbb7aef..217ceb55 100644 --- a/nisaba/scripts/natural_translit/utils/symbol_test.py +++ b/nisaba/scripts/natural_translit/utils/symbol_test.py @@ -65,8 +65,12 @@ def _latn_inventory() -> sym.Symbol.Inventory: sym.Symbol('b', text='b', raw='b', name='LETTER B'), sym.Symbol('i', text='i', raw='i', name='LETTER I'), sym.Symbol('i_uc', text='I', raw='I', name='UPPERCASE LETTER I'), + ) + syms.make_iterable_suppl('letter', syms.a, syms.b, syms.i, syms.i_uc) + syms.add_symbols( sym.Symbol('digit_1', text='1', raw='1', name='DIGIT ONE'), sym.Symbol('digit_2', text='2', raw='2', name='DIGIT TWO'), + list_alias='digit', ) for symbol in syms: symbol.features.new_profile(ft.Feature.Profile(_TEST_FTR, 'new')) @@ -128,6 +132,12 @@ def test_symbol_inventory_lookup(self): self.assertEqual(_SYM.text_lookup('🜔'), _SYM.schwa) self.assertEqual(_SYM.raw_lookup('🜔'), _CTRL.unk) + def test_symbol_inventory_iterable_suppl(self): + self.assertEqual(_LATN.letter.alias, 'letter') + self.assertIn(_LATN.a, _LATN.letter) + self.assertEqual(_LATN.digit.alias, 'digit') + self.assertIn(_LATN.digit_1, _LATN.digit) + def test_controls(self): self.assertEqual( sym.Symbol.descriptions(*_CTRL),