From d536f2b258cfd256d58f10348cebd3a9415b153c Mon Sep 17 00:00:00 2001 From: Isin Demirsahin Date: Fri, 24 Nov 2023 13:57:55 -0800 Subject: [PATCH] Add Feature and FeatureInventory classes. PiperOrigin-RevId: 585142666 --- .../natural_translit/features/BUILD.bazel | 103 +++++++++++++ .../natural_translit/features/feature2.py | 110 +++++++++++++ .../features/feature2_test.py | 59 +++++++ .../natural_translit/features/orthographic.py | 43 ++++++ .../features/orthographic_test.py | 31 ++++ .../natural_translit/features/phonological.py | 110 +++++++++++++ .../features/phonological_test.py | 39 +++++ .../natural_translit/features/qualifier.py | 30 ++++ .../features/qualifier_test.py | 30 ++++ .../natural_translit/phonology/BUILD.bazel | 16 +- .../natural_translit/phonology/feature.py | 145 +++--------------- .../phonology/feature_test.py | 30 ++++ .../natural_translit/utils/inventory2.py | 7 +- .../natural_translit/utils/inventory2_test.py | 13 ++ 14 files changed, 641 insertions(+), 125 deletions(-) create mode 100644 nisaba/scripts/natural_translit/features/BUILD.bazel create mode 100644 nisaba/scripts/natural_translit/features/feature2.py create mode 100644 nisaba/scripts/natural_translit/features/feature2_test.py create mode 100644 nisaba/scripts/natural_translit/features/orthographic.py create mode 100644 nisaba/scripts/natural_translit/features/orthographic_test.py create mode 100644 nisaba/scripts/natural_translit/features/phonological.py create mode 100644 nisaba/scripts/natural_translit/features/phonological_test.py create mode 100644 nisaba/scripts/natural_translit/features/qualifier.py create mode 100644 nisaba/scripts/natural_translit/features/qualifier_test.py create mode 100644 nisaba/scripts/natural_translit/phonology/feature_test.py diff --git a/nisaba/scripts/natural_translit/features/BUILD.bazel b/nisaba/scripts/natural_translit/features/BUILD.bazel new file mode 100644 index 00000000..04232977 --- /dev/null +++ b/nisaba/scripts/natural_translit/features/BUILD.bazel @@ -0,0 +1,103 @@ +# Copyright 2023 Nisaba Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +package( + default_applicable_licenses = [ + ], + default_visibility = [ + "//nisaba/scripts/natural_translit:__subpackages__", + "//nlp/sweet/saline:__subpackages__", + ], +) + +licenses(["notice"]) + +py_library( + name = "feature2", + srcs = ["feature2.py"], + deps = [ + "//nisaba/scripts/natural_translit/utils:inventory2", + "//nisaba/scripts/natural_translit/utils:log_op", + "//nisaba/scripts/natural_translit/utils:type_op", + ], +) + +py_test( + name = "feature2_test", + srcs = ["feature2_test.py"], + main = "feature2_test.py", + deps = [ + ":feature2", + "//nisaba/scripts/natural_translit/utils:type_op", + "@io_abseil_py//absl/testing:absltest", + ], +) + +py_library( + name = "orthographic", + srcs = ["orthographic.py"], + deps = [ + ":feature2", + "//nisaba/scripts/natural_translit/utils:list_op", + ], +) + +py_test( + name = "orthographic_test", + srcs = ["orthographic_test.py"], + main = "orthographic_test.py", + deps = [ + ":orthographic", + "@io_abseil_py//absl/testing:absltest", + ], +) + +py_library( + name = "phonological", + srcs = ["phonological.py"], + deps = [ + ":feature2", + "//nisaba/scripts/natural_translit/utils:list_op", + ], +) + +py_test( + name = "phonological_test", + srcs = ["phonological_test.py"], + main = "phonological_test.py", + deps = [ + ":phonological", + "@io_abseil_py//absl/testing:absltest", + ], +) + +py_library( + name = "qualifier", + srcs = ["qualifier.py"], + deps = [ + ":feature2", + "//nisaba/scripts/natural_translit/utils:list_op", + ], +) + +py_test( + name = "qualifier_test", + srcs = ["qualifier_test.py"], + main = "qualifier_test.py", + deps = [ + ":qualifier", + "@io_abseil_py//absl/testing:absltest", + ], +) diff --git a/nisaba/scripts/natural_translit/features/feature2.py b/nisaba/scripts/natural_translit/features/feature2.py new file mode 100644 index 00000000..a6bec54a --- /dev/null +++ b/nisaba/scripts/natural_translit/features/feature2.py @@ -0,0 +1,110 @@ +# Copyright 2023 Nisaba Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Feature, FeatureSet and FeatureInventory classes.""" + +from typing import Union +from nisaba.scripts.natural_translit.utils import inventory2 +from nisaba.scripts.natural_translit.utils import log_op as log +from nisaba.scripts.natural_translit.utils import type_op as ty + + +class Feature(ty.Thing): + """Feature class.""" + + def __init__(self, alias: str, category: str, group: str = ''): + super().__init__() + self.set_alias(alias) + self.text = alias + self.category = category + self.group = group if group else ty.UNASSIGNED + + +class FeatureSet: + """FeatureSet class.""" + + UNION = Union[Feature, 'FeatureSet', ty.Nothing] + + def __init__( + self, + *items: UNION + ): + super().__init__() + self._items = set() + self.add(*items) + + def __iter__(self): + return self._items.__iter__() + + def __len__(self): + return len(self._items) + + def __str__(self): + return self.str() + + def _set(self, arg: UNION) -> set[Feature]: + if isinstance(arg, Feature): return {arg} + if isinstance(arg, FeatureSet): return {f for f in arg} + return set() + + def _flat_set(self, *args: UNION) -> set[Feature]: + s = set() + for arg in args: + s.update(self._set(arg)) + return s + + def str(self): + return '(%s)' % ', '.join(f.text for f in self._items) + + def add(self, *args: UNION) -> None: + old = self.str() + self._items.update(self._flat_set(*args)) + log.dbg_message('(%s) to %s: %s' % ( + ', '.join(log.class_and_text(arg) for arg in args), + old, self.str() + )) + + def remove(self, *args: UNION) -> None: + old = self.str() + for f in self._flat_set(*args): + self._items.discard(f) + log.dbg_message('(%s) to %s: %s' % ( + ', '.join(log.class_and_text(arg) for arg in args), + old, self.str() + )) + + +class FeatureInventory(inventory2.Inventory): + """Feature inventory.""" + + def __init__(self, category: str): + super().__init__() + self.category = category + self.group_aliases = [] + + def add_feature(self, alias: str) -> None: + self.add_item(Feature(alias, self.category)) + + def make_group(self, group: str, aliases: list[str]) -> None: + features = [] + for alias in aliases: + new = Feature(alias, self.category, group) + if self.add_item(new): features.append(new) + self.make_supp(group, features) + self.group_aliases.append(group) + + def add_feature_set( + self, set_alias: str, *features: FeatureSet.UNION + ) -> None: + self.make_supp(set_alias, FeatureSet(*features)) diff --git a/nisaba/scripts/natural_translit/features/feature2_test.py b/nisaba/scripts/natural_translit/features/feature2_test.py new file mode 100644 index 00000000..9b3a0d67 --- /dev/null +++ b/nisaba/scripts/natural_translit/features/feature2_test.py @@ -0,0 +1,59 @@ +# Copyright 2023 Nisaba Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from absl.testing import absltest +from nisaba.scripts.natural_translit.features import feature2 as f +from nisaba.scripts.natural_translit.utils import type_op as ty + +_f0 = f.Feature('f0', 'c0') +_f1 = f.Feature('f1', 'c1') +_f2 = f.Feature('f2', 'c2') +_f3 = f.Feature('f3', 'c3') +_f4 = f.Feature('f4', 'c4') +_fs0 = f.FeatureSet() +_fs1 = f.FeatureSet(_f1, _f2) +_fs2 = f.FeatureSet(ty.UNSPECIFIED) +_fs3 = f.FeatureSet(_fs1, _f3) +_test = f.FeatureInventory('test') + + +class Feature2Test(absltest.TestCase): + + def test_feature(self): + self.assertEqual(_f0.alias, 'f0') + self.assertEqual(_f0.category, 'c0') + self.assertEqual(_f0.group, ty.UNASSIGNED) + + def test_feature_set_empty(self): + self.assertEmpty(_fs0) + self.assertEqual(_fs0.str(), '()') + + def test_feature_set_items(self): + self.assertIn(_f1, _fs1) + self.assertIn(_f2, _fs1) + + def test_feature_set_nothing(self): + self.assertEmpty(_fs2) + + def test_feature_set_feature_set(self): + self.assertEqual(_fs3._items, {_f1, _f2, _f3}) + + def test_feature_inventory_group(self): + _test.make_group('g', ['gf1', 'gf2']) + self.assertEqual(_test.gf1.group, 'g') + self.assertEqual(_test.g, [_test.gf1, _test.gf2]) + + +if __name__ == '__main__': + absltest.main() diff --git a/nisaba/scripts/natural_translit/features/orthographic.py b/nisaba/scripts/natural_translit/features/orthographic.py new file mode 100644 index 00000000..bcd12fef --- /dev/null +++ b/nisaba/scripts/natural_translit/features/orthographic.py @@ -0,0 +1,43 @@ +# Copyright 2023 Nisaba Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Orthographic features.""" + +from nisaba.scripts.natural_translit.features import feature2 +from nisaba.scripts.natural_translit.utils import list_op as ls + + +def _script() -> feature2.FeatureInventory: + """Script inventory.""" + f = feature2.FeatureInventory('script') + f.add_feature('test') + ls.apply_foreach(f.make_group, [ + ['latin', ['basic']], + ]) + return f + +script = _script() + + +def _grapheme() -> feature2.FeatureInventory: + """Grapheme feature inventory.""" + f = feature2.FeatureInventory('orthographic') + ls.apply_foreach(f.make_group, [ + ['case', ['lower', 'upper']], + ['texttype', ['raw', 'ctrl']], + ['dependence', ['standalone', 'combining']], + ]) + return f + +grapheme = _grapheme() diff --git a/nisaba/scripts/natural_translit/features/orthographic_test.py b/nisaba/scripts/natural_translit/features/orthographic_test.py new file mode 100644 index 00000000..9965d270 --- /dev/null +++ b/nisaba/scripts/natural_translit/features/orthographic_test.py @@ -0,0 +1,31 @@ +# Copyright 2023 Nisaba Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from absl.testing import absltest +from nisaba.scripts.natural_translit.features import orthographic + +_gr = orthographic.grapheme +_sc = orthographic.script + + +class OrthographicTest(absltest.TestCase): + + def test_script(self): + self.assertIn(_sc.basic, _sc.latin) + + def test_case(self): + self.assertEqual(_gr.case, [_gr.lower, _gr.upper]) + +if __name__ == '__main__': + absltest.main() diff --git a/nisaba/scripts/natural_translit/features/phonological.py b/nisaba/scripts/natural_translit/features/phonological.py new file mode 100644 index 00000000..de4d1700 --- /dev/null +++ b/nisaba/scripts/natural_translit/features/phonological.py @@ -0,0 +1,110 @@ +# Copyright 2023 Nisaba Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Phonological features.""" + +from nisaba.scripts.natural_translit.features import feature2 +from nisaba.scripts.natural_translit.utils import list_op as ls + + +def _articulatory() -> feature2.FeatureInventory: + """Articulatory features.""" + f = feature2.FeatureInventory('articulatory') + ls.apply_foreach(f.make_group, [ + ['pronunciation', ['silent', 'vocal']], + ['syllabicity', ['syllabic', 'nonsyllabic']], + ['airstream', ['pulmonic', 'nonpulmonic']], + ['airflow', ['central', 'lateral', 'nasal']], + ['release', ['aspirated', 'unaspirated', 'click']], + ['place', [ + 'labial', 'nonlabial', + 'bilabial', 'labiodental', 'dental', + 'alveolar', 'postalveolar', 'palatal', + 'retroflex', 'velar', 'uvular', + 'pharyngeal', 'epiglottal', 'glottal', + ]], + ['rhoticity', ['rhotic', 'nonrhotic']], + ['turbulence', ['none', 'sibilant', 'nonsibilant']], + ['consonancy', ['vowel', 'consonant']], + ['manner', ['stop', 'fricative', 'approximant', 'flap', 'trill']], + ['voicing', ['voiceless', 'voiced', 'partially_voiced', 'devoiced']], + ['composition', [ + 'noncomposite', 'composite', + 'diphthong', 'affricate', 'coarticulated' + ]], + ['height', [ + 'close', 'near_close', + 'close_mid', 'mid', 'open_mid', + 'near_open', 'open' + ]], + ['backness', ['front', 'near_front', 'center', 'near_back', 'back']] + ]) + # Vowel rows + ls.apply_foreach(f.add_feature_set, [ + ['close_vwl', f.vowel, f.close], + ['n_close_vwl', f.vowel, f.near_close], + ['c_mid_vwl', f.vowel, f.close_mid], + ['mid_vwl', f.vowel, f.mid], + ['o_mid_vwl', f.vowel, f.open_mid], + ['n_open_vwl', f.vowel, f.near_open], + ['open_vwl', f.vowel, f.open], + ]) + # Vowel columns + ls.apply_foreach(f.add_feature_set, [ + ['front_unr', f.front, f.nonlabial], + ['front_rnd', f.front, f.labial], + ['n_front_unr', f.near_front, f.nonlabial], + ['n_front_rnd', f.near_front, f.labial], + ['center_unr', f.center, f.nonlabial], + ['center_rnd', f.center, f.labial], + ['n_back_unr', f.near_back, f.nonlabial], + ['n_back_rnd', f.near_back, f.labial], + ['back_unr', f.back, f.nonlabial], + ['back_rnd', f.back, f.labial], + ]) + # Consonant rows - split by voicing + ls.apply_foreach(f.add_feature_set, [ + ['vcd_nasal', f.nasal, f.stop, f.voiced], + ['vcl_stop', f.stop, f.voiceless], + ['vcd_stop', f.stop, f.voiced], + ['vcl_nonsib_fricative', f.fricative, f.nonsibilant, f.voiceless], + ['vcd_nonsib_fricative', f.fricative, f.nonsibilant, f.voiced], + ['vcl_sib_fricative', f.fricative, f.sibilant, f.voiceless], + ['vcd_sib_fricative', f.fricative, f.sibilant, f.voiced], + ['vcl_lat_fricative', f.fricative, f.lateral, f.voiceless], + ['vcd_lat_fricative', f.fricative, f.lateral, f.voiced], + ['central_approximant', f.approximant, f.voiced], + ['lateral_approximant', f.approximant, f.voiced, f.lateral], + ['vlr_lbl', f.velar, f.labial], + ['vcd_flap', f.flap, f.voiced], + ['vcd_trill', f.trill, f.voiced], + ]) + # Click features + f.add_feature_set('click_release', f.click, f.nonpulmonic) + return f + +articulatory = _articulatory() + + +def _suprasegmental() -> feature2.FeatureInventory: + """Suprasegmental features.""" + f = feature2.FeatureInventory('suprasegmental') + # TODO: Convert suprasegmental features to groups of qualified features. + # eg. f.make_group('duration', ['short', 'long']) + f.make_group('suprasegmental', [ + 'duration', 'stress', 'pitch', 'contour', 'intonation' + ]) + return f + +suprasegmental = _suprasegmental() diff --git a/nisaba/scripts/natural_translit/features/phonological_test.py b/nisaba/scripts/natural_translit/features/phonological_test.py new file mode 100644 index 00000000..7921f140 --- /dev/null +++ b/nisaba/scripts/natural_translit/features/phonological_test.py @@ -0,0 +1,39 @@ +# Copyright 2023 Nisaba Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from absl.testing import absltest +from nisaba.scripts.natural_translit.features import phonological + +_ar = phonological.articulatory +_ss = phonological.suprasegmental + + +class PhonologicalTest(absltest.TestCase): + + def test_feature_voiced(self): + self.assertEqual(_ar.voiced.alias, 'voiced') + self.assertEqual(_ar.voiced.category, 'articulatory') + self.assertEqual(_ar.voiced.group, 'voicing') + + def test_group_voicing(self): + self.assertIn(_ar.voiced, _ar.voicing) + + def test_feature_set_vcd_stop(self): + self.assertIn(_ar.voiced, _ar.vcd_stop) + + def test_suprasegmental(self): + self.assertIn(_ss.duration, _ss) + +if __name__ == '__main__': + absltest.main() diff --git a/nisaba/scripts/natural_translit/features/qualifier.py b/nisaba/scripts/natural_translit/features/qualifier.py new file mode 100644 index 00000000..2a3bbb9b --- /dev/null +++ b/nisaba/scripts/natural_translit/features/qualifier.py @@ -0,0 +1,30 @@ +# Copyright 2023 Nisaba Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Simple placeholder for the phonological feature structure.""" + +from nisaba.scripts.natural_translit.features import feature2 +from nisaba.scripts.natural_translit.utils import list_op as ls + + +def _qualifier() -> feature2.FeatureInventory: + """Qualifiers.""" + f = feature2.FeatureInventory('qualifier') + ls.apply_foreach(f.make_group, [ + ['degree', ['top', 'high', 'middle', 'low', 'bottom']], + ['change', ['rising', 'falling', 'interrupt']], + ]) + return f + +qualifier = _qualifier() diff --git a/nisaba/scripts/natural_translit/features/qualifier_test.py b/nisaba/scripts/natural_translit/features/qualifier_test.py new file mode 100644 index 00000000..9ba252f9 --- /dev/null +++ b/nisaba/scripts/natural_translit/features/qualifier_test.py @@ -0,0 +1,30 @@ +# Copyright 2023 Nisaba Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from absl.testing import absltest +from nisaba.scripts.natural_translit.features import qualifier + +_q = qualifier.qualifier + + +class QualifierTest(absltest.TestCase): + + def test_degree(self): + self.assertEqual(_q.top.group, _q.bottom.group) + + def test_change(self): + self.assertIn(_q.rising, _q.change) + +if __name__ == '__main__': + absltest.main() diff --git a/nisaba/scripts/natural_translit/phonology/BUILD.bazel b/nisaba/scripts/natural_translit/phonology/BUILD.bazel index 68ac6d93..41ac8930 100644 --- a/nisaba/scripts/natural_translit/phonology/BUILD.bazel +++ b/nisaba/scripts/natural_translit/phonology/BUILD.bazel @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Phonology resources.""" load("@rules_python//python:py_library.bzl", "py_library") @@ -36,8 +35,19 @@ py_library( name = "feature", srcs = ["feature.py"], deps = [ - "//nisaba/scripts/natural_translit/utils:inventory", - "//nisaba/scripts/natural_translit/utils:list_op", + "//nisaba/scripts/natural_translit/features:phonological", + "//nisaba/scripts/natural_translit/features:qualifier", + "//nisaba/scripts/natural_translit/utils:inventory2", + ], +) + +py_test( + name = "feature_test_py", + srcs = ["feature_test.py"], + main = "feature_test.py", + deps = [ + ":feature", + "@io_abseil_py//absl/testing:absltest", ], ) diff --git a/nisaba/scripts/natural_translit/phonology/feature.py b/nisaba/scripts/natural_translit/phonology/feature.py index 9de63f83..23501d72 100644 --- a/nisaba/scripts/natural_translit/phonology/feature.py +++ b/nisaba/scripts/natural_translit/phonology/feature.py @@ -12,132 +12,37 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Simple placeholder for the phonological feature structure.""" +"""Converts feature inventories to str inventories for backward compatibility. -import collections -from nisaba.scripts.natural_translit.utils import inventory as i -from nisaba.scripts.natural_translit.utils import list_op as ls +TODO: Remove after updating modify_phon and phoneme_inventory to use +feature2. -# cat: category, val: value -PhonFeature = collections.namedtuple( - 'PhonFeature', ['alias', 'cat']) +""" +from nisaba.scripts.natural_translit.features import phonological +from nisaba.scripts.natural_translit.features import qualifier +from nisaba.scripts.natural_translit.utils import inventory2 -def ft_inventory( - feature_list: [PhonFeature], - store_list: [i.Store] = None -) -> collections.namedtuple: - return i.make_inventory(i.alias_list(feature_list), feature_list, store_list) -# TODO:Enum features by category -ARTICULATION_FEATURE = ls.apply_foreach(PhonFeature, [ - ['silent', 'pronunciation'], - ['syllabic', 'syllabicity'], - ['nonsyllabic', 'syllabicity'], - ['pulmonic', 'airstream'], - ['nonpulmonic', 'airstream'], - ['central', 'airflow'], - ['lateral', 'airflow'], - ['nasal', 'airflow'], - ['aspirated', 'release'], - ['click', 'release'], - ['labial', 'place'], - ['nonlabial', 'place'], - ['bilabial', 'place'], - ['labiodental', 'place'], - ['dental', 'place'], - ['alveolar', 'place'], - ['postalveolar', 'place'], - ['palatal', 'place'], - ['retroflex', 'place'], - ['velar', 'place'], - ['uvular', 'place'], - ['pharyngeal', 'place'], - ['epiglottal', 'place'], - ['glottal', 'place'], - ['rhotic', 'rhoticity'], - ['sibilant', 'amplitude'], - ['nonsibilant', 'amplitude'], - ['vowel', 'class'], - ['stop', 'manner'], - ['fricative', 'manner'], - ['approximant', 'manner'], - ['flap', 'manner'], - ['trill', 'manner'], - ['voiceless', 'voicing'], - ['voiced', 'voicing'], - ['partially_voiced', 'voicing'], - ['devoiced', 'voicing'], - ['composite', 'composite'], - ['diphthong', 'composite'], - ['affricate', 'composite'], - ['coarticulated', 'composite'], - ['close', 'height'], - ['near_close', 'height'], - ['close_mid', 'height'], - ['mid', 'height'], - ['open_mid', 'height'], - ['near_open', 'height'], - ['open', 'height'], - ['front', 'backness'], - ['near_front', 'backness'], - ['center', 'backness'], - ['near_back', 'backness'], - ['back', 'backness'], - ['duration', 'suprasegmental'], - ['stress', 'suprasegmental'], - ['pitch', 'suprasegmental'], - ['contour', 'suprasegmental'], - ['intonation', 'suprasegmental'], -]) +def _convert_and_add( + feature_inventory: inventory2.Inventory, + str_inventory: inventory2.Inventory, +) -> None: + for alias in feature_inventory.item_aliases: + str_inventory.add_item(feature_inventory.get(alias), 'alias') + for alias in feature_inventory.supp_aliases: + str_inventory.make_supp( + alias, [f.alias for f in feature_inventory.get(alias)] + ) -FEATURE_QUALIFIER = ls.apply_foreach(PhonFeature, [ - ['top', 'degree'], - ['high', 'degree'], - ['middle', 'degree'], - ['low', 'degree'], - ['bottom', 'degree'], - ['rising', 'change'], - ['falling', 'change'], - ['interrupt', 'change'], -]) -FEATURES = ARTICULATION_FEATURE + FEATURE_QUALIFIER -_F = ft_inventory(FEATURES) +def _str_inventory() -> inventory2.Inventory: + """Str feature inventory for backward compatibility.""" + f = inventory2.Inventory() + _convert_and_add(phonological.articulatory, f) + _convert_and_add(phonological.suprasegmental, f) + _convert_and_add(qualifier.qualifier, f) + return f -ROWS = ls.apply_foreach(i.store_as, [ - ['close_vwl', [_F.vowel, _F.close]], - ['n_close_vwl', [_F.vowel, _F.near_close]], - ['c_mid_vwl', [_F.vowel, _F.close_mid]], - ['mid_vwl', [_F.vowel, _F.mid]], - ['o_mid_vwl', [_F.vowel, _F.open_mid]], - ['n_open_vwl', [_F.vowel, _F.near_open]], - ['open_vwl', [_F.vowel, _F.open]], - ['front_unr', [_F.front, _F.nonlabial]], - ['front_rnd', [_F.front, _F.labial]], - ['n_front_unr', [_F.near_front, _F.nonlabial]], - ['n_front_rnd', [_F.near_front, _F.labial]], - ['center_unr', [_F.center, _F.nonlabial]], - ['center_rnd', [_F.center, _F.labial]], - ['n_back_unr', [_F.near_back, _F.nonlabial]], - ['n_back_rnd', [_F.near_back, _F.labial]], - ['back_unr', [_F.back, _F.nonlabial]], - ['back_rnd', [_F.back, _F.labial]], - ['vcd_nasal', [_F.nasal, _F.stop, _F.voiced]], - ['vcl_stop', [_F.stop, _F.voiceless]], - ['vcd_stop', [_F.stop, _F.voiced]], - ['vcl_nonsib_fricative', [_F.fricative, _F.nonsibilant, _F.voiceless]], - ['vcd_nonsib_fricative', [_F.fricative, _F.nonsibilant, _F.voiced]], - ['vcl_sib_fricative', [_F.fricative, _F.sibilant, _F.voiceless]], - ['vcd_sib_fricative', [_F.fricative, _F.sibilant, _F.voiced]], - ['vcl_lat_fricative', [_F.fricative, _F.lateral, _F.voiceless]], - ['vcd_lat_fricative', [_F.fricative, _F.lateral, _F.voiced]], - ['central_approximant', [_F.approximant, _F.voiced]], - ['lateral_approximant', [_F.approximant, _F.voiced, _F.lateral]], - ['vlr_lbl', [_F.velar, _F.labial]], - ['vcd_flap', [_F.flap, _F.voiced]], - ['vcd_trill', [_F.trill, _F.voiced]], - ['click_release', [_F.click, _F.nonpulmonic]], -]) -FEATURE_INVENTORY = ft_inventory(FEATURES, ROWS) +FEATURE_INVENTORY = _str_inventory() diff --git a/nisaba/scripts/natural_translit/phonology/feature_test.py b/nisaba/scripts/natural_translit/phonology/feature_test.py new file mode 100644 index 00000000..972d5840 --- /dev/null +++ b/nisaba/scripts/natural_translit/phonology/feature_test.py @@ -0,0 +1,30 @@ +# Copyright 2023 Nisaba Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from absl.testing import absltest +from nisaba.scripts.natural_translit.phonology import feature + +_f = feature.FEATURE_INVENTORY + + +class FeatureTest(absltest.TestCase): + + def test_str_voiced(self): + self.assertEqual(_f.voiced, 'voiced') + + def test_str_vcd_stop(self): + self.assertIn('voiced', _f.vcd_stop) + +if __name__ == '__main__': + absltest.main() diff --git a/nisaba/scripts/natural_translit/utils/inventory2.py b/nisaba/scripts/natural_translit/utils/inventory2.py index e548afa0..92c5f216 100644 --- a/nisaba/scripts/natural_translit/utils/inventory2.py +++ b/nisaba/scripts/natural_translit/utils/inventory2.py @@ -75,7 +75,7 @@ def _add_field(self, alias: str, value: ...) -> bool: return True def _get_field_value( - self, thing: ..., attr: str = '', + self, thing: ty.Thing, attr: str = '', typed: ty.TypeOrNothing = ty.UNSPECIFIED ) -> ...: """Gets the value for a field from a Thing. @@ -98,7 +98,7 @@ def _get_field_value( return field_value if ty.is_instance_dbg(field_value, typed) else ty.MISSING def add_item( - self, thing: ..., attr: str = '', + self, thing: ty.Thing, attr: str = '', typed: ty.TypeOrNothing = ty.UNSPECIFIED ) -> bool: field_value = self._get_field_value(thing, attr, typed) @@ -117,3 +117,6 @@ def make_supp(self, alias: str, value: ...) -> None: """Adds the value as a supplement.""" if self._add_field(alias, value): self.supp_aliases.append(alias) + + def get(self, alias: str, default: ... = ty.MISSING) -> ...: + return self.__dict__.get(alias, default) diff --git a/nisaba/scripts/natural_translit/utils/inventory2_test.py b/nisaba/scripts/natural_translit/utils/inventory2_test.py index 01315f3d..8996e527 100644 --- a/nisaba/scripts/natural_translit/utils/inventory2_test.py +++ b/nisaba/scripts/natural_translit/utils/inventory2_test.py @@ -99,5 +99,18 @@ def test_add_item_recurring_item(self): _i2.add_item(_T_CB_STR, 'value') self.assertLen(_i2, 2) + def test_get_item(self): + self.assertEqual(_i2.get('b'), _B_STR) + + def test_get_supp(self): + self.assertEqual(_i2.get('d'), _D_INT) + + def test_get_out_of_inventory(self): + self.assertEqual(_i2.get('x'), ty.MISSING) + + def test_get_out_of_inventory_default(self): + self.assertIsNone(_i2.get('x', None)) + + if __name__ == '__main__': absltest.main()