Vendor patched panphon (py3.8-compatible import)
Browse files- panphon/__init__.py +3 -0
- panphon/__pycache__/__init__.cpython-38.pyc +0 -0
- panphon/__pycache__/_panphon.cpython-38.pyc +0 -0
- panphon/__pycache__/errors.cpython-38.pyc +0 -0
- panphon/__pycache__/featuretable.cpython-38.pyc +0 -0
- panphon/__pycache__/segment.cpython-38.pyc +0 -0
- panphon/__pycache__/xsampa.cpython-38.pyc +0 -0
- panphon/_panphon.py +544 -0
- panphon/collapse.py +38 -0
- panphon/data/asjp.yml +136 -0
- panphon/data/diacritic_definitions.yml +669 -0
- panphon/data/diacritic_definitions_schema.yml +209 -0
- panphon/data/dolgopolsky_prime.yml +56 -0
- panphon/data/feature_weights.csv +2 -0
- panphon/data/ipa-xsampa.csv +175 -0
- panphon/data/ipa_all.csv +0 -0
- panphon/data/ipa_bases.csv +151 -0
- panphon/data/sort_order.yml +40 -0
- panphon/data/sort_order_schema.yml +10 -0
- panphon/distance.py +837 -0
- panphon/errors.py +5 -0
- panphon/featuretable.py +590 -0
- panphon/permissive.py +204 -0
- panphon/segment.py +224 -0
- panphon/sonority.py +85 -0
- panphon/xsampa.py +35 -0
panphon/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import absolute_import
|
| 2 |
+
from panphon.featuretable import FeatureTable
|
| 3 |
+
from panphon._panphon import pat
|
panphon/__pycache__/__init__.cpython-38.pyc
ADDED
|
Binary file (359 Bytes). View file
|
|
|
panphon/__pycache__/_panphon.cpython-38.pyc
ADDED
|
Binary file (22.8 kB). View file
|
|
|
panphon/__pycache__/errors.cpython-38.pyc
ADDED
|
Binary file (373 Bytes). View file
|
|
|
panphon/__pycache__/featuretable.cpython-38.pyc
ADDED
|
Binary file (25.4 kB). View file
|
|
|
panphon/__pycache__/segment.cpython-38.pyc
ADDED
|
Binary file (11 kB). View file
|
|
|
panphon/__pycache__/xsampa.cpython-38.pyc
ADDED
|
Binary file (1.89 kB). View file
|
|
|
panphon/_panphon.py
ADDED
|
@@ -0,0 +1,544 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
from __future__ import absolute_import, print_function, unicode_literals
|
| 3 |
+
from os import stat
|
| 4 |
+
import unicodedata
|
| 5 |
+
|
| 6 |
+
import os.path
|
| 7 |
+
from functools import reduce
|
| 8 |
+
|
| 9 |
+
import numpy
|
| 10 |
+
import pkg_resources
|
| 11 |
+
|
| 12 |
+
import regex as re
|
| 13 |
+
import unicodecsv as csv
|
| 14 |
+
|
| 15 |
+
from panphon import featuretable
|
| 16 |
+
|
| 17 |
+
from . import xsampa
|
| 18 |
+
|
| 19 |
+
from panphon.errors import SegmentError
|
| 20 |
+
|
| 21 |
+
# logging.basicConfig(level=logging.DEBUG)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
FT_REGEX = re.compile(r'([-+0])([a-z][A-Za-z]*)', re.U | re.X)
|
| 25 |
+
MT_REGEX = re.compile(r'\[[-+0a-zA-Z ,;]*\]')
|
| 26 |
+
SEG_REGEX = re.compile(r'[\p{InBasic_Latin}\p{InGreek_and_Coptic}' +
|
| 27 |
+
r'\p{InIPA_Extensions}ŋœ\u00C0-\u00FF]' +
|
| 28 |
+
r'[\u0300-\u0360\u0362-\u036F]*' +
|
| 29 |
+
r'\p{InSpacing_Modifier_Letters}*',
|
| 30 |
+
re.U | re.X)
|
| 31 |
+
filenames = {
|
| 32 |
+
'spe+': os.path.join('data', 'ipa_all.csv'),
|
| 33 |
+
'panphon': os.path.join('data', 'ipa_all.csv'),
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def segment_text(text, seg_regex=SEG_REGEX):
|
| 38 |
+
"""Return an iterator of segments in the text.
|
| 39 |
+
|
| 40 |
+
Args:
|
| 41 |
+
text (unicode): string of IPA Unicode text
|
| 42 |
+
seg_regex (_regex.Pattern): compiled regex defining a segment (base +
|
| 43 |
+
modifiers)
|
| 44 |
+
|
| 45 |
+
Return:
|
| 46 |
+
generator: segments in the input text
|
| 47 |
+
"""
|
| 48 |
+
for m in seg_regex.finditer(text):
|
| 49 |
+
yield m.group(0)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def fts(s):
|
| 53 |
+
"""Given string `s` with +/-[alphabetical sequence]s, return list of features.
|
| 54 |
+
|
| 55 |
+
Args:
|
| 56 |
+
s (str): string with segments of the sort "+son -syl 0cor"
|
| 57 |
+
|
| 58 |
+
Return:
|
| 59 |
+
list: list of (value, feature) tuples
|
| 60 |
+
"""
|
| 61 |
+
return [m.groups() for m in FT_REGEX.finditer(s)]
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def pat(p):
|
| 65 |
+
"""Given a string `p` with feature matrices (features grouped with square
|
| 66 |
+
brackets into segments, return a list of sets of (value, feature) tuples.
|
| 67 |
+
|
| 68 |
+
Args:
|
| 69 |
+
p (str): list of feature matrices as strings
|
| 70 |
+
|
| 71 |
+
Return:
|
| 72 |
+
list: list of sets of (value, feature) tuples
|
| 73 |
+
"""
|
| 74 |
+
pattern = []
|
| 75 |
+
for matrix in [m.group(0) for m in MT_REGEX.finditer(p)]:
|
| 76 |
+
segment = set([m.groups() for m in FT_REGEX.finditer(matrix)])
|
| 77 |
+
pattern.append(segment)
|
| 78 |
+
return pattern
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def word2array(ft_names, word):
|
| 82 |
+
"""Converts `word` [[(value, feature),...],...] to a NumPy array
|
| 83 |
+
|
| 84 |
+
Given a word consisting of lists of lists/sets of (value, feature) tuples,
|
| 85 |
+
return a NumPy array where each row is a segment and each column is a
|
| 86 |
+
feature.
|
| 87 |
+
|
| 88 |
+
Args:
|
| 89 |
+
ft_names (list): list of feature names (as strings) in order; this
|
| 90 |
+
argument controls what features are included in the
|
| 91 |
+
array that is output and their order vis-a-vis the
|
| 92 |
+
columns of the array
|
| 93 |
+
word (list): list of lists of feature tuples (output by
|
| 94 |
+
FeatureTable.word_fts)
|
| 95 |
+
|
| 96 |
+
Returns:
|
| 97 |
+
ndarray: array in which each row is a segment and each column
|
| 98 |
+
is a feature
|
| 99 |
+
"""
|
| 100 |
+
vdict = {'+': 1, '-': -1, '0': 0}
|
| 101 |
+
|
| 102 |
+
def seg2col(seg):
|
| 103 |
+
seg = dict([(k, v) for (v, k) in seg])
|
| 104 |
+
return [vdict[seg[ft]] for ft in ft_names]
|
| 105 |
+
return numpy.array([seg2col(s) for s in word], order='F')
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
class FeatureTable(object):
|
| 109 |
+
"""Encapsulate the segment <=> feature mapping in the file
|
| 110 |
+
"data/ipa_all.csv".
|
| 111 |
+
"""
|
| 112 |
+
|
| 113 |
+
def __init__(self, feature_set='spe+'):
|
| 114 |
+
"""Construct a FeatureTable object
|
| 115 |
+
|
| 116 |
+
Args:
|
| 117 |
+
feature_set (str): the feature set that the FeatureTable will use;
|
| 118 |
+
currently, there is only one of these ("spe+")
|
| 119 |
+
|
| 120 |
+
"""
|
| 121 |
+
filename = filenames[feature_set]
|
| 122 |
+
self.segments, self.seg_dict, self.names = self._read_table(filename)
|
| 123 |
+
self.seg_seq = {seg[0]: i for (i, seg) in enumerate(self.segments)}
|
| 124 |
+
self.weights = self._read_weights()
|
| 125 |
+
self.seg_regex = self._build_seg_regex()
|
| 126 |
+
self.longest_seg = max([len(x) for x in self.seg_dict.keys()])
|
| 127 |
+
self.xsampa = xsampa.XSampa()
|
| 128 |
+
|
| 129 |
+
@staticmethod
|
| 130 |
+
def normalize(data):
|
| 131 |
+
return unicodedata.normalize('NFD', data)
|
| 132 |
+
|
| 133 |
+
def _read_table(self, filename):
|
| 134 |
+
"""Read the data from data/ipa_all.csv into self.segments, a
|
| 135 |
+
list of 2-tuples of unicode strings and sets of feature tuples and
|
| 136 |
+
self.seg_dict, a dictionary mapping from unicode segments and sets of
|
| 137 |
+
feature tuples.
|
| 138 |
+
"""
|
| 139 |
+
filename = pkg_resources.resource_filename(
|
| 140 |
+
__name__, filename)
|
| 141 |
+
segments = []
|
| 142 |
+
with open(filename, 'rb') as f:
|
| 143 |
+
reader = csv.reader(f, encoding='utf-8')
|
| 144 |
+
header = next(reader)
|
| 145 |
+
names = header[1:]
|
| 146 |
+
for row in reader:
|
| 147 |
+
seg = row[0]
|
| 148 |
+
vals = row[1:]
|
| 149 |
+
specs = set(zip(vals, names))
|
| 150 |
+
segments.append((seg, specs))
|
| 151 |
+
seg_dict = dict(segments)
|
| 152 |
+
return segments, seg_dict, names
|
| 153 |
+
|
| 154 |
+
def _read_weights(self, filename=os.path.join('data', 'feature_weights.csv')):
|
| 155 |
+
filename = pkg_resources.resource_filename(
|
| 156 |
+
__name__, filename)
|
| 157 |
+
with open(filename, 'rb') as f:
|
| 158 |
+
reader = csv.reader(f, encoding='utf-8')
|
| 159 |
+
next(reader)
|
| 160 |
+
weights = [float(x) for x in next(reader)]
|
| 161 |
+
return weights
|
| 162 |
+
|
| 163 |
+
def _build_seg_regex(self):
|
| 164 |
+
# Build a regex that will match individual segments in a string.
|
| 165 |
+
segs = sorted(self.seg_dict.keys(), key=lambda x: len(x), reverse=True)
|
| 166 |
+
return re.compile(r'(?P<all>{})'.format('|'.join(segs)))
|
| 167 |
+
|
| 168 |
+
def fts(self, segment):
|
| 169 |
+
"""Returns features corresponding to `segment` as list of (value,
|
| 170 |
+
feature) tuples.
|
| 171 |
+
|
| 172 |
+
Args:
|
| 173 |
+
segment (unicode): segment for which features are to be returned as
|
| 174 |
+
Unicode IPA string.
|
| 175 |
+
|
| 176 |
+
Returns:
|
| 177 |
+
set: set of (value, feature) tuples, if `segment` is valid; otherwise,
|
| 178 |
+
None
|
| 179 |
+
"""
|
| 180 |
+
if segment in self.seg_dict:
|
| 181 |
+
return self.seg_dict[segment]
|
| 182 |
+
else:
|
| 183 |
+
return None
|
| 184 |
+
|
| 185 |
+
def match(self, ft_mask, ft_seg):
|
| 186 |
+
"""Answer question "are `ft_mask`'s features a subset of ft_seg?"
|
| 187 |
+
|
| 188 |
+
Args:
|
| 189 |
+
ft_mask (set): pattern defined as set of (value, feature) tuples
|
| 190 |
+
ft_seg (set): segment defined as a set of (value, feature) tuples
|
| 191 |
+
|
| 192 |
+
Returns:
|
| 193 |
+
bool: True iff all features in `ft_mask` are also in `ft_seg`
|
| 194 |
+
"""
|
| 195 |
+
return set(ft_mask) <= set(ft_seg)
|
| 196 |
+
|
| 197 |
+
def fts_match(self, features, segment):
|
| 198 |
+
"""Answer question "are `ft_mask`'s features a subset of ft_seg?"
|
| 199 |
+
|
| 200 |
+
This is like `FeatureTable.match` except that it checks whether a
|
| 201 |
+
segment is valid and returns None if it is not.
|
| 202 |
+
|
| 203 |
+
Args:
|
| 204 |
+
features (set): pattern defined as set of (value, feature) tuples
|
| 205 |
+
segment (set): segment defined as a set of (value, feature) tuples
|
| 206 |
+
|
| 207 |
+
Returns:
|
| 208 |
+
bool: True iff all features in `ft_mask` are also in `ft_seg`; None
|
| 209 |
+
if segment is not valid
|
| 210 |
+
"""
|
| 211 |
+
features = set(features)
|
| 212 |
+
if self.seg_known(segment):
|
| 213 |
+
return features <= self.fts(segment)
|
| 214 |
+
else:
|
| 215 |
+
return None
|
| 216 |
+
|
| 217 |
+
def longest_one_seg_prefix(self, word, normalize=True):
|
| 218 |
+
"""Return longest Unicode IPA prefix of a word
|
| 219 |
+
|
| 220 |
+
Args:
|
| 221 |
+
word (unicode): input word as Unicode IPA string
|
| 222 |
+
|
| 223 |
+
Returns:
|
| 224 |
+
unicode: longest single-segment prefix of `word` in database
|
| 225 |
+
"""
|
| 226 |
+
if normalize:
|
| 227 |
+
word = FeatureTable.normalize(word)
|
| 228 |
+
|
| 229 |
+
for i in range(self.longest_seg, 0, -1):
|
| 230 |
+
if word[:i] in self.seg_dict:
|
| 231 |
+
return word[:i]
|
| 232 |
+
return ''
|
| 233 |
+
|
| 234 |
+
def validate_word(self, word):
|
| 235 |
+
"""Returns True if `word` consists exhaustively of valid IPA segments
|
| 236 |
+
|
| 237 |
+
Args:
|
| 238 |
+
word (unicode): input word as Unicode IPA string
|
| 239 |
+
|
| 240 |
+
Returns:
|
| 241 |
+
bool: True if `word` can be divided exhaustively into IPA segments
|
| 242 |
+
that exist in the database
|
| 243 |
+
|
| 244 |
+
"""
|
| 245 |
+
while word:
|
| 246 |
+
match = self.seg_regex.match(word)
|
| 247 |
+
if match:
|
| 248 |
+
word = word[len(match.group(0)):]
|
| 249 |
+
else:
|
| 250 |
+
# print('{}\t->\t{}\t'.format(orig, word).encode('utf-8'), file=sys.stderr)
|
| 251 |
+
return False
|
| 252 |
+
return True
|
| 253 |
+
|
| 254 |
+
def segs(self, word):
|
| 255 |
+
"""Returns a list of segments from a word
|
| 256 |
+
|
| 257 |
+
Args:
|
| 258 |
+
word (unicode): input word as Unicode IPA string
|
| 259 |
+
|
| 260 |
+
Returns:
|
| 261 |
+
list: list of strings corresponding to segments found in `word`
|
| 262 |
+
"""
|
| 263 |
+
return [m.group('all') for m in self.seg_regex.finditer(word)]
|
| 264 |
+
|
| 265 |
+
def word_fts(self, word):
|
| 266 |
+
"""Return featural analysis of `word`
|
| 267 |
+
|
| 268 |
+
Args:
|
| 269 |
+
word (unicode): one or more IPA segments
|
| 270 |
+
|
| 271 |
+
Returns:
|
| 272 |
+
list: list of lists (value, feature) tuples where each inner list
|
| 273 |
+
corresponds to a segment in `word`
|
| 274 |
+
"""
|
| 275 |
+
return list(map(self.fts, self.segs(word)))
|
| 276 |
+
|
| 277 |
+
def word_array(self, ft_names, word):
|
| 278 |
+
"""Return `word` as [-1, 0, 1] features in a NumPy array
|
| 279 |
+
|
| 280 |
+
Args:
|
| 281 |
+
ft_names (list): list of feature names in order
|
| 282 |
+
word (unicode): word as an IPA string
|
| 283 |
+
|
| 284 |
+
Returns:
|
| 285 |
+
ndarray: segments in rows, features in columns as [-1, 0 , 1]
|
| 286 |
+
"""
|
| 287 |
+
return word2array(ft_names, self.word_fts(word))
|
| 288 |
+
|
| 289 |
+
def seg_known(self, segment):
|
| 290 |
+
"""Return True if `segment` is in segment <=> features database
|
| 291 |
+
|
| 292 |
+
Args:
|
| 293 |
+
segment (unicode): consonant or vowel
|
| 294 |
+
|
| 295 |
+
Returns:
|
| 296 |
+
bool: True, if `segment` is in the database
|
| 297 |
+
"""
|
| 298 |
+
return segment in self.seg_dict
|
| 299 |
+
|
| 300 |
+
def segs_safe(self, word):
|
| 301 |
+
"""Return a list of segments (as strings) from a word
|
| 302 |
+
|
| 303 |
+
Characters that are not valid segments are included in the list as
|
| 304 |
+
individual characters.
|
| 305 |
+
|
| 306 |
+
Args:
|
| 307 |
+
word (unicode): word as an IPA string
|
| 308 |
+
|
| 309 |
+
Returns:
|
| 310 |
+
list: list of Unicode IPA strings corresponding to segments in
|
| 311 |
+
`word`
|
| 312 |
+
"""
|
| 313 |
+
segs = []
|
| 314 |
+
while word:
|
| 315 |
+
m = self.seg_regex.match(word)
|
| 316 |
+
if m:
|
| 317 |
+
segs.append(m.group(1))
|
| 318 |
+
word = word[len(m.group(1)):]
|
| 319 |
+
else:
|
| 320 |
+
segs.append(word[0])
|
| 321 |
+
word = word[1:]
|
| 322 |
+
return segs
|
| 323 |
+
|
| 324 |
+
def filter_segs(self, segs):
|
| 325 |
+
"""Given list of strings, return only those which are valid segments
|
| 326 |
+
|
| 327 |
+
Args:
|
| 328 |
+
segs (list): list of IPA Unicode strings
|
| 329 |
+
|
| 330 |
+
Return:
|
| 331 |
+
list: list of IPA Unicode strings identical to `segs` but with
|
| 332 |
+
invalid segments filtered out
|
| 333 |
+
"""
|
| 334 |
+
return list(filter(self.seg_known, segs))
|
| 335 |
+
|
| 336 |
+
def filter_string(self, word):
|
| 337 |
+
"""Return a string like the input but containing only legal IPA segments
|
| 338 |
+
|
| 339 |
+
Args:
|
| 340 |
+
word (unicode): input string to be filtered
|
| 341 |
+
|
| 342 |
+
Returns:
|
| 343 |
+
unicode: string identical to `word` but with invalid IPA segments
|
| 344 |
+
absent
|
| 345 |
+
|
| 346 |
+
"""
|
| 347 |
+
segs = [m.group(0) for m in self.seg_regex.finditer(word)]
|
| 348 |
+
return ''.join(segs)
|
| 349 |
+
|
| 350 |
+
def fts_intersection(self, segs):
|
| 351 |
+
"""Return the features shared by `segs`
|
| 352 |
+
|
| 353 |
+
Args:
|
| 354 |
+
segs (list): list of Unicode IPA segments
|
| 355 |
+
|
| 356 |
+
Returns:
|
| 357 |
+
set: set of (value, feature) tuples shared by the valid segments in
|
| 358 |
+
`segs`
|
| 359 |
+
"""
|
| 360 |
+
fts_vecs = [self.fts(s) for s in self.filter_segs(segs)]
|
| 361 |
+
return reduce(lambda a, b: a & b, fts_vecs)
|
| 362 |
+
|
| 363 |
+
def fts_match_any(self, fts, inv):
|
| 364 |
+
"""Return `True` if any segment in `inv` matches the features in `fts`
|
| 365 |
+
|
| 366 |
+
Args:
|
| 367 |
+
fts (list): a collection of (value, feature) tuples
|
| 368 |
+
inv (list): a collection of IPA segments represented as Unicode
|
| 369 |
+
strings
|
| 370 |
+
|
| 371 |
+
Returns:
|
| 372 |
+
bool: `True` if any segment in `inv` matches the features in `fts`
|
| 373 |
+
"""
|
| 374 |
+
return any([self.fts_match(fts, s) for s in inv])
|
| 375 |
+
|
| 376 |
+
def fts_match_all(self, fts, inv):
|
| 377 |
+
"""Return `True` if all segments in `inv` matches the features in fts
|
| 378 |
+
|
| 379 |
+
Args:
|
| 380 |
+
fts (list): a collection of (value, feature) tuples
|
| 381 |
+
inv (list): a collection of IPA segments represented as Unicode
|
| 382 |
+
strings
|
| 383 |
+
|
| 384 |
+
Returns:
|
| 385 |
+
bool: `True` if all segments in `inv` matches the features in `fts`
|
| 386 |
+
"""
|
| 387 |
+
return all([self.fts_match(fts, s) for s in inv])
|
| 388 |
+
|
| 389 |
+
def fts_contrast2(self, fs, ft_name, inv):
|
| 390 |
+
"""Return `True` if there is a segment in `inv` that contrasts in feature
|
| 391 |
+
`ft_name`.
|
| 392 |
+
|
| 393 |
+
Args:
|
| 394 |
+
fs (list): feature specifications used to filter `inv`.
|
| 395 |
+
ft_name (str): name of the feature where contrast must be present.
|
| 396 |
+
inv (list): collection of segments represented as Unicode segments.
|
| 397 |
+
|
| 398 |
+
Returns:
|
| 399 |
+
bool: `True` if two segments in `inv` are identical in features except
|
| 400 |
+
for feature `ft_name`
|
| 401 |
+
"""
|
| 402 |
+
inv_fts = [self.fts(x) for x in inv if set(fs) <= self.fts(x)]
|
| 403 |
+
for a in inv_fts:
|
| 404 |
+
for b in inv_fts:
|
| 405 |
+
if a != b:
|
| 406 |
+
diff = a ^ b
|
| 407 |
+
if len(diff) == 2:
|
| 408 |
+
if all([nm == ft_name for (_, nm) in diff]):
|
| 409 |
+
return True
|
| 410 |
+
return False
|
| 411 |
+
|
| 412 |
+
def fts_count(self, fts, inv):
|
| 413 |
+
"""Return the count of segments in an inventory matching a given
|
| 414 |
+
feature mask.
|
| 415 |
+
|
| 416 |
+
Args:
|
| 417 |
+
fts (set): feature mask given as a set of (value, feature) tuples
|
| 418 |
+
inv (set): inventory of segments (as Unicode IPA strings)
|
| 419 |
+
|
| 420 |
+
Returns:
|
| 421 |
+
int: number of segments in `inv` that match feature mask `fts`
|
| 422 |
+
"""
|
| 423 |
+
return len(list(filter(lambda s: self.fts_match(fts, s), inv)))
|
| 424 |
+
|
| 425 |
+
def match_pattern(self, pat, word):
|
| 426 |
+
"""Implements fixed-width pattern matching.
|
| 427 |
+
|
| 428 |
+
Matches just in case pattern is the same length (in segments) as the
|
| 429 |
+
word and each of the segments in the pattern is a featural subset of the
|
| 430 |
+
corresponding segment in the word. Matches return the corresponding list
|
| 431 |
+
of feature sets; failed matches return None.
|
| 432 |
+
|
| 433 |
+
Args:
|
| 434 |
+
pat (list): pattern consisting of a sequence of sets of (value,
|
| 435 |
+
feature) tuples
|
| 436 |
+
word (unicode): a Unicode IPA string consisting of zero or more
|
| 437 |
+
segments
|
| 438 |
+
|
| 439 |
+
Returns:
|
| 440 |
+
list: corresponding list of feature sets or, if there is no match,
|
| 441 |
+
None
|
| 442 |
+
"""
|
| 443 |
+
segs = self.word_fts(word)
|
| 444 |
+
if len(pat) != len(segs):
|
| 445 |
+
return None
|
| 446 |
+
else:
|
| 447 |
+
if all([set(p) <= s for (p, s) in zip(pat, segs)]):
|
| 448 |
+
return segs
|
| 449 |
+
|
| 450 |
+
def match_pattern_seq(self, pat, const):
|
| 451 |
+
"""Implements limited pattern matching. Matches just in case pattern is
|
| 452 |
+
the same length (in segments) as the constituent and each of the
|
| 453 |
+
segments in the pattern is a featural subset of the corresponding
|
| 454 |
+
segment in the word.
|
| 455 |
+
|
| 456 |
+
Args:
|
| 457 |
+
pat (list): pattern consisting of a list of sets of (value, feature)
|
| 458 |
+
tuples.
|
| 459 |
+
const (list): a sequence of Unicode IPA strings consisting of zero
|
| 460 |
+
or more segments.
|
| 461 |
+
|
| 462 |
+
Returns:
|
| 463 |
+
bool: `True` if `const` matches `pat`
|
| 464 |
+
"""
|
| 465 |
+
segs = [self.fts(s) for s in const]
|
| 466 |
+
if len(pat) != len(segs):
|
| 467 |
+
return False
|
| 468 |
+
else:
|
| 469 |
+
return all([set(p) <= s for (p, s) in zip(pat, segs)])
|
| 470 |
+
|
| 471 |
+
def all_segs_matching_fts(self, fts):
|
| 472 |
+
"""Return segments matching a feature mask, both as (value, feature)
|
| 473 |
+
tuples (sorted in reverse order by length).
|
| 474 |
+
|
| 475 |
+
Args:
|
| 476 |
+
fts (list): feature mask as (value, feature) tuples.
|
| 477 |
+
|
| 478 |
+
Returns:
|
| 479 |
+
list: segments matching `fts`, sorted in reverse order by length
|
| 480 |
+
"""
|
| 481 |
+
matching_segs = []
|
| 482 |
+
for seg, pairs in self.segments:
|
| 483 |
+
if set(fts) <= set(pairs):
|
| 484 |
+
matching_segs.append(seg)
|
| 485 |
+
return sorted(matching_segs, key=lambda x: len(x), reverse=True)
|
| 486 |
+
|
| 487 |
+
def compile_regex_from_str(self, ft_str):
|
| 488 |
+
"""Given a string describing features masks for a sequence of segments,
|
| 489 |
+
return a regex matching the corresponding strings.
|
| 490 |
+
|
| 491 |
+
Args:
|
| 492 |
+
ft_str (str): feature masks, each enclosed in square brackets, in
|
| 493 |
+
which the features are delimited by any standard delimiter.
|
| 494 |
+
|
| 495 |
+
Returns:
|
| 496 |
+
Pattern: regular expression pattern equivalent to `ft_str`
|
| 497 |
+
"""
|
| 498 |
+
|
| 499 |
+
sequence = []
|
| 500 |
+
for m in re.finditer(r'\[([^]]+)\]', ft_str):
|
| 501 |
+
ft_mask = fts(m.group(1))
|
| 502 |
+
segs = self.all_segs_matching_fts(ft_mask)
|
| 503 |
+
sub_pat = '({})'.format('|'.join(segs))
|
| 504 |
+
sequence.append(sub_pat)
|
| 505 |
+
pattern = ''.join(sequence)
|
| 506 |
+
regex = re.compile(pattern)
|
| 507 |
+
return regex
|
| 508 |
+
|
| 509 |
+
def segment_to_vector(self, seg):
|
| 510 |
+
"""Given a Unicode IPA segment, return a list of feature specificiations
|
| 511 |
+
in cannonical order.
|
| 512 |
+
|
| 513 |
+
Args:
|
| 514 |
+
seg (unicode): IPA consonant or vowel
|
| 515 |
+
|
| 516 |
+
Returns:
|
| 517 |
+
list: feature specifications ('+'/'-'/'0') in the order from
|
| 518 |
+
`FeatureTable.names`
|
| 519 |
+
"""
|
| 520 |
+
ft_dict = {ft: val for (val, ft) in self.fts(seg)}
|
| 521 |
+
return [ft_dict[name] for name in self.names]
|
| 522 |
+
|
| 523 |
+
def tensor_to_numeric(self, t):
|
| 524 |
+
return list(map(lambda a:
|
| 525 |
+
list(map(lambda b: {'+': 1, '-': -1, '0': 0}[b], a)), t))
|
| 526 |
+
|
| 527 |
+
def word_to_vector_list(self, word, numeric=False, xsampa=False):
|
| 528 |
+
"""Return a list of feature vectors, given a Unicode IPA word.
|
| 529 |
+
|
| 530 |
+
Args:
|
| 531 |
+
word (unicode): string in IPA
|
| 532 |
+
numeric (bool): if True, return features as numeric values instead
|
| 533 |
+
of strings
|
| 534 |
+
|
| 535 |
+
Returns:
|
| 536 |
+
list: a list of lists of '+'/'-'/'0' or 1/-1/0
|
| 537 |
+
"""
|
| 538 |
+
if xsampa:
|
| 539 |
+
word = self.xsampa.convert(word)
|
| 540 |
+
tensor = list(map(self.segment_to_vector, self.segs(word)))
|
| 541 |
+
if numeric:
|
| 542 |
+
return self.tensor_to_numeric(tensor)
|
| 543 |
+
else:
|
| 544 |
+
return tensor
|
panphon/collapse.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import (absolute_import, division, print_function,
|
| 2 |
+
unicode_literals)
|
| 3 |
+
|
| 4 |
+
import os.path
|
| 5 |
+
|
| 6 |
+
import pkg_resources
|
| 7 |
+
import yaml
|
| 8 |
+
|
| 9 |
+
from panphon import _panphon
|
| 10 |
+
from panphon import permissive
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class Collapser(object):
|
| 14 |
+
def __init__(self, tablename='dogolpolsky_prime.yml', feature_set='spe+', feature_model='strict'):
|
| 15 |
+
fm = {'strict': _panphon.FeatureTable,
|
| 16 |
+
'permissive': permissive.PermissiveFeatureTable}
|
| 17 |
+
self.fm = fm[feature_model](feature_set=feature_set)
|
| 18 |
+
self.rules = self._load_table(tablename)
|
| 19 |
+
|
| 20 |
+
def _load_table(self, tablename):
|
| 21 |
+
fn = os.path.join('data', tablename)
|
| 22 |
+
fn = pkg_resources.resource_filename(__name__, fn)
|
| 23 |
+
with open(fn, 'r') as f:
|
| 24 |
+
rules = []
|
| 25 |
+
table = yaml.load(f.read(), Loader=yaml.FullLoader)
|
| 26 |
+
for rule in table:
|
| 27 |
+
rules.append((_panphon.fts(rule['def']), rule['label']))
|
| 28 |
+
return rules
|
| 29 |
+
|
| 30 |
+
def collapse(self, s):
|
| 31 |
+
segs = []
|
| 32 |
+
for seg in self.fm.seg_regex.findall(s):
|
| 33 |
+
fts = self.fm.fts(seg)
|
| 34 |
+
for mask, label in self.rules:
|
| 35 |
+
if self.fm.match(mask, fts):
|
| 36 |
+
segs.append(label)
|
| 37 |
+
break
|
| 38 |
+
return ''.join(segs)
|
panphon/data/asjp.yml
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-
|
| 2 |
+
name: voiceless labiodental fricative
|
| 3 |
+
def: "[-son +cont -voi +strid +ant -cor +lab]"
|
| 4 |
+
label: f
|
| 5 |
+
-
|
| 6 |
+
name: voiced labiodental fricative
|
| 7 |
+
def: "[-son +cont +voi +strid +ant -cor +lab]"
|
| 8 |
+
label: v
|
| 9 |
+
-
|
| 10 |
+
name: bilabial nasal
|
| 11 |
+
def: "[+son +nas +ant -cor +lab]"
|
| 12 |
+
label: m
|
| 13 |
+
-
|
| 14 |
+
name: voiceless bilabial obstruent
|
| 15 |
+
def: "[-son -voi +ant -cor +lab]"
|
| 16 |
+
label: p
|
| 17 |
+
-
|
| 18 |
+
name: voiced bilabial obstruent
|
| 19 |
+
def: "[-son +voi +ant -cor +lab]"
|
| 20 |
+
label: b
|
| 21 |
+
-
|
| 22 |
+
name: dental fricatives
|
| 23 |
+
def: "[-son +cont +ant +cor +dist]"
|
| 24 |
+
label: 8
|
| 25 |
+
-
|
| 26 |
+
name: dental nasal
|
| 27 |
+
def: "[+son +nas -cont +ant +cor +dist]"
|
| 28 |
+
label: 4
|
| 29 |
+
-
|
| 30 |
+
name: voiceless alveolar stop
|
| 31 |
+
def: "[-son -cont -voi +ant +cor -dist]"
|
| 32 |
+
label: t
|
| 33 |
+
-
|
| 34 |
+
name: voiced alveolar stop
|
| 35 |
+
def: "[-son -cont +voi +ant +cor -dist]"
|
| 36 |
+
label: d
|
| 37 |
+
-
|
| 38 |
+
name: voiceless alveolar fricative
|
| 39 |
+
def: "[-son -cont -voi +ant +cor -dist]"
|
| 40 |
+
label: s
|
| 41 |
+
-
|
| 42 |
+
name: voiced alveolar fricative
|
| 43 |
+
def: "[-son -cont +voi +ant +cor -dist]"
|
| 44 |
+
label: z
|
| 45 |
+
-
|
| 46 |
+
name: alveolar affricate
|
| 47 |
+
def: "[-son +delrel +ant +cor -dist]"
|
| 48 |
+
label: c
|
| 49 |
+
-
|
| 50 |
+
name: alveolar nasal
|
| 51 |
+
def: "[+son -cont +nas +ant +cor -dist]"
|
| 52 |
+
label: n
|
| 53 |
+
-
|
| 54 |
+
name: voiceless postalveolar fricative
|
| 55 |
+
def: "[-son +cont -voi -ant +cor +dist]"
|
| 56 |
+
label: S
|
| 57 |
+
-
|
| 58 |
+
name: voiced postalveolar fricative
|
| 59 |
+
def: "[-son +cont +voi -ant +cor +dist]"
|
| 60 |
+
label: Z
|
| 61 |
+
-
|
| 62 |
+
name: alveolar affricate
|
| 63 |
+
def: "[-son +delrel -voi -ant +cor +dist]"
|
| 64 |
+
label: C
|
| 65 |
+
-
|
| 66 |
+
name: alveolar affricate
|
| 67 |
+
def: "[-son +delrel +voi -ant +cor +dist]"
|
| 68 |
+
label: j
|
| 69 |
+
-
|
| 70 |
+
name: palatal stop
|
| 71 |
+
def: "[-son -cont -delrel -ant -cor +hi -lo -back]"
|
| 72 |
+
label: T
|
| 73 |
+
-
|
| 74 |
+
name: palatal nasal
|
| 75 |
+
def: "[+son -cont +nas -ant -cor +hi -lo -back]"
|
| 76 |
+
label: 5
|
| 77 |
+
-
|
| 78 |
+
name: voiceless velar stop
|
| 79 |
+
def: "[-son -cont -delrel -voi -ant -cor +hi -lo +back]"
|
| 80 |
+
label: k
|
| 81 |
+
-
|
| 82 |
+
name: voiced velar stop
|
| 83 |
+
def: "[-son -cont -delrel +voi -ant -cor +hi -lo +back]"
|
| 84 |
+
label: g
|
| 85 |
+
-
|
| 86 |
+
name: velar fricative
|
| 87 |
+
def: "[-son +cont -ant -cor +hi -lo +back]"
|
| 88 |
+
label: x
|
| 89 |
+
-
|
| 90 |
+
name: velar nasal
|
| 91 |
+
def: "[+son -cont +nas -ant -cor +hi -lo +back]"
|
| 92 |
+
label: N
|
| 93 |
+
-
|
| 94 |
+
name: voiceless uvular stop
|
| 95 |
+
def: "[-son -cont -delrel -voi -ant -cor -hi -lo +back]"
|
| 96 |
+
label: q
|
| 97 |
+
-
|
| 98 |
+
name: voiced uvular stop
|
| 99 |
+
def: "[-son -cont -delrel +voi -ant -cor -hi -lo +back]"
|
| 100 |
+
label: G
|
| 101 |
+
-
|
| 102 |
+
name: uvular and pharyngeal fricative
|
| 103 |
+
def: "[-son +cont -delrel -ant -cor -hi +back]"
|
| 104 |
+
label: X
|
| 105 |
+
-
|
| 106 |
+
name: glottal stop
|
| 107 |
+
def: "[-syl +son -cont +cg -ant -cor -hi -lo -back]"
|
| 108 |
+
label: 7
|
| 109 |
+
-
|
| 110 |
+
name: glottal fricative
|
| 111 |
+
def: "[-syl +son +cont -ant -cor -hi -lo -back]"
|
| 112 |
+
label: h
|
| 113 |
+
-
|
| 114 |
+
name: coronal lateral
|
| 115 |
+
def: "[+son +cont +lat +ant +cor]"
|
| 116 |
+
label: l
|
| 117 |
+
-
|
| 118 |
+
name: all non-coronal laterals
|
| 119 |
+
def: "[+son +cont +lat]"
|
| 120 |
+
label: L
|
| 121 |
+
-
|
| 122 |
+
name: labiovelar approximant
|
| 123 |
+
def: "[-syl +son -cons +lab -ant -cor +hi -lo +back +round]"
|
| 124 |
+
label: w
|
| 125 |
+
-
|
| 126 |
+
name: palatal approximant
|
| 127 |
+
def: "[-syl +son -cons -ant -cor +hi -lo -back]"
|
| 128 |
+
label: j
|
| 129 |
+
-
|
| 130 |
+
name: coronal trill or tap
|
| 131 |
+
def: "[+son +cons +cont -lat +ant +cor]"
|
| 132 |
+
label: r
|
| 133 |
+
-
|
| 134 |
+
name: coronal approximant
|
| 135 |
+
def: "[+son -cons +cont -lat +ant +cor]"
|
| 136 |
+
label: r
|
panphon/data/diacritic_definitions.yml
ADDED
|
@@ -0,0 +1,669 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# DIACRITICS AND MODIFIERS
|
| 2 |
+
diacritics:
|
| 3 |
+
|
| 4 |
+
# Airstream mechanism
|
| 5 |
+
|
| 6 |
+
- marker: ʼ
|
| 7 |
+
name: Ejective
|
| 8 |
+
position: post
|
| 9 |
+
conditions:
|
| 10 |
+
- son: "-"
|
| 11 |
+
voi: "-"
|
| 12 |
+
content:
|
| 13 |
+
cg: "+"
|
| 14 |
+
|
| 15 |
+
# Laryngeal features
|
| 16 |
+
|
| 17 |
+
- marker: ̥
|
| 18 |
+
name: Voiceless
|
| 19 |
+
position: post
|
| 20 |
+
conditions:
|
| 21 |
+
- son: "+"
|
| 22 |
+
voi: "+"
|
| 23 |
+
content:
|
| 24 |
+
voi: "-"
|
| 25 |
+
|
| 26 |
+
- marker: ʰ
|
| 27 |
+
name: Aspirated
|
| 28 |
+
position: post
|
| 29 |
+
conditions:
|
| 30 |
+
- son: "-"
|
| 31 |
+
cg: "-"
|
| 32 |
+
cont: "-"
|
| 33 |
+
- cont: "+"
|
| 34 |
+
son: "-"
|
| 35 |
+
voi: "-"
|
| 36 |
+
content:
|
| 37 |
+
sg: "+"
|
| 38 |
+
|
| 39 |
+
# Backness modifications
|
| 40 |
+
|
| 41 |
+
- marker: ̟
|
| 42 |
+
name: "Advanced"
|
| 43 |
+
position: post
|
| 44 |
+
conditions:
|
| 45 |
+
- syl: "-"
|
| 46 |
+
- syl: "+"
|
| 47 |
+
content: {}
|
| 48 |
+
|
| 49 |
+
- marker: ̠
|
| 50 |
+
name: "Retracted"
|
| 51 |
+
position: post
|
| 52 |
+
conditions:
|
| 53 |
+
- syl: "-"
|
| 54 |
+
- syl: "+"
|
| 55 |
+
content: {}
|
| 56 |
+
|
| 57 |
+
- marker: ̈
|
| 58 |
+
name: "Centralized"
|
| 59 |
+
position: post
|
| 60 |
+
conditions:
|
| 61 |
+
- syl: "+"
|
| 62 |
+
exclude:
|
| 63 |
+
- ə
|
| 64 |
+
content: {}
|
| 65 |
+
|
| 66 |
+
# Syllabicity
|
| 67 |
+
|
| 68 |
+
- marker: ̩
|
| 69 |
+
name: "Syllabic"
|
| 70 |
+
position: post
|
| 71 |
+
conditions:
|
| 72 |
+
- syl: "-"
|
| 73 |
+
cont: "+"
|
| 74 |
+
delrel: "-"
|
| 75 |
+
- syl: "-"
|
| 76 |
+
son: "+"
|
| 77 |
+
exclude:
|
| 78 |
+
- ʔ
|
| 79 |
+
content:
|
| 80 |
+
syl: "+"
|
| 81 |
+
|
| 82 |
+
- marker: ̯
|
| 83 |
+
name: "Non-syllabic"
|
| 84 |
+
position: post
|
| 85 |
+
conditions:
|
| 86 |
+
- syl: "+"
|
| 87 |
+
content:
|
| 88 |
+
syl: "-"
|
| 89 |
+
|
| 90 |
+
# Rhoticity
|
| 91 |
+
|
| 92 |
+
- marker: ˞
|
| 93 |
+
name: "Rhotacized"
|
| 94 |
+
position: post
|
| 95 |
+
conditions:
|
| 96 |
+
- syl: "+"
|
| 97 |
+
content:
|
| 98 |
+
ant: "-"
|
| 99 |
+
hi: "+"
|
| 100 |
+
round: "+"
|
| 101 |
+
|
| 102 |
+
# Voice quality
|
| 103 |
+
|
| 104 |
+
- marker: ̤
|
| 105 |
+
name: "Breathy Voiced"
|
| 106 |
+
position: post
|
| 107 |
+
conditions:
|
| 108 |
+
- voi: "+"
|
| 109 |
+
exclude:
|
| 110 |
+
- ʔ
|
| 111 |
+
content:
|
| 112 |
+
sg: "+"
|
| 113 |
+
|
| 114 |
+
- marker: ̰
|
| 115 |
+
name: "Creaky Voiced"
|
| 116 |
+
position: post
|
| 117 |
+
conditions:
|
| 118 |
+
- voi: "+"
|
| 119 |
+
exclude:
|
| 120 |
+
- ʔ
|
| 121 |
+
content:
|
| 122 |
+
cg: "+"
|
| 123 |
+
|
| 124 |
+
- marker: ˀ # not IPA
|
| 125 |
+
name: Glottalized
|
| 126 |
+
position: post
|
| 127 |
+
conditions:
|
| 128 |
+
- cg: "-"
|
| 129 |
+
content:
|
| 130 |
+
cg: "+"
|
| 131 |
+
|
| 132 |
+
- marker: ˀ # not IPA
|
| 133 |
+
name: Preglottalized
|
| 134 |
+
position: pre
|
| 135 |
+
conditions:
|
| 136 |
+
- syl: "-"
|
| 137 |
+
- cg: "-"
|
| 138 |
+
content:
|
| 139 |
+
cg: "+"
|
| 140 |
+
|
| 141 |
+
# Secondary articulations
|
| 142 |
+
|
| 143 |
+
- marker: ̼
|
| 144 |
+
name: Linguolabial
|
| 145 |
+
position: post
|
| 146 |
+
conditions:
|
| 147 |
+
- cor: "+"
|
| 148 |
+
ant: "+"
|
| 149 |
+
cont: "-"
|
| 150 |
+
delrel: "-"
|
| 151 |
+
- cont: "+"
|
| 152 |
+
son: "-"
|
| 153 |
+
cor: "+"
|
| 154 |
+
ant: "+"
|
| 155 |
+
strid: "-"
|
| 156 |
+
delrel: "-"
|
| 157 |
+
content:
|
| 158 |
+
lab: "+"
|
| 159 |
+
|
| 160 |
+
- marker: ʷ
|
| 161 |
+
name: Labialized
|
| 162 |
+
position: post
|
| 163 |
+
conditions:
|
| 164 |
+
- syl: "-"
|
| 165 |
+
exclude:
|
| 166 |
+
- w
|
| 167 |
+
- ʍ
|
| 168 |
+
- ɥ
|
| 169 |
+
content:
|
| 170 |
+
round: "+"
|
| 171 |
+
back: "+"
|
| 172 |
+
hi: "+"
|
| 173 |
+
|
| 174 |
+
- marker: ʲ
|
| 175 |
+
name: Palatalized
|
| 176 |
+
position: post
|
| 177 |
+
conditions:
|
| 178 |
+
- syl: "-"
|
| 179 |
+
exclude:
|
| 180 |
+
- j
|
| 181 |
+
- ɥ
|
| 182 |
+
content:
|
| 183 |
+
hi: "+"
|
| 184 |
+
back: "-"
|
| 185 |
+
|
| 186 |
+
- marker: ᶣ
|
| 187 |
+
name: Labiopalatalized
|
| 188 |
+
position: post
|
| 189 |
+
conditions:
|
| 190 |
+
- lab: "-"
|
| 191 |
+
syl: "-"
|
| 192 |
+
content:
|
| 193 |
+
hi: "+"
|
| 194 |
+
back: "-"
|
| 195 |
+
round: "+"
|
| 196 |
+
|
| 197 |
+
- marker: ˠ
|
| 198 |
+
name: Velarized
|
| 199 |
+
position: post
|
| 200 |
+
conditions:
|
| 201 |
+
- syl: "-"
|
| 202 |
+
- hi: "-"
|
| 203 |
+
- back: "-"
|
| 204 |
+
exclude:
|
| 205 |
+
- k
|
| 206 |
+
- ɡ
|
| 207 |
+
- ŋ
|
| 208 |
+
- x
|
| 209 |
+
- ɣ
|
| 210 |
+
- ɰ
|
| 211 |
+
- ʟ
|
| 212 |
+
content:
|
| 213 |
+
hi: "+"
|
| 214 |
+
back: "+"
|
| 215 |
+
|
| 216 |
+
- marker: ˤ
|
| 217 |
+
name: Pharyngealized
|
| 218 |
+
position: post
|
| 219 |
+
conditions:
|
| 220 |
+
- {}
|
| 221 |
+
exclude:
|
| 222 |
+
- ʔ
|
| 223 |
+
- ʕ
|
| 224 |
+
- ħ
|
| 225 |
+
content:
|
| 226 |
+
lo: "+"
|
| 227 |
+
back: "+"
|
| 228 |
+
|
| 229 |
+
- marker: ̴
|
| 230 |
+
name: "Velarized or Pharyngealized"
|
| 231 |
+
position: post
|
| 232 |
+
conditions:
|
| 233 |
+
- cor: "+"
|
| 234 |
+
lat: "+"
|
| 235 |
+
delrel: "-"
|
| 236 |
+
content:
|
| 237 |
+
hi: "+"
|
| 238 |
+
back: "+"
|
| 239 |
+
|
| 240 |
+
# Height modifications
|
| 241 |
+
|
| 242 |
+
- marker: ̝
|
| 243 |
+
name: Raised
|
| 244 |
+
position: post
|
| 245 |
+
conditions:
|
| 246 |
+
- cont: "+"
|
| 247 |
+
content: {}
|
| 248 |
+
|
| 249 |
+
- marker: ̞
|
| 250 |
+
name: Lowered
|
| 251 |
+
position: post
|
| 252 |
+
conditions:
|
| 253 |
+
- cont: "+"
|
| 254 |
+
content: {}
|
| 255 |
+
|
| 256 |
+
# Tongue root state
|
| 257 |
+
|
| 258 |
+
- marker: ̘
|
| 259 |
+
name: ATR
|
| 260 |
+
position: post
|
| 261 |
+
conditions:
|
| 262 |
+
- syl: "+"
|
| 263 |
+
content:
|
| 264 |
+
tense: "+"
|
| 265 |
+
|
| 266 |
+
- marker: ̙
|
| 267 |
+
name: RTR
|
| 268 |
+
position: post
|
| 269 |
+
conditions:
|
| 270 |
+
- syl: "+"
|
| 271 |
+
content:
|
| 272 |
+
tense: "-"
|
| 273 |
+
|
| 274 |
+
# Coronal modifications
|
| 275 |
+
|
| 276 |
+
- marker: ̺
|
| 277 |
+
name: Apical
|
| 278 |
+
position: post
|
| 279 |
+
conditions:
|
| 280 |
+
- cor: "+"
|
| 281 |
+
content:
|
| 282 |
+
distr: "-"
|
| 283 |
+
|
| 284 |
+
- marker: ̻
|
| 285 |
+
name: Laminal
|
| 286 |
+
position: post
|
| 287 |
+
conditions:
|
| 288 |
+
- cor: "+"
|
| 289 |
+
content:
|
| 290 |
+
distr: "+"
|
| 291 |
+
|
| 292 |
+
# Nasality
|
| 293 |
+
|
| 294 |
+
- marker: ̃
|
| 295 |
+
name: Nasalized
|
| 296 |
+
position: post
|
| 297 |
+
conditions:
|
| 298 |
+
- voi: "+"
|
| 299 |
+
nas: "-"
|
| 300 |
+
exclude:
|
| 301 |
+
- ʔ
|
| 302 |
+
- h
|
| 303 |
+
content:
|
| 304 |
+
nas: "+"
|
| 305 |
+
|
| 306 |
+
# Properties of release
|
| 307 |
+
|
| 308 |
+
- marker: ⁿ
|
| 309 |
+
name: "Nasal Release"
|
| 310 |
+
position: post
|
| 311 |
+
conditions:
|
| 312 |
+
- son: "-"
|
| 313 |
+
cont: "-"
|
| 314 |
+
delrel: "-"
|
| 315 |
+
content:
|
| 316 |
+
nas: "+"
|
| 317 |
+
|
| 318 |
+
- marker: ˡ
|
| 319 |
+
name: "Lateral Release"
|
| 320 |
+
position: post
|
| 321 |
+
conditions:
|
| 322 |
+
- son: "-"
|
| 323 |
+
cont: "-"
|
| 324 |
+
cor: "+"
|
| 325 |
+
delrel: "-"
|
| 326 |
+
content:
|
| 327 |
+
lat: "+"
|
| 328 |
+
delrel: "+"
|
| 329 |
+
|
| 330 |
+
# Suprasegmentals
|
| 331 |
+
|
| 332 |
+
- marker: ː
|
| 333 |
+
name: Long
|
| 334 |
+
position: post
|
| 335 |
+
conditions:
|
| 336 |
+
- long: "-"
|
| 337 |
+
content:
|
| 338 |
+
long: "+"
|
| 339 |
+
|
| 340 |
+
- marker: ̆
|
| 341 |
+
name: Extra Short
|
| 342 |
+
position: post
|
| 343 |
+
conditions:
|
| 344 |
+
- syl: "+"
|
| 345 |
+
content:
|
| 346 |
+
long: "-"
|
| 347 |
+
|
| 348 |
+
# COMBINATIONS OF DIACRITICS AND MODIFIERS
|
| 349 |
+
combinations:
|
| 350 |
+
|
| 351 |
+
# Quantity
|
| 352 |
+
|
| 353 |
+
- name: Long Labialized
|
| 354 |
+
combines:
|
| 355 |
+
- Long
|
| 356 |
+
- Labialized
|
| 357 |
+
|
| 358 |
+
- name: Long Palatalized
|
| 359 |
+
combines:
|
| 360 |
+
- Long
|
| 361 |
+
- Palatalized
|
| 362 |
+
|
| 363 |
+
- name: Long Velarized
|
| 364 |
+
combines:
|
| 365 |
+
- Long
|
| 366 |
+
- Velarized
|
| 367 |
+
|
| 368 |
+
- name: Long Pharyngealized
|
| 369 |
+
combines:
|
| 370 |
+
- Long
|
| 371 |
+
- Pharyngealized
|
| 372 |
+
|
| 373 |
+
# Airstream mechanisms
|
| 374 |
+
|
| 375 |
+
## Ejective
|
| 376 |
+
|
| 377 |
+
- name: Ejective Labialized
|
| 378 |
+
combines:
|
| 379 |
+
- Ejective
|
| 380 |
+
- Labialized
|
| 381 |
+
|
| 382 |
+
- name: Ejective Palatalized
|
| 383 |
+
combines:
|
| 384 |
+
- Ejective
|
| 385 |
+
- Palatalized
|
| 386 |
+
|
| 387 |
+
- name: Ejective Long
|
| 388 |
+
combines:
|
| 389 |
+
- Ejective
|
| 390 |
+
- Long
|
| 391 |
+
|
| 392 |
+
# Laryngeal features
|
| 393 |
+
|
| 394 |
+
## Voiceless
|
| 395 |
+
|
| 396 |
+
- name: Voiceless Labialized
|
| 397 |
+
combines:
|
| 398 |
+
- Voiceless
|
| 399 |
+
- Labialized
|
| 400 |
+
|
| 401 |
+
- name: Voiceless Palatalized
|
| 402 |
+
combines:
|
| 403 |
+
- Voiceless
|
| 404 |
+
- Palatalized
|
| 405 |
+
|
| 406 |
+
- name: Voiceless Velarized
|
| 407 |
+
combines:
|
| 408 |
+
- Voiceless
|
| 409 |
+
- Velarized
|
| 410 |
+
|
| 411 |
+
- name: Voiceless Pharyngealized
|
| 412 |
+
combines:
|
| 413 |
+
- Voiceless
|
| 414 |
+
- Pharyngealized
|
| 415 |
+
|
| 416 |
+
- name: Voiceless Long
|
| 417 |
+
combines:
|
| 418 |
+
- Voiceless
|
| 419 |
+
- Long
|
| 420 |
+
|
| 421 |
+
## Aspirated
|
| 422 |
+
|
| 423 |
+
- name: Aspirated Labialized
|
| 424 |
+
combines:
|
| 425 |
+
- Aspirated
|
| 426 |
+
- Labialized
|
| 427 |
+
|
| 428 |
+
- name: Aspirated Palatalized
|
| 429 |
+
combines:
|
| 430 |
+
- Aspirated
|
| 431 |
+
- Palatalized
|
| 432 |
+
|
| 433 |
+
- name: Aspirated Velarized
|
| 434 |
+
combines:
|
| 435 |
+
- Aspirated
|
| 436 |
+
- Velarized
|
| 437 |
+
|
| 438 |
+
- name: Aspirated Labiopalatalized
|
| 439 |
+
combines:
|
| 440 |
+
- Aspirated
|
| 441 |
+
- Labiopalatalized
|
| 442 |
+
|
| 443 |
+
- name: Aspirated Pharyngealized
|
| 444 |
+
combines:
|
| 445 |
+
- Aspirated
|
| 446 |
+
- Pharyngealized
|
| 447 |
+
|
| 448 |
+
- name: Aspirated Long
|
| 449 |
+
combines:
|
| 450 |
+
- Aspirated
|
| 451 |
+
- Long
|
| 452 |
+
|
| 453 |
+
# Syllabicity
|
| 454 |
+
|
| 455 |
+
- name: Syllabic Labialized
|
| 456 |
+
combines:
|
| 457 |
+
- Syllabic
|
| 458 |
+
- Labialized
|
| 459 |
+
|
| 460 |
+
# Rhoticity
|
| 461 |
+
|
| 462 |
+
# Voice quality
|
| 463 |
+
|
| 464 |
+
## Breathy voice
|
| 465 |
+
|
| 466 |
+
- name: Breathy Devoiced
|
| 467 |
+
combines:
|
| 468 |
+
- "Breathy Voiced"
|
| 469 |
+
- Voiceless
|
| 470 |
+
|
| 471 |
+
- name: Breathy Labialized
|
| 472 |
+
combines:
|
| 473 |
+
- "Breathy Voiced"
|
| 474 |
+
- Labialized
|
| 475 |
+
|
| 476 |
+
- name: Breathy Palatalized
|
| 477 |
+
combines:
|
| 478 |
+
- "Breathy Voiced"
|
| 479 |
+
- Palatalized
|
| 480 |
+
|
| 481 |
+
- name: Breathy Velarized
|
| 482 |
+
combines:
|
| 483 |
+
- "Breathy Voiced"
|
| 484 |
+
- Velarized
|
| 485 |
+
|
| 486 |
+
- name: Breathy Pharyngealized
|
| 487 |
+
combines:
|
| 488 |
+
- "Breathy Voiced"
|
| 489 |
+
- Pharyngealized
|
| 490 |
+
|
| 491 |
+
- name: Breathy Long
|
| 492 |
+
combines:
|
| 493 |
+
- "Breathy Voiced"
|
| 494 |
+
- Long
|
| 495 |
+
|
| 496 |
+
## Creaky voice
|
| 497 |
+
|
| 498 |
+
- name: Creaky Labialized
|
| 499 |
+
combines:
|
| 500 |
+
- Creaky Voiced
|
| 501 |
+
- Labialized
|
| 502 |
+
|
| 503 |
+
- name: Creaky Palatalized
|
| 504 |
+
combines:
|
| 505 |
+
- Creaky Voiced
|
| 506 |
+
- Palatalized
|
| 507 |
+
|
| 508 |
+
- name: Creaky Velarized
|
| 509 |
+
combines:
|
| 510 |
+
- Creaky Voiced
|
| 511 |
+
- Velarized
|
| 512 |
+
|
| 513 |
+
- name: Creaky Pharyngealized
|
| 514 |
+
combines:
|
| 515 |
+
- Creaky Voiced
|
| 516 |
+
- Pharyngealized
|
| 517 |
+
|
| 518 |
+
- name: Creaky Long
|
| 519 |
+
combines:
|
| 520 |
+
- Creaky Voiced
|
| 521 |
+
- Long
|
| 522 |
+
|
| 523 |
+
# Secondary articulations
|
| 524 |
+
|
| 525 |
+
## Linguolabial
|
| 526 |
+
|
| 527 |
+
## Labialized
|
| 528 |
+
|
| 529 |
+
- name: Labialized Aspirated
|
| 530 |
+
combines:
|
| 531 |
+
- Labialized
|
| 532 |
+
- Aspirated
|
| 533 |
+
|
| 534 |
+
- name: Labialized Aspirated Long
|
| 535 |
+
combines:
|
| 536 |
+
- Labialized
|
| 537 |
+
- Aspirated
|
| 538 |
+
- Long
|
| 539 |
+
|
| 540 |
+
- name: Labialized Ejective
|
| 541 |
+
combines:
|
| 542 |
+
- Labialized
|
| 543 |
+
- Ejective
|
| 544 |
+
|
| 545 |
+
- name: Labialized Glottalized
|
| 546 |
+
combines:
|
| 547 |
+
- Labialized
|
| 548 |
+
- Glottalized
|
| 549 |
+
|
| 550 |
+
- name: Labialized Velarized
|
| 551 |
+
combines:
|
| 552 |
+
- Labialized
|
| 553 |
+
- Velarized
|
| 554 |
+
|
| 555 |
+
- name: Labialized Velarized Aspirated
|
| 556 |
+
combines:
|
| 557 |
+
- Labialized
|
| 558 |
+
- Velarized
|
| 559 |
+
- Aspirated
|
| 560 |
+
|
| 561 |
+
- name: Labialized Pharyngealized
|
| 562 |
+
combines:
|
| 563 |
+
- Labialized
|
| 564 |
+
- Pharyngealized
|
| 565 |
+
|
| 566 |
+
- name: Labialized Pharyngealized Ejective
|
| 567 |
+
combines:
|
| 568 |
+
- Labialized
|
| 569 |
+
- Pharyngealized
|
| 570 |
+
- Ejective
|
| 571 |
+
|
| 572 |
+
- name: Labialized Pharyngealized Aspirated
|
| 573 |
+
combines:
|
| 574 |
+
- Labialized
|
| 575 |
+
- Pharyngealized
|
| 576 |
+
- Aspirated
|
| 577 |
+
|
| 578 |
+
- name: Labialized Long
|
| 579 |
+
combines:
|
| 580 |
+
- Labialized
|
| 581 |
+
- Long
|
| 582 |
+
|
| 583 |
+
## Palatalized
|
| 584 |
+
|
| 585 |
+
- name: Palatalized Ejective
|
| 586 |
+
combines:
|
| 587 |
+
- Palatalized
|
| 588 |
+
- Ejective
|
| 589 |
+
|
| 590 |
+
- name: Palatalized Labialized
|
| 591 |
+
combines:
|
| 592 |
+
- Palatalized
|
| 593 |
+
- Labialized
|
| 594 |
+
|
| 595 |
+
- name: Palatalized Long
|
| 596 |
+
combines:
|
| 597 |
+
- Palatalized
|
| 598 |
+
- Long
|
| 599 |
+
|
| 600 |
+
- name: Palatalized Aspirated
|
| 601 |
+
combines:
|
| 602 |
+
- Palatalized
|
| 603 |
+
- Aspirated
|
| 604 |
+
|
| 605 |
+
- name: Palatalized Labialized Aspirated
|
| 606 |
+
combines:
|
| 607 |
+
- Palatalized
|
| 608 |
+
- Labialized
|
| 609 |
+
- Aspirated
|
| 610 |
+
|
| 611 |
+
# Pharyngealized
|
| 612 |
+
|
| 613 |
+
- name: Pharyngealized Ejective
|
| 614 |
+
combines:
|
| 615 |
+
- Pharyngealized
|
| 616 |
+
- Ejective
|
| 617 |
+
|
| 618 |
+
- name: Pharyngealized Aspirated
|
| 619 |
+
combines:
|
| 620 |
+
- Pharyngealized
|
| 621 |
+
- Aspirated
|
| 622 |
+
|
| 623 |
+
- name: Pharyngealized Long
|
| 624 |
+
combines:
|
| 625 |
+
- Pharyngealized
|
| 626 |
+
- Long
|
| 627 |
+
|
| 628 |
+
# Tongue root state
|
| 629 |
+
|
| 630 |
+
## ATR
|
| 631 |
+
|
| 632 |
+
- name: ATR Velarized
|
| 633 |
+
combines:
|
| 634 |
+
- ATR
|
| 635 |
+
- Velarized
|
| 636 |
+
|
| 637 |
+
- name: ATR Long
|
| 638 |
+
combines:
|
| 639 |
+
- ATR
|
| 640 |
+
- Long
|
| 641 |
+
|
| 642 |
+
## RTR
|
| 643 |
+
|
| 644 |
+
- name: RTR Velarized
|
| 645 |
+
combines:
|
| 646 |
+
- RTR
|
| 647 |
+
- Velarized
|
| 648 |
+
|
| 649 |
+
- name: RTR Long
|
| 650 |
+
combines:
|
| 651 |
+
- RTR
|
| 652 |
+
- Long
|
| 653 |
+
|
| 654 |
+
# Nasality
|
| 655 |
+
|
| 656 |
+
- name: Nasalized Creaky
|
| 657 |
+
combines:
|
| 658 |
+
- Nasalized
|
| 659 |
+
- Creaky Voiced
|
| 660 |
+
|
| 661 |
+
- name: Nasalized Pharyngealized
|
| 662 |
+
combines:
|
| 663 |
+
- Nasalized
|
| 664 |
+
- Pharyngealized
|
| 665 |
+
|
| 666 |
+
- name: Nasalized Long
|
| 667 |
+
combines:
|
| 668 |
+
- Nasalized
|
| 669 |
+
- Long
|
panphon/data/diacritic_definitions_schema.yml
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
type: map
|
| 2 |
+
mapping:
|
| 3 |
+
"diacritics":
|
| 4 |
+
type: seq
|
| 5 |
+
sequence:
|
| 6 |
+
- type: map
|
| 7 |
+
mapping:
|
| 8 |
+
"marker":
|
| 9 |
+
type: str
|
| 10 |
+
required: yes
|
| 11 |
+
"name":
|
| 12 |
+
type: str
|
| 13 |
+
required: yes
|
| 14 |
+
"position":
|
| 15 |
+
type: str
|
| 16 |
+
required: no
|
| 17 |
+
"conditions":
|
| 18 |
+
type: seq
|
| 19 |
+
sequence:
|
| 20 |
+
- type: map
|
| 21 |
+
mapping:
|
| 22 |
+
"syl":
|
| 23 |
+
type: str
|
| 24 |
+
enum: ["0", "-", "+"]
|
| 25 |
+
required: no
|
| 26 |
+
"son":
|
| 27 |
+
type: str
|
| 28 |
+
enum: ["0", "-", "+"]
|
| 29 |
+
required: no
|
| 30 |
+
"cons":
|
| 31 |
+
type: str
|
| 32 |
+
enum: ["0", "-", "+"]
|
| 33 |
+
required: no
|
| 34 |
+
"cont":
|
| 35 |
+
type: str
|
| 36 |
+
enum: ["0", "-", "+"]
|
| 37 |
+
required: no
|
| 38 |
+
"delrel":
|
| 39 |
+
type: str
|
| 40 |
+
enum: ["0", "-", "+"]
|
| 41 |
+
required: no
|
| 42 |
+
"lat":
|
| 43 |
+
type: str
|
| 44 |
+
enum: ["0", "-", "+"]
|
| 45 |
+
required: no
|
| 46 |
+
"nas":
|
| 47 |
+
type: str
|
| 48 |
+
enum: ["0", "-", "+"]
|
| 49 |
+
required: no
|
| 50 |
+
"strid":
|
| 51 |
+
type: str
|
| 52 |
+
enum: ["0", "-", "+"]
|
| 53 |
+
required: no
|
| 54 |
+
"voi":
|
| 55 |
+
type: str
|
| 56 |
+
enum: ["0", "-", "+"]
|
| 57 |
+
required: no
|
| 58 |
+
"sg":
|
| 59 |
+
type: str
|
| 60 |
+
enum: ["0", "-", "+"]
|
| 61 |
+
required: no
|
| 62 |
+
"cg":
|
| 63 |
+
type: str
|
| 64 |
+
enum: ["0", "-", "+"]
|
| 65 |
+
required: no
|
| 66 |
+
"ant":
|
| 67 |
+
type: str
|
| 68 |
+
enum: ["0", "-", "+"]
|
| 69 |
+
required: no
|
| 70 |
+
"cor":
|
| 71 |
+
type: str
|
| 72 |
+
enum: ["0", "-", "+"]
|
| 73 |
+
required: no
|
| 74 |
+
"distr":
|
| 75 |
+
type: str
|
| 76 |
+
enum: ["0", "-", "+"]
|
| 77 |
+
required: no
|
| 78 |
+
"lab":
|
| 79 |
+
type: str
|
| 80 |
+
enum: ["0", "-", "+"]
|
| 81 |
+
required: no
|
| 82 |
+
"hi":
|
| 83 |
+
type: str
|
| 84 |
+
enum: ["0", "-", "+"]
|
| 85 |
+
required: no
|
| 86 |
+
"lo":
|
| 87 |
+
type: str
|
| 88 |
+
enum: ["0", "-", "+"]
|
| 89 |
+
required: no
|
| 90 |
+
"back":
|
| 91 |
+
type: str
|
| 92 |
+
enum: ["0", "-", "+"]
|
| 93 |
+
required: no
|
| 94 |
+
"round":
|
| 95 |
+
type: str
|
| 96 |
+
enum: ["0", "-", "+"]
|
| 97 |
+
required: no
|
| 98 |
+
"tense":
|
| 99 |
+
type: str
|
| 100 |
+
enum: ["0", "-", "+"]
|
| 101 |
+
required: no
|
| 102 |
+
"long":
|
| 103 |
+
type: str
|
| 104 |
+
enum: ["0", "-", "+"]
|
| 105 |
+
required: no
|
| 106 |
+
"exclude":
|
| 107 |
+
type: seq
|
| 108 |
+
sequence:
|
| 109 |
+
- type: str
|
| 110 |
+
"content":
|
| 111 |
+
type: map
|
| 112 |
+
mapping:
|
| 113 |
+
"syl":
|
| 114 |
+
type: str
|
| 115 |
+
enum: ["0", "-", "+"]
|
| 116 |
+
required: no
|
| 117 |
+
"son":
|
| 118 |
+
type: str
|
| 119 |
+
enum: ["0", "-", "+"]
|
| 120 |
+
required: no
|
| 121 |
+
"cons":
|
| 122 |
+
type: str
|
| 123 |
+
enum: ["0", "-", "+"]
|
| 124 |
+
required: no
|
| 125 |
+
"cont":
|
| 126 |
+
type: str
|
| 127 |
+
enum: ["0", "-", "+"]
|
| 128 |
+
required: no
|
| 129 |
+
"delrel":
|
| 130 |
+
type: str
|
| 131 |
+
enum: ["0", "-", "+"]
|
| 132 |
+
required: no
|
| 133 |
+
"lat":
|
| 134 |
+
type: str
|
| 135 |
+
enum: ["0", "-", "+"]
|
| 136 |
+
required: no
|
| 137 |
+
"nas":
|
| 138 |
+
type: str
|
| 139 |
+
enum: ["0", "-", "+"]
|
| 140 |
+
required: no
|
| 141 |
+
"strid":
|
| 142 |
+
type: str
|
| 143 |
+
enum: ["0", "-", "+"]
|
| 144 |
+
required: no
|
| 145 |
+
"voi":
|
| 146 |
+
type: str
|
| 147 |
+
enum: ["0", "-", "+"]
|
| 148 |
+
required: no
|
| 149 |
+
"sg":
|
| 150 |
+
type: str
|
| 151 |
+
enum: ["0", "-", "+"]
|
| 152 |
+
required: no
|
| 153 |
+
"cg":
|
| 154 |
+
type: str
|
| 155 |
+
enum: ["0", "-", "+"]
|
| 156 |
+
required: no
|
| 157 |
+
"ant":
|
| 158 |
+
type: str
|
| 159 |
+
enum: ["0", "-", "+"]
|
| 160 |
+
required: no
|
| 161 |
+
"cor":
|
| 162 |
+
type: str
|
| 163 |
+
enum: ["0", "-", "+"]
|
| 164 |
+
required: no
|
| 165 |
+
"distr":
|
| 166 |
+
type: str
|
| 167 |
+
enum: ["0", "-", "+"]
|
| 168 |
+
required: no
|
| 169 |
+
"lab":
|
| 170 |
+
type: str
|
| 171 |
+
enum: ["0", "-", "+"]
|
| 172 |
+
required: no
|
| 173 |
+
"hi":
|
| 174 |
+
type: str
|
| 175 |
+
enum: ["0", "-", "+"]
|
| 176 |
+
required: no
|
| 177 |
+
"lo":
|
| 178 |
+
type: str
|
| 179 |
+
enum: ["0", "-", "+"]
|
| 180 |
+
required: no
|
| 181 |
+
"back":
|
| 182 |
+
type: str
|
| 183 |
+
enum: ["0", "-", "+"]
|
| 184 |
+
required: no
|
| 185 |
+
"round":
|
| 186 |
+
type: str
|
| 187 |
+
enum: ["0", "-", "+"]
|
| 188 |
+
required: no
|
| 189 |
+
"tense":
|
| 190 |
+
type: str
|
| 191 |
+
enum: ["0", "-", "+"]
|
| 192 |
+
required: no
|
| 193 |
+
"long":
|
| 194 |
+
type: str
|
| 195 |
+
enum: ["0", "-", "+"]
|
| 196 |
+
required: no
|
| 197 |
+
|
| 198 |
+
"combinations":
|
| 199 |
+
type: seq
|
| 200 |
+
sequence:
|
| 201 |
+
- type: map
|
| 202 |
+
mapping:
|
| 203 |
+
"name":
|
| 204 |
+
type: str
|
| 205 |
+
required: yes
|
| 206 |
+
"combines":
|
| 207 |
+
type: seq
|
| 208 |
+
sequence:
|
| 209 |
+
- type: str
|
panphon/data/dolgopolsky_prime.yml
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-
|
| 2 |
+
name: labial obstruents
|
| 3 |
+
def: "[-son +ant -cor + lab]"
|
| 4 |
+
label: P
|
| 5 |
+
-
|
| 6 |
+
name: coronal fricatives
|
| 7 |
+
def: "[-son +cor +cont +strid -delrel]"
|
| 8 |
+
label: S
|
| 9 |
+
-
|
| 10 |
+
name: velar/postvelar obstruents
|
| 11 |
+
def: "[-son -ant -cor +back]"
|
| 12 |
+
label: K
|
| 13 |
+
-
|
| 14 |
+
name: coronal affricates
|
| 15 |
+
def: "[+cor +delrel]"
|
| 16 |
+
label: K
|
| 17 |
+
-
|
| 18 |
+
name: other coronal obstruents
|
| 19 |
+
def: "[-son +cor]"
|
| 20 |
+
label: T
|
| 21 |
+
-
|
| 22 |
+
name: labial nasal
|
| 23 |
+
def: "[-syl +son +nas +ant -cor +lab]"
|
| 24 |
+
label: M
|
| 25 |
+
-
|
| 26 |
+
name: nasal
|
| 27 |
+
def: "[-syl +son -cont +nas]"
|
| 28 |
+
label: N
|
| 29 |
+
-
|
| 30 |
+
name: lateral
|
| 31 |
+
def: "[+lat]"
|
| 32 |
+
label: R
|
| 33 |
+
-
|
| 34 |
+
name: approximates and trills
|
| 35 |
+
def: "[-syl +son -nas]"
|
| 36 |
+
label: R
|
| 37 |
+
-
|
| 38 |
+
name: palatal glide
|
| 39 |
+
def: "[-syl +son +cont +hi -lo -back]"
|
| 40 |
+
label: J
|
| 41 |
+
-
|
| 42 |
+
name: labiovelar glide
|
| 43 |
+
def: "[-syl +son]"
|
| 44 |
+
label: W
|
| 45 |
+
-
|
| 46 |
+
name: laryngeals
|
| 47 |
+
def: "[-syl +son +cons +cont -cor -hi -back -lo]"
|
| 48 |
+
label: H
|
| 49 |
+
-
|
| 50 |
+
name: consonants
|
| 51 |
+
def: "[-syl]"
|
| 52 |
+
label: C
|
| 53 |
+
-
|
| 54 |
+
name: vowels
|
| 55 |
+
def: "[+syl]"
|
| 56 |
+
label: V
|
panphon/data/feature_weights.csv
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
syl,son,cons,cont,delrel,lat,nas,strid,voi,sg,cg,ant,cor,distr,lab,hi,lo,back,round,tense,long,velaric
|
| 2 |
+
1,1,1,0.5,0.25,0.25,0.25,0.125,0.125,0.125,0.125,0.25,0.25,0.125,0.25,0.25,0.25,0.25,0.25,0.25,0.125,0.25
|
panphon/data/ipa-xsampa.csv
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
IPA,X-SAMPA,Name
|
| 2 |
+
p,p,vl bilabial plosive
|
| 3 |
+
b,b,vd bilabial plosive
|
| 4 |
+
t,t,vl alveolar plosive
|
| 5 |
+
d,d,vd alveolar plosive
|
| 6 |
+
ʈ,t`,vl retroflex plosive
|
| 7 |
+
ɖ,d`,vd retroflex plosive
|
| 8 |
+
c,c,vl palatal plosive
|
| 9 |
+
ɟ,J\,vd palatal plosive
|
| 10 |
+
k,k,ld velar plosive
|
| 11 |
+
ɡ,g,vd velar plosive
|
| 12 |
+
q,q,vl uvular plosive
|
| 13 |
+
ɢ,G\,vd uvular plosive
|
| 14 |
+
ʔ,?,glottal plosive
|
| 15 |
+
m,m,bilabial nasal
|
| 16 |
+
ɱ,F,vl labiodental nasal
|
| 17 |
+
n,n,alveolar nasal
|
| 18 |
+
ɳ,n`,vl retroflex nasal
|
| 19 |
+
ɲ,J,vl palatal nasal
|
| 20 |
+
ŋ,N,vl velar nasal
|
| 21 |
+
ɴ,N\,vl uvular nasal
|
| 22 |
+
ʙ,B\,vd bilabial trill
|
| 23 |
+
r,r,vd alveolar trill
|
| 24 |
+
ʀ,R\,vl uvular trill
|
| 25 |
+
ɾ,4,vl alveolar tap
|
| 26 |
+
ɽ,r`,vl retroflex flap
|
| 27 |
+
ɸ,p\,vl bilabial fricative
|
| 28 |
+
β,B,vd bilabial fricative
|
| 29 |
+
f,f,vl labiodental fricative
|
| 30 |
+
v,v,vd labiodental fricative
|
| 31 |
+
θ,T,vl dental fricative
|
| 32 |
+
ð,D,vd dental fricative
|
| 33 |
+
s,s,vl alveolar fricative
|
| 34 |
+
z,z,vd alveolar fricative
|
| 35 |
+
ʃ,S,vl postalveolar fricative
|
| 36 |
+
ʒ,Z,vd postalveolar fricative
|
| 37 |
+
ʂ,s`,vl retroflex fricative
|
| 38 |
+
ʐ,z`,vd retroflex fricative
|
| 39 |
+
ç,C,vl palatal fricative
|
| 40 |
+
ʝ,j\,vd palatal fricative
|
| 41 |
+
x,x,vl velar fricative
|
| 42 |
+
ɣ,G,vd velar fricative
|
| 43 |
+
χ,X,vl uvular fricative
|
| 44 |
+
ʁ,R,vd uvular fricative
|
| 45 |
+
ħ,X\,vl pharyngeal fricative
|
| 46 |
+
ʕ,?\,vd pharyngeal fricative
|
| 47 |
+
h,h,vl glottal fricative
|
| 48 |
+
ʔ,?,glottal plosive
|
| 49 |
+
ɬ,K,vl alveolar lateral fricative
|
| 50 |
+
ɮ,K\,vd alveolar lateral fricative
|
| 51 |
+
ʋ,P,vd labiodental approximant
|
| 52 |
+
ɹ,r\,vd (post)alveolar approximant
|
| 53 |
+
ɻ,r\`,vd retroflex approximant
|
| 54 |
+
j,j,vd palatal approximant
|
| 55 |
+
ɰ,M\,vd velar approximant
|
| 56 |
+
l,l,vd alveolar lateral approximant
|
| 57 |
+
ɭ,l`,vd retroflex lateral approximant
|
| 58 |
+
ʎ,L,vd palatal lateral approximant
|
| 59 |
+
ʟ,L\,vd velar lateral approximant
|
| 60 |
+
pʼ,p_>,ejective
|
| 61 |
+
tʼ,t_>,ejective
|
| 62 |
+
ʈʼ,t`_>,ejective
|
| 63 |
+
cʼ,c_>,ejective
|
| 64 |
+
kʼ,k_>,ejective
|
| 65 |
+
qʼ,q_>,ejective
|
| 66 |
+
ɓ,b_<,vl bilabial implosive
|
| 67 |
+
ɗ,d_<,vl alveolar implosive
|
| 68 |
+
ƙ,k_<,vl velar implosive
|
| 69 |
+
ɠ,g_<,vl velar implosive
|
| 70 |
+
i,i,close front unrounded
|
| 71 |
+
y,y,close front rounded
|
| 72 |
+
ɨ,1,close central unrounded
|
| 73 |
+
ʉ,},close central rounded
|
| 74 |
+
ɯ,M,close back unrounded
|
| 75 |
+
u,u,close back rounded
|
| 76 |
+
ɪ,I,lax close front unrounded
|
| 77 |
+
ʏ,Y,lax close front rounded
|
| 78 |
+
ʊ,U,lax close back rounded
|
| 79 |
+
e,e,close-mid front unrounded
|
| 80 |
+
ø,2,front close-mid rounded
|
| 81 |
+
ɤ,7,close-mid back unrounded
|
| 82 |
+
o,o,close-mid back rounded
|
| 83 |
+
ə,@,schwa
|
| 84 |
+
ɘ,@\,close-mid central unrounded vowel
|
| 85 |
+
ɵ,8,rounded schwa
|
| 86 |
+
ɛ,E,open-mid front unrounded
|
| 87 |
+
œ,9,front open-mid rounded
|
| 88 |
+
ʌ,V,open-mid back unrounded
|
| 89 |
+
ɔ,O,open-mid back rounded
|
| 90 |
+
æ,{,mid-open front unrounded vowel
|
| 91 |
+
ɐ,6,open-mid schwa
|
| 92 |
+
a,a,open front unrounded
|
| 93 |
+
ă,a_X,extra short open front unrounded
|
| 94 |
+
ɶ,&,front open rounded
|
| 95 |
+
ɑ,A,open back unrounded
|
| 96 |
+
ɒ,Q,open back rounded
|
| 97 |
+
̥,_0,voiceless
|
| 98 |
+
̬,_v,voiced
|
| 99 |
+
ʰ,_h,aspirated
|
| 100 |
+
̤,_t,breathy voiced
|
| 101 |
+
̰,_k,creaky voiced
|
| 102 |
+
̼,_N,linguolabial
|
| 103 |
+
̪,_d,dental
|
| 104 |
+
̺,_a,apical
|
| 105 |
+
̻,_m,laminal
|
| 106 |
+
̹,_O,more rounded
|
| 107 |
+
̜,_c,less rounded
|
| 108 |
+
̟,_+,advanced
|
| 109 |
+
̠,_-,retracted
|
| 110 |
+
̈,"_""",centralized
|
| 111 |
+
̽,_x,mid-centralized
|
| 112 |
+
̩,=,syllabic
|
| 113 |
+
̯,_^,non-syllabic
|
| 114 |
+
ʷ,_w,labialized
|
| 115 |
+
ʲ,',palatalized
|
| 116 |
+
ˠ,_G,velarized
|
| 117 |
+
ˤ,_?\,pharyngealized
|
| 118 |
+
̴,_e,velarized or pharyngealized
|
| 119 |
+
̝,_r,raised
|
| 120 |
+
̞,_o,lowered
|
| 121 |
+
̃,~,nasalized
|
| 122 |
+
ⁿ,_n,nasal release
|
| 123 |
+
ˡ,_l,lateral release
|
| 124 |
+
̚,_},not audibly released
|
| 125 |
+
̘,_A,advanced tongue root
|
| 126 |
+
̙,_q,retracted tongue root
|
| 127 |
+
̋,_T,extra high tone
|
| 128 |
+
́,_H,high tone
|
| 129 |
+
̄,_M,mid tone
|
| 130 |
+
̀,_L,low tone
|
| 131 |
+
̏,_B,extra low tone
|
| 132 |
+
ˈ,"""",(primary) stress mark
|
| 133 |
+
ˌ,%,secondary stress
|
| 134 |
+
ː,:,length mark
|
| 135 |
+
ˑ,:\,half-length
|
| 136 |
+
̆,_X,extra-short
|
| 137 |
+
.,.,syllable break
|
| 138 |
+
ʍ,W,vl labial-velar fricative
|
| 139 |
+
w,w,vd labio-velar approximant
|
| 140 |
+
ɥ,H,labial-palatal approximant
|
| 141 |
+
ʜ,H\,vl epiglottal fricative
|
| 142 |
+
ʢ,<\,vl epiglottal fricative
|
| 143 |
+
ʡ,>\,vl epiglottal plosive
|
| 144 |
+
ɕ,s\,vl alveolopalatal fricative
|
| 145 |
+
ʑ,z\,vl alveolopalatal fricative
|
| 146 |
+
ʘ,O\,bilabial click
|
| 147 |
+
ǀ,|\,dental click
|
| 148 |
+
ǃ,!\,click
|
| 149 |
+
ǂ,'=\,alveolar click
|
| 150 |
+
ǁ,|\|\,alveolar lateral click
|
| 151 |
+
ɺ,l\,vl alveolar lateral flap
|
| 152 |
+
ɜ,3,open-mid central
|
| 153 |
+
ʛ,G\_<,vl uvular implosive
|
| 154 |
+
ɚ,@`,rhotacized schwa
|
| 155 |
+
ɞ,3\,open-mid central rounded
|
| 156 |
+
ɦ,h\,vd glottal fricative
|
| 157 |
+
ɫ,5,velarized vl alveolar lateral
|
| 158 |
+
ʄ,J\_<,vl palatal implosive
|
| 159 |
+
ʼ,_>,ejective
|
| 160 |
+
ɝ,3`,rhotacized open-mid central
|
| 161 |
+
t͡ʃ,tS,vl postalveolar affricate
|
| 162 |
+
d͡ʒ,dZ,vd postalveolar affricate
|
| 163 |
+
t͡ɕ,ts\,vl alveolo-palatal affricate
|
| 164 |
+
d͡ʑ,dz\,vd alveolo-palatal affricate
|
| 165 |
+
t͡ɬ,tK,vl alveolar lateral affricate
|
| 166 |
+
k͡p,kp,vl labial-velar plosive
|
| 167 |
+
g͡b,gb,vd labial-velar plosive
|
| 168 |
+
ŋ͡m,Nm,labial-velar nasal stop
|
| 169 |
+
ʈ͡ʂ,ts`,vl retroflex affricate
|
| 170 |
+
ɖ͡ʐ,tz`,vd retroflex affricate
|
| 171 |
+
˩,_B,extra low tone
|
| 172 |
+
˨,_L,low tone
|
| 173 |
+
˧,_M,mid tone
|
| 174 |
+
˦,_H,high tone
|
| 175 |
+
˥,_T,extra high tone
|
panphon/data/ipa_all.csv
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
panphon/data/ipa_bases.csv
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
ipa,syl,son,cons,cont,delrel,lat,nas,strid,voi,sg,cg,ant,cor,distr,lab,hi,lo,back,round,velaric,tense,long,hitone,hireg
|
| 2 |
+
ʘ,-,-,+,-,-,-,-,0,-,-,-,0,-,0,+,+,-,0,-,+,0,-,0,0
|
| 3 |
+
ǀ,-,-,+,-,-,-,-,0,-,-,-,+,+,+,-,+,-,0,-,+,0,-,0,0
|
| 4 |
+
ǃ,-,-,+,-,-,-,-,0,-,-,-,+,+,-,-,+,-,0,-,+,0,-,0,0
|
| 5 |
+
ǂ,-,-,+,-,-,-,-,0,-,-,-,-,+,+,-,+,-,0,-,+,0,-,0,0
|
| 6 |
+
ǁ,-,-,+,-,-,+,-,0,-,-,-,+,+,-,-,+,-,0,-,+,0,-,0,0
|
| 7 |
+
k͡p,-,-,+,-,-,-,-,0,-,-,-,0,-,0,+,+,-,0,-,-,0,-,0,0
|
| 8 |
+
ɡ͡b,-,-,+,-,-,-,-,0,+,-,-,0,-,0,+,+,-,0,-,-,0,-,0,0
|
| 9 |
+
c,-,-,+,-,-,-,-,0,-,-,-,-,-,0,-,+,-,-,-,-,0,-,0,0
|
| 10 |
+
ɡ,-,-,+,-,-,-,-,0,+,-,-,-,-,0,-,+,-,+,-,-,0,-,0,0
|
| 11 |
+
k,-,-,+,-,-,-,-,0,-,-,-,-,-,0,-,+,-,+,-,-,0,-,0,0
|
| 12 |
+
q,-,-,+,-,-,-,-,0,-,-,-,-,-,0,-,-,-,+,-,-,0,-,0,0
|
| 13 |
+
ɖ,-,-,+,-,-,-,-,0,+,-,-,-,+,-,-,-,-,-,-,-,0,-,0,0
|
| 14 |
+
ɟ,-,-,+,-,-,-,-,0,+,-,-,-,-,0,-,+,-,-,-,-,0,-,0,0
|
| 15 |
+
ɠ,-,-,+,-,-,-,-,0,+,-,+,-,-,0,-,+,-,+,-,-,0,-,0,0
|
| 16 |
+
ɢ,-,-,+,-,-,-,-,0,+,-,-,-,-,0,-,-,-,+,-,-,0,-,0,0
|
| 17 |
+
ʄ,-,-,+,-,-,-,-,0,+,-,+,-,-,0,-,+,-,-,-,-,0,-,0,0
|
| 18 |
+
ʈ,-,-,+,-,-,-,-,0,-,-,-,-,+,-,-,-,-,-,-,-,0,-,0,0
|
| 19 |
+
ʛ,-,-,+,-,-,-,-,0,+,-,+,-,-,0,-,-,-,+,-,-,0,-,0,0
|
| 20 |
+
b,-,-,+,-,-,-,-,0,+,-,-,+,-,0,+,-,-,-,-,-,0,-,0,0
|
| 21 |
+
b͡d,-,-,+,-,-,-,-,0,+,-,-,+,+,-,+,-,-,-,-,-,0,-,0,0
|
| 22 |
+
d,-,-,+,-,-,-,-,0,+,-,-,+,+,-,-,-,-,-,-,-,0,-,0,0
|
| 23 |
+
d̪,-,-,+,-,-,-,-,0,+,-,-,+,+,+,-,-,-,-,-,-,0,-,0,0
|
| 24 |
+
p,-,-,+,-,-,-,-,0,-,-,-,+,-,0,+,-,-,-,-,-,0,-,0,0
|
| 25 |
+
p͡t,-,-,+,-,-,-,-,0,-,-,-,+,+,-,+,-,-,-,-,-,0,-,0,0
|
| 26 |
+
t,-,-,+,-,-,-,-,0,-,-,-,+,+,-,-,-,-,-,-,-,0,-,0,0
|
| 27 |
+
t̪,-,-,+,-,-,-,-,0,-,-,-,+,+,+,-,-,-,-,-,-,0,-,0,0
|
| 28 |
+
ɓ,-,-,+,-,-,-,-,0,+,-,+,+,-,0,+,-,-,-,-,-,0,-,0,0
|
| 29 |
+
ɗ,-,-,+,-,-,-,-,0,+,-,+,+,+,-,-,-,-,-,-,-,0,-,0,0
|
| 30 |
+
b͡β,-,-,+,-,+,-,-,-,+,-,-,+,-,0,+,0,0,0,-,-,0,-,0,0
|
| 31 |
+
k͡x,-,-,+,-,+,-,-,0,-,-,-,-,-,0,-,+,-,0,-,-,0,-,0,0
|
| 32 |
+
p͡ɸ,-,-,+,-,+,-,-,-,-,-,-,+,-,0,+,0,0,0,-,-,0,-,0,0
|
| 33 |
+
q͡χ,-,-,+,-,+,-,-,0,-,-,-,-,-,0,-,-,-,+,-,-,0,-,0,0
|
| 34 |
+
ɡ͡ɣ,-,-,+,-,+,-,-,0,+,-,-,-,-,0,-,+,-,0,-,-,0,-,0,0
|
| 35 |
+
ɢ͡ʁ,-,-,+,-,+,-,-,0,+,-,-,-,-,0,-,-,-,+,-,-,0,-,0,0
|
| 36 |
+
c͡ç,-,-,+,-,+,-,-,0,-,-,-,-,+,+,-,+,-,-,-,-,0,-,0,0
|
| 37 |
+
d͡ʒ,-,-,+,-,+,-,-,+,+,-,-,-,+,+,-,-,-,-,-,-,0,-,0,0
|
| 38 |
+
t͡ʃ,-,-,+,-,+,-,-,+,-,-,-,-,+,+,-,-,-,-,-,-,0,-,0,0
|
| 39 |
+
ɖ͡ʐ,-,-,+,-,+,-,-,+,+,-,-,-,+,-,-,0,0,0,-,-,0,-,0,0
|
| 40 |
+
ɟ͡ʝ,-,-,+,-,+,-,-,0,+,-,-,-,+,+,-,+,-,-,-,-,0,-,0,0
|
| 41 |
+
ʈ͡ʂ,-,-,+,-,+,-,-,+,-,-,-,-,+,-,-,0,0,0,-,-,0,-,0,0
|
| 42 |
+
b͡v,-,-,+,-,+,-,-,+,+,-,-,+,-,0,+,-,-,-,-,-,0,-,0,0
|
| 43 |
+
d̪͡z̪,-,-,+,-,+,-,-,+,+,-,-,+,+,+,-,0,0,0,-,-,0,-,0,0
|
| 44 |
+
d̪͡ð,-,-,+,-,+,-,-,0,+,-,-,+,+,+,-,0,0,0,-,-,0,-,0,0
|
| 45 |
+
d̪͡ɮ̪,-,-,+,-,+,+,-,0,+,-,-,+,+,-,-,0,0,0,-,-,0,-,0,0
|
| 46 |
+
d͡z,-,-,+,-,+,-,-,+,+,-,-,+,+,-,-,-,-,-,-,-,0,-,0,0
|
| 47 |
+
d͡ɮ,-,-,+,-,+,+,-,0,+,-,-,-,+,-,-,0,0,0,-,-,0,-,0,0
|
| 48 |
+
d͡ʑ,-,-,+,-,+,-,-,0,+,-,-,-,+,+,-,+,-,-,-,-,0,-,0,0
|
| 49 |
+
p͡f,-,-,+,-,+,-,-,+,-,-,-,+,-,0,+,-,-,-,-,-,0,-,0,0
|
| 50 |
+
t̪͡s̪,-,-,+,-,+,-,-,+,-,-,-,+,+,+,-,0,0,0,-,-,0,-,0,0
|
| 51 |
+
t̪͡ɬ̪,-,-,+,-,+,+,-,0,-,-,-,+,+,+,-,0,0,0,-,-,0,-,0,0
|
| 52 |
+
t̪͡θ,-,-,+,-,+,-,-,0,-,-,-,+,+,+,-,0,0,0,-,-,0,-,0,0
|
| 53 |
+
t͡s,-,-,+,-,+,-,-,+,-,-,-,+,+,-,-,-,-,-,-,-,0,-,0,0
|
| 54 |
+
t͡ɕ,-,-,+,-,+,-,-,0,-,-,-,-,+,+,-,+,-,-,-,-,0,-,0,0
|
| 55 |
+
t͡ɬ,-,-,+,-,+,+,-,0,-,-,-,+,+,-,-,0,0,0,-,-,0,-,0,0
|
| 56 |
+
x,-,-,+,+,-,-,-,0,-,-,-,-,-,0,-,+,-,+,-,-,0,-,0,0
|
| 57 |
+
ç,-,-,+,+,-,-,-,0,-,-,-,-,-,0,-,+,-,-,-,-,0,-,0,0
|
| 58 |
+
ħ,-,-,+,+,-,-,-,0,-,-,-,-,-,0,-,-,+,+,-,-,0,-,0,0
|
| 59 |
+
ɣ,-,-,+,+,-,-,-,0,+,-,-,-,-,0,-,+,-,+,-,-,0,-,0,0
|
| 60 |
+
ʁ,-,-,+,+,-,-,-,0,+,-,-,-,-,0,-,-,-,+,-,-,0,-,0,0
|
| 61 |
+
ʂ,-,-,+,+,-,-,-,+,-,-,-,-,+,-,-,-,-,-,-,-,0,-,0,0
|
| 62 |
+
ʃ,-,-,+,+,-,-,-,+,-,-,-,-,+,+,-,-,-,-,-,-,0,-,0,0
|
| 63 |
+
ʐ,-,-,+,+,-,-,-,+,+,-,-,-,+,-,-,-,-,-,-,-,0,-,0,0
|
| 64 |
+
ʒ,-,-,+,+,-,-,-,+,+,-,-,-,+,+,-,-,-,-,-,-,0,-,0,0
|
| 65 |
+
ʕ,-,-,+,+,-,-,-,0,+,-,-,-,-,0,-,-,+,+,-,-,0,-,0,0
|
| 66 |
+
ʝ,-,-,+,+,-,-,-,0,+,-,-,-,-,0,-,+,-,-,-,-,0,-,0,0
|
| 67 |
+
χ,-,-,+,+,-,-,-,0,-,-,-,-,-,0,-,-,-,+,-,-,0,-,0,0
|
| 68 |
+
f,-,-,+,+,-,-,-,+,-,-,-,+,-,0,+,-,-,-,-,-,0,-,0,0
|
| 69 |
+
s,-,-,+,+,-,-,-,+,-,-,-,+,+,-,-,-,-,-,-,-,0,-,0,0
|
| 70 |
+
s̪,-,-,+,+,-,-,-,+,-,-,-,+,+,+,-,-,-,-,-,-,0,-,0,0
|
| 71 |
+
v,-,-,+,+,-,-,-,+,+,-,-,+,-,0,+,-,-,-,-,-,0,-,0,0
|
| 72 |
+
z,-,-,+,+,-,-,-,+,+,-,-,+,+,-,-,-,-,-,-,-,0,-,0,0
|
| 73 |
+
z̪,-,-,+,+,-,-,-,+,+,-,-,+,+,+,-,-,-,-,-,-,0,-,0,0
|
| 74 |
+
ð,-,-,+,+,-,-,-,0,+,-,-,+,+,+,-,-,-,-,-,-,0,-,0,0
|
| 75 |
+
ɸ,-,-,+,+,-,-,-,-,-,-,-,+,-,0,+,-,-,-,-,-,0,-,0,0
|
| 76 |
+
β,-,-,+,+,-,-,-,-,+,-,-,+,-,0,+,-,-,-,-,-,0,-,0,0
|
| 77 |
+
θ,-,-,+,+,-,-,-,0,-,-,-,+,+,+,-,-,-,-,-,-,0,-,0,0
|
| 78 |
+
ɧ,-,-,+,+,+,-,-,0,-,-,-,-,+,+,-,+,-,0,-,-,0,-,0,0
|
| 79 |
+
ɕ,-,-,+,+,+,-,-,0,-,-,-,-,+,+,-,+,-,-,-,-,0,-,0,0
|
| 80 |
+
ɬ,-,-,+,+,+,+,-,0,-,-,-,+,+,-,-,0,0,0,-,-,0,-,0,0
|
| 81 |
+
ɬ̪,-,-,+,+,+,+,-,0,-,-,-,+,+,+,-,0,0,0,-,-,0,-,0,0
|
| 82 |
+
ɮ,-,-,+,+,+,+,-,0,+,-,-,-,+,-,-,0,0,0,-,-,0,-,0,0
|
| 83 |
+
ʑ,-,-,+,+,+,-,-,0,+,-,-,-,+,+,-,+,-,-,-,-,0,-,0,0
|
| 84 |
+
ɱ,-,+,+,-,0,-,+,0,+,-,-,+,-,0,+,0,0,0,-,-,0,-,0,0
|
| 85 |
+
ʔ,-,+,-,-,-,-,-,0,-,-,+,-,-,0,-,-,-,-,-,-,0,-,0,0
|
| 86 |
+
ŋ,-,+,+,-,-,-,+,0,+,-,-,-,-,0,-,+,-,+,-,-,0,-,0,0
|
| 87 |
+
ɳ,-,+,+,-,-,-,+,0,+,-,-,-,+,0,-,-,-,-,-,-,0,-,0,0
|
| 88 |
+
ɴ,-,+,+,-,-,-,+,0,+,-,-,-,-,0,-,-,-,+,-,-,0,-,0,0
|
| 89 |
+
m,-,+,+,-,-,-,+,0,+,-,-,+,-,0,+,-,-,-,-,-,0,-,0,0
|
| 90 |
+
n,-,+,+,-,-,-,+,0,+,-,-,+,+,-,-,-,-,-,-,-,0,-,0,0
|
| 91 |
+
n̪,-,+,+,-,-,-,+,0,+,-,-,+,+,+,-,-,-,-,-,-,0,-,0,0
|
| 92 |
+
ɲ,-,+,+,-,-,-,+,0,+,-,-,-,-,0,-,+,-,-,-,-,0,-,0,0
|
| 93 |
+
ɥ,-,+,-,+,0,-,-,0,+,-,-,-,-,0,+,+,-,-,+,-,+,-,0,0
|
| 94 |
+
ɰ,-,+,-,+,0,-,-,0,+,-,-,-,-,0,-,+,-,0,-,-,+,-,0,0
|
| 95 |
+
ʋ,-,+,-,+,0,-,-,0,+,-,-,+,-,0,+,0,0,0,-,-,0,-,0,0
|
| 96 |
+
ʀ,-,+,+,+,0,-,-,0,+,-,-,-,-,0,-,-,-,+,-,-,0,-,0,0
|
| 97 |
+
ʙ,-,+,+,+,0,-,-,0,+,-,-,+,-,0,+,0,0,0,-,-,0,-,0,0
|
| 98 |
+
ʟ,-,+,+,+,0,+,-,0,+,-,-,-,-,0,-,+,-,0,-,-,0,-,0,0
|
| 99 |
+
ɭ,-,+,+,+,0,+,-,0,+,-,-,-,+,-,-,0,0,0,-,-,0,-,0,0
|
| 100 |
+
ɽ,-,+,+,+,0,-,-,0,+,-,-,-,+,-,-,0,0,0,-,-,0,-,0,0
|
| 101 |
+
ʎ,-,+,+,+,0,+,-,0,+,-,-,-,+,+,-,+,-,-,-,-,0,-,0,0
|
| 102 |
+
r,-,+,+,+,0,-,-,0,+,-,-,+,+,-,-,0,0,0,-,-,0,-,0,0
|
| 103 |
+
r̪,-,+,+,+,0,-,-,0,+,-,-,+,+,+,-,0,0,0,-,-,0,-,0,0
|
| 104 |
+
ɫ,-,+,+,+,0,+,-,0,+,-,-,+,+,-,-,-,-,+,-,-,0,-,0,0
|
| 105 |
+
ɺ,-,+,+,+,0,+,-,0,+,-,-,+,+,-,-,0,0,0,-,-,0,-,0,0
|
| 106 |
+
ɾ,-,+,+,+,0,-,-,0,+,-,-,+,+,-,-,0,0,0,-,-,0,-,0,0
|
| 107 |
+
ʍ,-,+,-,+,-,-,-,0,-,-,-,-,-,0,+,+,-,+,+,-,0,-,0,0
|
| 108 |
+
h,-,+,+,+,-,-,-,0,-,-,-,-,-,0,-,-,-,-,-,-,0,-,0,0
|
| 109 |
+
j,-,+,-,+,-,-,-,0,+,-,-,-,-,0,-,+,-,-,-,-,0,-,0,0
|
| 110 |
+
w,-,+,-,+,-,-,-,0,+,-,-,-,-,0,+,+,-,+,+,-,0,-,0,0
|
| 111 |
+
ɹ,-,+,-,+,-,-,-,0,+,-,-,+,+,-,-,+,-,+,+,-,0,-,0,0
|
| 112 |
+
ɻ,-,+,-,+,-,-,-,0,+,-,-,-,+,-,-,-,-,-,-,-,0,-,0,0
|
| 113 |
+
l,-,+,+,+,-,+,-,0,+,-,-,+,+,-,-,-,-,-,-,-,0,-,0,0
|
| 114 |
+
l̪,-,+,+,+,-,+,-,0,+,-,-,+,+,+,-,-,-,-,-,-,0,-,0,0
|
| 115 |
+
ɦ,-,+,+,+,-,-,-,0,+,-,-,-,-,0,-,-,-,-,-,-,0,-,0,0
|
| 116 |
+
ɑ,+,+,-,+,0,-,-,0,+,-,-,0,-,0,-,-,+,+,-,-,+,-,0,0
|
| 117 |
+
ɘ,+,+,-,+,0,-,-,0,+,-,-,0,-,0,-,-,-,-,-,-,+,-,0,0
|
| 118 |
+
ɞ,+,+,-,+,0,-,-,0,+,-,-,0,-,0,+,-,-,-,+,-,-,-,0,0
|
| 119 |
+
ɤ,+,+,-,+,0,-,-,0,+,-,-,0,-,0,-,-,-,+,-,-,+,-,0,0
|
| 120 |
+
ɵ,+,+,-,+,0,-,-,0,+,-,-,0,-,0,+,-,-,-,+,-,+,-,0,0
|
| 121 |
+
ʉ,+,+,-,+,0,-,-,0,+,-,-,0,-,0,+,+,-,-,+,-,+,-,0,0
|
| 122 |
+
a,+,+,-,+,-,-,-,0,+,-,-,0,-,0,-,-,+,-,-,-,+,-,0,0
|
| 123 |
+
e,+,+,-,+,-,-,-,0,+,-,-,0,-,0,-,-,-,-,-,-,+,-,0,0
|
| 124 |
+
i,+,+,-,+,-,-,-,0,+,-,-,0,-,0,-,+,-,-,-,-,+,-,0,0
|
| 125 |
+
o,+,+,-,+,-,-,-,0,+,-,-,0,-,0,-,-,-,+,+,-,+,-,0,0
|
| 126 |
+
u,+,+,-,+,-,-,-,0,+,-,-,0,-,0,+,+,-,+,+,-,+,-,0,0
|
| 127 |
+
y,+,+,-,+,-,-,-,0,+,-,-,0,-,0,+,+,-,-,+,-,+,-,0,0
|
| 128 |
+
æ,+,+,-,+,-,-,-,0,+,-,-,0,-,0,-,-,+,-,-,-,+,-,0,0
|
| 129 |
+
ø,+,+,-,+,-,-,-,0,+,-,-,0,-,0,-,-,-,-,+,-,+,-,0,0
|
| 130 |
+
œ,+,+,-,+,-,-,-,0,+,-,-,0,-,0,-,-,-,-,+,-,-,-,0,0
|
| 131 |
+
ɒ,+,+,-,+,-,-,-,0,+,-,-,0,-,0,-,-,+,+,+,-,+,-,0,0
|
| 132 |
+
ɔ,+,+,-,+,-,-,-,0,+,-,-,0,-,0,-,-,-,+,+,-,-,-,0,0
|
| 133 |
+
ə,+,+,-,+,-,-,-,0,+,-,-,0,-,0,-,-,-,+,-,-,-,-,0,0
|
| 134 |
+
ɘ,+,+,-,+,-,-,-,0,+,-,-,0,-,0,-,-,-,+,-,-,-,-,0,0
|
| 135 |
+
ɵ,+,+,-,+,-,-,-,0,+,-,-,0,-,0,-,-,-,+,+,-,-,-,0,0
|
| 136 |
+
ɞ,+,+,-,+,-,-,-,0,+,-,-,0,-,0,-,-,-,+,+,-,+,-,0,0
|
| 137 |
+
ɜ,+,+,-,+,-,-,-,0,+,-,-,0,-,0,-,-,-,+,-,-,+,-,0,0
|
| 138 |
+
ɛ,+,+,-,+,-,-,-,0,+,-,-,0,-,0,-,-,-,-,-,-,-,-,0,0
|
| 139 |
+
ɨ,+,+,-,+,-,-,-,0,+,-,-,0,-,0,-,+,-,+,-,-,+,-,0,0
|
| 140 |
+
ɪ,+,+,-,+,-,-,-,0,+,-,-,0,-,0,-,+,-,-,-,-,-,-,0,0
|
| 141 |
+
ɯ,+,+,-,+,-,-,-,0,+,-,-,0,-,0,-,+,-,+,-,-,-,-,0,0
|
| 142 |
+
ɶ,+,+,-,+,-,-,-,0,+,-,-,0,-,0,-,-,+,-,+,-,+,-,0,0
|
| 143 |
+
ʊ,+,+,-,+,-,-,-,0,+,-,-,0,-,0,-,+,-,+,+,-,-,-,0,0
|
| 144 |
+
ɐ,+,+,-,+,-,-,-,0,+,-,-,0,-,0,-,-,-,+,-,-,+,-,0,0
|
| 145 |
+
ʌ,+,+,-,+,-,-,-,0,+,-,-,0,-,0,-,-,-,+,-,-,+,-,0,0
|
| 146 |
+
ʏ,+,+,-,+,-,-,-,0,+,-,-,0,-,0,-,+,-,-,+,-,-,-,0,0
|
| 147 |
+
˩,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-,-
|
| 148 |
+
˨,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,+,-
|
| 149 |
+
˧,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
| 150 |
+
˦,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-,+
|
| 151 |
+
˥,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,+,+
|
panphon/data/sort_order.yml
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
- name: syl
|
| 2 |
+
reverse: True
|
| 3 |
+
- name: son
|
| 4 |
+
reverse: True
|
| 5 |
+
- name: cont
|
| 6 |
+
reverse: True
|
| 7 |
+
- name: cons
|
| 8 |
+
reverse: True
|
| 9 |
+
- name: delrel
|
| 10 |
+
reverse: True
|
| 11 |
+
- name: nas
|
| 12 |
+
reverse: True
|
| 13 |
+
- name: lat
|
| 14 |
+
reverse: True
|
| 15 |
+
- name: sg
|
| 16 |
+
reverse: True
|
| 17 |
+
- name: cg
|
| 18 |
+
reverse: True
|
| 19 |
+
- name: hi
|
| 20 |
+
reverse: True
|
| 21 |
+
- name: lo
|
| 22 |
+
reverse: True
|
| 23 |
+
- name: lab
|
| 24 |
+
reverse: True
|
| 25 |
+
- name: ant
|
| 26 |
+
reverse: True
|
| 27 |
+
- name: cor
|
| 28 |
+
reverse: True
|
| 29 |
+
- name: distr
|
| 30 |
+
reverse: True
|
| 31 |
+
- name: back
|
| 32 |
+
reverse: True
|
| 33 |
+
- name: round
|
| 34 |
+
reverse: True
|
| 35 |
+
- name: tense
|
| 36 |
+
reverse: True
|
| 37 |
+
- name: long
|
| 38 |
+
reverse: True
|
| 39 |
+
- name: voi
|
| 40 |
+
reverse: True
|
panphon/data/sort_order_schema.yml
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
type: seq
|
| 2 |
+
sequence:
|
| 3 |
+
- type: map
|
| 4 |
+
mapping:
|
| 5 |
+
"name":
|
| 6 |
+
type: str
|
| 7 |
+
required: yes
|
| 8 |
+
"reverse":
|
| 9 |
+
type: bool
|
| 10 |
+
required: yes
|
panphon/distance.py
ADDED
|
@@ -0,0 +1,837 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import (absolute_import, division, print_function,
|
| 2 |
+
unicode_literals)
|
| 3 |
+
|
| 4 |
+
import os.path
|
| 5 |
+
from functools import partial
|
| 6 |
+
|
| 7 |
+
import editdistance
|
| 8 |
+
import numpy as np
|
| 9 |
+
import regex as re
|
| 10 |
+
import pkg_resources
|
| 11 |
+
import yaml
|
| 12 |
+
|
| 13 |
+
from . import _panphon, permissive, featuretable, xsampa
|
| 14 |
+
|
| 15 |
+
def zerodiviszero(f):
|
| 16 |
+
def wrapper(*args, **kwargs):
|
| 17 |
+
try:
|
| 18 |
+
return f(*args, **kwargs)
|
| 19 |
+
except ZeroDivisionError:
|
| 20 |
+
return 0
|
| 21 |
+
return wrapper
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def xsampaopt(f):
|
| 25 |
+
def wrapper(*args, **kwargs):
|
| 26 |
+
if 'xsampa' in kwargs and kwargs['xsampa']:
|
| 27 |
+
self, source, target = args
|
| 28 |
+
source = self.xs.convert(source)
|
| 29 |
+
target = self.xs.convert(target)
|
| 30 |
+
args = (self, source, target)
|
| 31 |
+
return f(*args, **kwargs)
|
| 32 |
+
return wrapper
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def ftstr2dict(ftstr):
|
| 36 |
+
fts = {}
|
| 37 |
+
for m in re.finditer(r'([-0+])(\w+)', ftstr):
|
| 38 |
+
v, k = m.groups()
|
| 39 |
+
fts[k] = {'-': -1, '0': 0, '+': 1}[v]
|
| 40 |
+
return fts
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class Distance(object):
|
| 44 |
+
"""Measures of phonological distance."""
|
| 45 |
+
|
| 46 |
+
def __init__(self, feature_set='spe+', feature_model='segment'):
|
| 47 |
+
"""Construct a `Distance` object
|
| 48 |
+
|
| 49 |
+
Args:
|
| 50 |
+
feature_set (str): feature set to be used by the `Distance` object
|
| 51 |
+
feature_model (str): feature parsing model to be used by the
|
| 52 |
+
`Distance` object
|
| 53 |
+
"""
|
| 54 |
+
fm = {'strict': _panphon.FeatureTable,
|
| 55 |
+
'permissive': permissive.PermissiveFeatureTable,
|
| 56 |
+
'segment': featuretable.FeatureTable}
|
| 57 |
+
self.fm = fm[feature_model](feature_set=feature_set)
|
| 58 |
+
self.xs = xsampa.XSampa()
|
| 59 |
+
self.dolgo_prime = self._dolgopolsky_prime()
|
| 60 |
+
|
| 61 |
+
def _dolgopolsky_prime(self, filename=os.path.join('data', 'dolgopolsky_prime.yml')):
|
| 62 |
+
"""Reads dolgopolsky classes and constructs function cascade
|
| 63 |
+
|
| 64 |
+
Args:
|
| 65 |
+
filename (str): path to YAML file (from panphon root) containing
|
| 66 |
+
dolgopolsky classes
|
| 67 |
+
"""
|
| 68 |
+
filename = pkg_resources.resource_filename(
|
| 69 |
+
__name__, filename)
|
| 70 |
+
with open(filename, 'r') as f:
|
| 71 |
+
rules = []
|
| 72 |
+
dolgo_prime = yaml.load(f.read(), Loader=yaml.FullLoader)
|
| 73 |
+
for rule in dolgo_prime:
|
| 74 |
+
rules.append((ftstr2dict(rule['def']), rule['label']))
|
| 75 |
+
return rules
|
| 76 |
+
|
| 77 |
+
def map_to_dolgo_prime(self, s):
|
| 78 |
+
"""Map a string to dolgopolsky' classes
|
| 79 |
+
|
| 80 |
+
Args:
|
| 81 |
+
s (unicode): IPA word
|
| 82 |
+
|
| 83 |
+
Returns:
|
| 84 |
+
(unicode): word with all segments collapsed to D' classes
|
| 85 |
+
"""
|
| 86 |
+
segs = []
|
| 87 |
+
for seg in self.fm.seg_regex.finditer(s):
|
| 88 |
+
fts = self.fm.fts(seg.group(0))
|
| 89 |
+
for mask, label in self.dolgo_prime:
|
| 90 |
+
if fts >= mask:
|
| 91 |
+
segs.append(label)
|
| 92 |
+
break
|
| 93 |
+
return ''.join(segs)
|
| 94 |
+
|
| 95 |
+
def levenshtein_distance(self, source, target):
|
| 96 |
+
"""Slow implementation of Levenshtein distance using NumPy arrays
|
| 97 |
+
|
| 98 |
+
Args:
|
| 99 |
+
source (unicode): source word
|
| 100 |
+
target (unicode): target word
|
| 101 |
+
|
| 102 |
+
Returns:
|
| 103 |
+
int: minimum number of Levenshtein edits required to get from
|
| 104 |
+
`source` to `target`
|
| 105 |
+
"""
|
| 106 |
+
if len(source) < len(target):
|
| 107 |
+
return self.levenshtein_distance(target, source)
|
| 108 |
+
# So now we have len(source) >= len(target).
|
| 109 |
+
if len(target) == 0:
|
| 110 |
+
return len(source)
|
| 111 |
+
# We call tuple() to force strings to be used as sequences
|
| 112 |
+
# ('c', 'a', 't', 's') - numpy uses them as values by default.
|
| 113 |
+
source = np.array(tuple(source))
|
| 114 |
+
target = np.array(tuple(target))
|
| 115 |
+
# We use a dynamic programming algorithm, but with the
|
| 116 |
+
# added optimization that we only need the last two rows
|
| 117 |
+
# of the matrix.
|
| 118 |
+
previous_row = np.arange(target.size + 1)
|
| 119 |
+
for s in source:
|
| 120 |
+
# Insertion (target grows longer than source):
|
| 121 |
+
current_row = previous_row + 1
|
| 122 |
+
# Substitution or matching:
|
| 123 |
+
# Target and source items are aligned, and either
|
| 124 |
+
# are different (cost of 1), or are the same (cost of 0).
|
| 125 |
+
current_row[1:] = np.minimum(current_row[1:], np.add(previous_row[:-1], target != s))
|
| 126 |
+
# Deletion (target grows shorter than source):
|
| 127 |
+
current_row[1:] = np.minimum(current_row[1:], current_row[0:-1] + 1)
|
| 128 |
+
previous_row = current_row
|
| 129 |
+
return previous_row[-1]
|
| 130 |
+
|
| 131 |
+
def fast_levenshtein_distance(self, source, target):
|
| 132 |
+
"""Wrapper for the distance function in the Levenshtein module
|
| 133 |
+
|
| 134 |
+
Args:
|
| 135 |
+
source (unicode): source word
|
| 136 |
+
target (unicode): target word
|
| 137 |
+
|
| 138 |
+
Returns:
|
| 139 |
+
int: minimum number of Levenshtein edits required to get from
|
| 140 |
+
`source` to `target`
|
| 141 |
+
"""
|
| 142 |
+
return int(editdistance.eval(source, target))
|
| 143 |
+
|
| 144 |
+
def fast_levenshtein_distance_div_maxlen(self, source, target):
|
| 145 |
+
"""Levenshtein distance divided by maxlen
|
| 146 |
+
|
| 147 |
+
Args:
|
| 148 |
+
source (unicode): source word
|
| 149 |
+
target (unicode): target word
|
| 150 |
+
|
| 151 |
+
Returns:
|
| 152 |
+
int: minimum number of Levenshtein edits required to get from
|
| 153 |
+
`source` to `target` divided by the length of the longest
|
| 154 |
+
of these arguments
|
| 155 |
+
"""
|
| 156 |
+
maxlen = max(len(source), len(target))
|
| 157 |
+
return int(editdistance.eval(source, target)) / maxlen
|
| 158 |
+
|
| 159 |
+
def dolgo_prime_distance(self, source, target):
|
| 160 |
+
"""Levenshtein distance using D' phonetic equivalence classes
|
| 161 |
+
|
| 162 |
+
`source` and `target` are converted to dolgopolsky' equivalence classes
|
| 163 |
+
(each segment is mapped to the appropriate class) and then the
|
| 164 |
+
Levenshtein distance between the resulting representations is
|
| 165 |
+
computed.
|
| 166 |
+
|
| 167 |
+
Args:
|
| 168 |
+
source (unicode): source word
|
| 169 |
+
target (unicode): target word
|
| 170 |
+
|
| 171 |
+
Returns:
|
| 172 |
+
int: minimum number of Levenshtein edits required to get from
|
| 173 |
+
dolgopolsky' versions of `source` to `target`
|
| 174 |
+
"""
|
| 175 |
+
source = self.map_to_dolgo_prime(source)
|
| 176 |
+
target = self.map_to_dolgo_prime(target)
|
| 177 |
+
return self.fast_levenshtein_distance(source, target)
|
| 178 |
+
|
| 179 |
+
@zerodiviszero
|
| 180 |
+
@xsampaopt
|
| 181 |
+
def dolgo_prime_distance_div_maxlen(self, source, target, xsampa=False):
|
| 182 |
+
"""Levenshtein distance using D' classes, normalized by max length
|
| 183 |
+
|
| 184 |
+
`source` and `target` are converted to dolgopolsky' equivalence classes
|
| 185 |
+
(each segment is mapped to the appropriate class) and then the
|
| 186 |
+
Levenshtein distance between the resulting representations is
|
| 187 |
+
computed. The result is divided by the length of the longest argument
|
| 188 |
+
(`source` or `target`) after mapping to D' classes.
|
| 189 |
+
|
| 190 |
+
Args:
|
| 191 |
+
source (unicode): source word
|
| 192 |
+
target (unicode): target word
|
| 193 |
+
|
| 194 |
+
Returns:
|
| 195 |
+
int: minimum number of Levenshtein edits required to get from
|
| 196 |
+
dolgopolsky' versions of `source` to `target`
|
| 197 |
+
"""
|
| 198 |
+
source = self.map_to_dolgo_prime(source)
|
| 199 |
+
target = self.map_to_dolgo_prime(target)
|
| 200 |
+
maxlen = max(len(source), len(target))
|
| 201 |
+
return self.fast_levenshtein_distance(source, target) / maxlen
|
| 202 |
+
|
| 203 |
+
def min_edit_distance(self, del_cost, ins_cost, sub_cost, start, source, target):
|
| 204 |
+
"""Return minimum edit distance, parameterized, slow
|
| 205 |
+
|
| 206 |
+
Args:
|
| 207 |
+
del_cost (function): cost function for deletion
|
| 208 |
+
ins_cost (function): cost function for insertion
|
| 209 |
+
sub_cost (function): cost function for substitution
|
| 210 |
+
start (sequence): start symbol: string for strings, list for lists,
|
| 211 |
+
list of list for list of lists
|
| 212 |
+
source (sequence): source string/sequence of feature vectors
|
| 213 |
+
target (sequence): target string/sequence of feature vectors
|
| 214 |
+
|
| 215 |
+
Returns:
|
| 216 |
+
Number: minimum edit distance from source to target, with edit costs
|
| 217 |
+
as defined
|
| 218 |
+
"""
|
| 219 |
+
# Get lengths of source and target
|
| 220 |
+
n, m = len(source), len(target)
|
| 221 |
+
source, target = start + source, start + target
|
| 222 |
+
# Create "matrix"
|
| 223 |
+
d = []
|
| 224 |
+
for i in range(n + 1):
|
| 225 |
+
d.append((m + 1) * [None])
|
| 226 |
+
# Initialize "matrix"
|
| 227 |
+
d[0][0] = 0
|
| 228 |
+
for i in range(1, n + 1):
|
| 229 |
+
d[i][0] = d[i - 1][0] + del_cost(source[i])
|
| 230 |
+
for j in range(1, m + 1):
|
| 231 |
+
d[0][j] = d[0][j - 1] + ins_cost(target[j])
|
| 232 |
+
# Recurrence relation
|
| 233 |
+
for i in range(1, n + 1):
|
| 234 |
+
for j in range(1, m + 1):
|
| 235 |
+
d[i][j] = min([
|
| 236 |
+
d[i - 1][j] + del_cost(source[i]),
|
| 237 |
+
d[i - 1][j - 1] + sub_cost(source[i], target[j]),
|
| 238 |
+
d[i][j - 1] + ins_cost(target[j]),
|
| 239 |
+
])
|
| 240 |
+
return d[n][m]
|
| 241 |
+
|
| 242 |
+
def feature_difference(self, ft1, ft2):
|
| 243 |
+
"""Given two feature values, return the difference divided by 2 *deprecated*
|
| 244 |
+
|
| 245 |
+
Args:
|
| 246 |
+
ft1 (int): feature value in {1, 0, -1}
|
| 247 |
+
ft2 (int): feature value in {1, 0, -1}
|
| 248 |
+
|
| 249 |
+
Returns:
|
| 250 |
+
float: half the absolute value of the difference between ft1 and ft2
|
| 251 |
+
"""
|
| 252 |
+
return abs(ft1 - ft2) / 2
|
| 253 |
+
|
| 254 |
+
def unweighted_deletion_cost(self, v1, gl_wt=1.0):
|
| 255 |
+
"""Return cost of deleting segment corresponding to feature vector
|
| 256 |
+
|
| 257 |
+
Features are not weighted; features specified as '0' add 0.5 to the raw
|
| 258 |
+
deletion cost; other features add 1 to the raw deletion cost; the cost
|
| 259 |
+
is normalized by the number of features
|
| 260 |
+
|
| 261 |
+
Args:
|
| 262 |
+
v1 (list): vector of feature values
|
| 263 |
+
global_weight (Number): global weighting factor
|
| 264 |
+
|
| 265 |
+
Returns:
|
| 266 |
+
float: sum of feature costs divided by the number of features and
|
| 267 |
+
multiplied by a global weighting factor
|
| 268 |
+
"""
|
| 269 |
+
assert isinstance(v1, list)
|
| 270 |
+
return sum(map(lambda x: 0.5 if x == 0 else 1, v1)) / len(v1) * gl_wt
|
| 271 |
+
|
| 272 |
+
def unweighted_substitution_cost(self, v1, v2):
|
| 273 |
+
"""Given two feature vectors, return the difference
|
| 274 |
+
|
| 275 |
+
Args:
|
| 276 |
+
v1 (list): vector of feature values
|
| 277 |
+
v2 (list): vector of feature values
|
| 278 |
+
|
| 279 |
+
Returns:
|
| 280 |
+
float: sum of the differences between the features in `v1` and `v2`,
|
| 281 |
+
divided by the number of features
|
| 282 |
+
"""
|
| 283 |
+
return sum([abs(ft1 - ft2) / 2 for (ft1, ft2) in zip(v1, v2)]) / len(v1)
|
| 284 |
+
|
| 285 |
+
def unweighted_insertion_cost(self, v1, gl_wt=1.0):
|
| 286 |
+
"""Return cost of inserting segment corresponding to feature vector
|
| 287 |
+
|
| 288 |
+
Features are not weighted; features with the value '0' add 0.5 to the
|
| 289 |
+
raw cost; other features add 1.0 to the raw cost; the raw cost is then
|
| 290 |
+
normalized by the number of features
|
| 291 |
+
|
| 292 |
+
Args:
|
| 293 |
+
v1 (list): vector of feature values
|
| 294 |
+
global_weight (Number): global weighting factor
|
| 295 |
+
|
| 296 |
+
Returns:
|
| 297 |
+
float: sum of the costs of inserting each of the features in `v1`
|
| 298 |
+
divided by the number of features in the vector and
|
| 299 |
+
multiplied by a global weighting factor
|
| 300 |
+
"""
|
| 301 |
+
return sum(map(lambda x: 0.5 if x == 0 else 1, v1)) / len(v1) * gl_wt
|
| 302 |
+
|
| 303 |
+
@xsampaopt
|
| 304 |
+
def feature_edit_distance(self, source, target, xsampa=False):
|
| 305 |
+
"""String edit distance with equally-weighed features.
|
| 306 |
+
|
| 307 |
+
All articulatory features are given equal weight. The distance between
|
| 308 |
+
an unspecified value and a specified value is smaller than the distance
|
| 309 |
+
between two features with oppoiste values.
|
| 310 |
+
|
| 311 |
+
Args:
|
| 312 |
+
source (unicode): source string
|
| 313 |
+
target (unicode): target string
|
| 314 |
+
|
| 315 |
+
Returns:
|
| 316 |
+
float: feature edit distance with equally-weighed features an insdel
|
| 317 |
+
costs set so insdel operations cost as much, roughly, as
|
| 318 |
+
substituting a whole segment
|
| 319 |
+
"""
|
| 320 |
+
return self.min_edit_distance(self.unweighted_deletion_cost,
|
| 321 |
+
self.unweighted_insertion_cost,
|
| 322 |
+
self.unweighted_substitution_cost,
|
| 323 |
+
[[]],
|
| 324 |
+
self.fm.word_to_vector_list(source, numeric=True),
|
| 325 |
+
self.fm.word_to_vector_list(target, numeric=True))
|
| 326 |
+
|
| 327 |
+
@xsampaopt
|
| 328 |
+
def jt_feature_edit_distance(self, source, target, xsampa=False):
|
| 329 |
+
"""String edit distance with equally-weighed features.
|
| 330 |
+
|
| 331 |
+
All articulatory features are given equal weight. The distance between
|
| 332 |
+
an unspecified value and a specified value is smaller than the distance
|
| 333 |
+
between two features with oppoiste values. Insdel costs are cheap.
|
| 334 |
+
|
| 335 |
+
Args:
|
| 336 |
+
source (unicode): source string
|
| 337 |
+
target (unicode): target string
|
| 338 |
+
xsampa (bool): source and target are X-SAMPA
|
| 339 |
+
|
| 340 |
+
Returns:
|
| 341 |
+
float: feature edit distance with equally-weighed features and insdel
|
| 342 |
+
costs set so insdel operations cost 1/4 as much, roughly, as
|
| 343 |
+
substituting a whole segment
|
| 344 |
+
"""
|
| 345 |
+
return self.min_edit_distance(partial(self.unweighted_deletion_cost, gl_wt=0.25),
|
| 346 |
+
partial(self.unweighted_insertion_cost, gl_wt=0.25),
|
| 347 |
+
self.unweighted_substitution_cost,
|
| 348 |
+
[[]],
|
| 349 |
+
self.fm.word_to_vector_list(source, numeric=True),
|
| 350 |
+
self.fm.word_to_vector_list(target, numeric=True))
|
| 351 |
+
|
| 352 |
+
@zerodiviszero
|
| 353 |
+
@xsampaopt
|
| 354 |
+
def feature_edit_distance_div_maxlen(self, source, target, xsampa=False):
|
| 355 |
+
"""Like `Distance.feature_edit_distance` but normalized by maxlen
|
| 356 |
+
|
| 357 |
+
Args:
|
| 358 |
+
source (unicode): source string
|
| 359 |
+
target (unicode): target string
|
| 360 |
+
xsampa (bool): source and target are X-SAMPA
|
| 361 |
+
|
| 362 |
+
Returns:
|
| 363 |
+
float: feature edit distance with equally-weighed features and insdel
|
| 364 |
+
costs set so insdel operations cost as much, roughly, as
|
| 365 |
+
substituting a whole segment
|
| 366 |
+
|
| 367 |
+
Raw result is divided by the length of the longest argument
|
| 368 |
+
"""
|
| 369 |
+
source_len, target_len = len(self.fm.word_to_vector_list(source)), len(self.fm.word_to_vector_list(target))
|
| 370 |
+
maxlen = max(source_len, target_len)
|
| 371 |
+
return self.feature_edit_distance(source, target) / maxlen
|
| 372 |
+
|
| 373 |
+
@zerodiviszero
|
| 374 |
+
@xsampaopt
|
| 375 |
+
def jt_feature_edit_distance_div_maxlen(self, source, target, xsampa=False):
|
| 376 |
+
"""Like `Distance.feature_edit_distance` but normalized by maxlen
|
| 377 |
+
|
| 378 |
+
Args:
|
| 379 |
+
source (unicode): source string
|
| 380 |
+
target (unicode): target string
|
| 381 |
+
xsampa (bool): source and target are X-SAMPA
|
| 382 |
+
|
| 383 |
+
Returns:
|
| 384 |
+
float: feature edit distance with equally-weighed features and insdel
|
| 385 |
+
costs set so insdel operations cost 1/4 as much, roughly, as
|
| 386 |
+
substituting a whole segment
|
| 387 |
+
|
| 388 |
+
Raw result is divided by the length of the longest argument
|
| 389 |
+
"""
|
| 390 |
+
source_len, target_len = len(self.fm.word_to_vector_list(source)), len(self.fm.word_to_vector_list(target))
|
| 391 |
+
maxlen = max(source_len, target_len)
|
| 392 |
+
return self.jt_feature_edit_distance(source, target) / maxlen
|
| 393 |
+
|
| 394 |
+
def phoneme_error_rate(self, hyp, ref):
|
| 395 |
+
"""Phoneme error rate over lists of hypothesized and reference strings.
|
| 396 |
+
Calculates edit distance in terms of phonemes, instead of Unicode characters
|
| 397 |
+
Normalizes by the total number of phones in the reference
|
| 398 |
+
|
| 399 |
+
Args:
|
| 400 |
+
hyp (list[unicode]): hypothesized strings
|
| 401 |
+
ref (list[unicode]): reference strings
|
| 402 |
+
|
| 403 |
+
Returns:
|
| 404 |
+
float: phoneme error rate (PER)
|
| 405 |
+
"""
|
| 406 |
+
if hyp and ref:
|
| 407 |
+
errors = []
|
| 408 |
+
for (h, r) in zip(hyp, ref):
|
| 409 |
+
phoneme_edits = self.min_edit_distance(
|
| 410 |
+
lambda v: 1,
|
| 411 |
+
lambda v: 1,
|
| 412 |
+
lambda x,y: 0 if x == y else 1,
|
| 413 |
+
[[]],
|
| 414 |
+
self.fm.ipa_segs(h),
|
| 415 |
+
self.fm.ipa_segs(r)
|
| 416 |
+
)
|
| 417 |
+
errors.append(phoneme_edits)
|
| 418 |
+
total_phones = sum([len(self.fm.ipa_segs(r)) for r in ref])
|
| 419 |
+
|
| 420 |
+
return sum(errors) / total_phones
|
| 421 |
+
else:
|
| 422 |
+
return 0.0
|
| 423 |
+
|
| 424 |
+
def feature_error_rate(self, hyp, ref, xsampa=False):
|
| 425 |
+
"""Feature error rate over lists of hypothesized and reference strings.
|
| 426 |
+
|
| 427 |
+
Args:
|
| 428 |
+
hyp (list[unicode]): hypothesized strings
|
| 429 |
+
ref (list[unicode]): reference strings
|
| 430 |
+
|
| 431 |
+
Returns:
|
| 432 |
+
float: feature error rate (FER)
|
| 433 |
+
|
| 434 |
+
"""
|
| 435 |
+
if hyp and ref:
|
| 436 |
+
errors = sum([self.feature_edit_distance(h, r) for (h, r) in zip(hyp, ref)])
|
| 437 |
+
ft = featuretable.FeatureTable()
|
| 438 |
+
total_phones = sum([len(ft.ipa_segs(r)) for r in ref])
|
| 439 |
+
return errors / total_phones
|
| 440 |
+
else:
|
| 441 |
+
return 0.0
|
| 442 |
+
|
| 443 |
+
def hamming_substitution_cost(self, v1, v2):
|
| 444 |
+
"""Substitution cost for feature vectors computed as Hamming distance.
|
| 445 |
+
|
| 446 |
+
Substitution cost for feature vectors computed as Hamming distance and
|
| 447 |
+
normalized by dividing this result by the length of the vectors.
|
| 448 |
+
|
| 449 |
+
Args:
|
| 450 |
+
v1 (list): feature vector
|
| 451 |
+
v2 (list): feature vector
|
| 452 |
+
|
| 453 |
+
Returns:
|
| 454 |
+
float: Hamming distance between `v1` and `v2` divided by the length
|
| 455 |
+
of `v1` and `v2`
|
| 456 |
+
"""
|
| 457 |
+
diffs = [ft1 != ft2 for (ft1, ft2) in zip(v1, v2)]
|
| 458 |
+
return sum(diffs) / len(diffs) # Booleans are cohersed to integers.
|
| 459 |
+
|
| 460 |
+
@xsampaopt
|
| 461 |
+
def hamming_feature_edit_distance(self, source, target, xsampa=False):
|
| 462 |
+
"""String edit distance with equally-weighed features.
|
| 463 |
+
|
| 464 |
+
All articulatory features are given equal weight. The distance between an
|
| 465 |
+
unspecified value and a specified value is smaller than the distance between
|
| 466 |
+
two features with oppoiste values.
|
| 467 |
+
|
| 468 |
+
The insertion and deletion cost is always one, somewhat favoring
|
| 469 |
+
substitution.
|
| 470 |
+
|
| 471 |
+
This function has no normalization but should obey the triangle
|
| 472 |
+
inequality and thus provide a true distance metric.
|
| 473 |
+
|
| 474 |
+
Args:
|
| 475 |
+
source (unicode): source string
|
| 476 |
+
target (unicode): target string
|
| 477 |
+
xsampa (bool): source and target are X-SAMPA
|
| 478 |
+
|
| 479 |
+
Returns:
|
| 480 |
+
float: Hamming feature edit distance between `source` and `target`
|
| 481 |
+
with high insdel costs
|
| 482 |
+
"""
|
| 483 |
+
return self.min_edit_distance(lambda v: 1,
|
| 484 |
+
lambda v: 1,
|
| 485 |
+
self.hamming_substitution_cost,
|
| 486 |
+
[[]],
|
| 487 |
+
self.fm.word_to_vector_list(source, numeric=True),
|
| 488 |
+
self.fm.word_to_vector_list(target, numeric=True))
|
| 489 |
+
|
| 490 |
+
@xsampaopt
|
| 491 |
+
def jt_hamming_feature_edit_distance(self, source, target, xsampa=False):
|
| 492 |
+
"""String edit distance with equally-weighed features.
|
| 493 |
+
|
| 494 |
+
All articulatory features are given equal weight. The distance between an
|
| 495 |
+
unspecified value and a specified value is smaller than the distance between
|
| 496 |
+
two features with oppoiste values.
|
| 497 |
+
|
| 498 |
+
The insertion and deletion cost is always one, somewhat favoring
|
| 499 |
+
substitution.
|
| 500 |
+
|
| 501 |
+
This function has no normalization but should obey the triangle
|
| 502 |
+
inequality and thus provide a true distance metric.
|
| 503 |
+
|
| 504 |
+
Args:
|
| 505 |
+
source (unicode): source string
|
| 506 |
+
target (unicode): target string
|
| 507 |
+
xsampa (bool): source and target are X-SAMPA
|
| 508 |
+
|
| 509 |
+
Returns:
|
| 510 |
+
float: Hamming feature edit distance between `source` and `target`
|
| 511 |
+
with low insdel costs (1/4 cost of total substitution)
|
| 512 |
+
"""
|
| 513 |
+
return self.min_edit_distance(lambda v: 0.25,
|
| 514 |
+
lambda v: 0.25,
|
| 515 |
+
self.hamming_substitution_cost,
|
| 516 |
+
[[]],
|
| 517 |
+
self.fm.word_to_vector_list(source, numeric=True),
|
| 518 |
+
self.fm.word_to_vector_list(target, numeric=True))
|
| 519 |
+
|
| 520 |
+
@zerodiviszero
|
| 521 |
+
@xsampaopt
|
| 522 |
+
def hamming_feature_edit_distance_div_maxlen(self, source, target, xsampa=False):
|
| 523 |
+
"""Hamming feature edit distance divded by maxlen
|
| 524 |
+
|
| 525 |
+
The same as `Distance.hamming_feature_edit_distance` except that the
|
| 526 |
+
resulting value is divided by the length of the longest argument. It
|
| 527 |
+
therefore does not obey the triangle inequality and is not a proper
|
| 528 |
+
metric.
|
| 529 |
+
|
| 530 |
+
Args:
|
| 531 |
+
source (unicode): source string
|
| 532 |
+
target (unicode): target string
|
| 533 |
+
xsampa (bool): source and target are X-SAMPA
|
| 534 |
+
|
| 535 |
+
Returns:
|
| 536 |
+
float: Hamming feature edit distance between `source` and `target`
|
| 537 |
+
with high insdel costs, normalized by length of longest
|
| 538 |
+
argument
|
| 539 |
+
"""
|
| 540 |
+
source = self.fm.word_to_vector_list(source, numeric=True)
|
| 541 |
+
target = self.fm.word_to_vector_list(target, numeric=True)
|
| 542 |
+
maxlen = max(len(source), len(target))
|
| 543 |
+
raw = self.min_edit_distance(lambda v: 1,
|
| 544 |
+
lambda v: 1,
|
| 545 |
+
self.hamming_substitution_cost,
|
| 546 |
+
[[]],
|
| 547 |
+
source,
|
| 548 |
+
target)
|
| 549 |
+
return raw / maxlen
|
| 550 |
+
|
| 551 |
+
@xsampaopt
|
| 552 |
+
def jt_hamming_feature_edit_distance_div_maxlen(self, source, target, xsampa=False):
|
| 553 |
+
"""Hamming feature edit distance divded by maxlen
|
| 554 |
+
|
| 555 |
+
The same as `Distance.hamming_feature_edit_distance` except that the
|
| 556 |
+
resulting value is divided by the length of the longest argument. It
|
| 557 |
+
therefore does not obey the triangle inequality and is not a proper
|
| 558 |
+
metric.
|
| 559 |
+
|
| 560 |
+
Args:
|
| 561 |
+
source (unicode): source string
|
| 562 |
+
target (unicode): target string
|
| 563 |
+
xsampa (bool): source and target are X-SAMPA
|
| 564 |
+
|
| 565 |
+
Returns:
|
| 566 |
+
float: Hamming feature edit distance between `source` and `target`
|
| 567 |
+
with low insdel costs, normalized by length of longest
|
| 568 |
+
argument
|
| 569 |
+
"""
|
| 570 |
+
source = self.fm.word_to_vector_list(source, numeric=True)
|
| 571 |
+
target = self.fm.word_to_vector_list(target, numeric=True)
|
| 572 |
+
maxlen = max(len(source), len(target))
|
| 573 |
+
raw = self.min_edit_distance(lambda v: 0.25,
|
| 574 |
+
lambda v: 0.25,
|
| 575 |
+
self.hamming_substitution_cost,
|
| 576 |
+
[[]], source, target)
|
| 577 |
+
return raw / maxlen
|
| 578 |
+
|
| 579 |
+
def weighted_feature_difference(self, w, ft1, ft2):
|
| 580 |
+
"""Return the weighted difference between two features *deprecated*
|
| 581 |
+
|
| 582 |
+
Args:
|
| 583 |
+
w (Number): weight
|
| 584 |
+
ft1 (str): feature value
|
| 585 |
+
ft2 (str): feature value
|
| 586 |
+
|
| 587 |
+
Returns:
|
| 588 |
+
float: difference between two features multiplied by weight; raw
|
| 589 |
+
differences are:
|
| 590 |
+
'+' - '-' = 1.0
|
| 591 |
+
'-' - '+' = 1.0
|
| 592 |
+
'+' - '0' = 0.5
|
| 593 |
+
'-' - '0' = 0.5
|
| 594 |
+
'0' - '+' = 0.5
|
| 595 |
+
'0' - '-' = 0.5
|
| 596 |
+
Raw differences are multipled by weight `w`
|
| 597 |
+
"""
|
| 598 |
+
return self.feature_difference(ft1, ft2) * w
|
| 599 |
+
|
| 600 |
+
def weighted_substitution_cost(self, v1, v2, gl_wt=1.0):
|
| 601 |
+
"""Given two feature vectors, return the difference
|
| 602 |
+
|
| 603 |
+
Args:
|
| 604 |
+
v1 (list): feature vector
|
| 605 |
+
v2 (list): feature vector
|
| 606 |
+
|
| 607 |
+
Returns:
|
| 608 |
+
float: sum of weighted feature difference for each feature pair in
|
| 609 |
+
zip(v1, v2)
|
| 610 |
+
"""
|
| 611 |
+
return sum([abs(ft1 - ft2) * w
|
| 612 |
+
for (w, ft1, ft2)
|
| 613 |
+
in zip(self.fm.weights, v1, v2)]) * gl_wt
|
| 614 |
+
|
| 615 |
+
def weighted_insertion_cost(self, v1, gl_wt=1.0):
|
| 616 |
+
"""Return cost of inserting segment corresponding to feature vector
|
| 617 |
+
|
| 618 |
+
Args:
|
| 619 |
+
v1 (list): feature vector
|
| 620 |
+
gl_wt (float): global weights
|
| 621 |
+
|
| 622 |
+
Returns:
|
| 623 |
+
float: sum of weights multiplied by global weight (`gl_wt`)
|
| 624 |
+
"""
|
| 625 |
+
assert isinstance(v1, list)
|
| 626 |
+
return sum(self.fm.weights) * gl_wt
|
| 627 |
+
|
| 628 |
+
def weighted_deletion_cost(self, v1, gl_wt=1.0):
|
| 629 |
+
"""Return cost of deleting segment corresponding to feature vector
|
| 630 |
+
|
| 631 |
+
Args:
|
| 632 |
+
v1 (list): feature vector
|
| 633 |
+
gl_wt (float): global weights
|
| 634 |
+
|
| 635 |
+
Returns:
|
| 636 |
+
float: sum of weights multiplied by global weight (`gl_wt`)"""
|
| 637 |
+
assert isinstance(v1, list)
|
| 638 |
+
return sum(self.fm.weights) * gl_wt
|
| 639 |
+
|
| 640 |
+
def weighted_feature_edit_distance(self, source, target, xsampa=False):
|
| 641 |
+
"""String edit distance with weighted features
|
| 642 |
+
|
| 643 |
+
The cost of changine an articulatory feature is weighted according to
|
| 644 |
+
the class of the feature and the subjective probability of the
|
| 645 |
+
feature changing in phonological alternation and loanword contexts.
|
| 646 |
+
These weights are stored in `Distance.weights`.
|
| 647 |
+
|
| 648 |
+
Args:
|
| 649 |
+
source (unicode): source string
|
| 650 |
+
target (uniocde): target string
|
| 651 |
+
xsampa (bool): source and target are X-SAMPA
|
| 652 |
+
|
| 653 |
+
Returns:
|
| 654 |
+
float: feature weighted string edit distance between `source` and
|
| 655 |
+
`target`
|
| 656 |
+
"""
|
| 657 |
+
return self.min_edit_distance(self.weighted_deletion_cost,
|
| 658 |
+
self.weighted_insertion_cost,
|
| 659 |
+
self.weighted_substitution_cost,
|
| 660 |
+
[[]],
|
| 661 |
+
self.fm.word_to_vector_list(source, numeric=True, xsampa=xsampa),
|
| 662 |
+
self.fm.word_to_vector_list(target, numeric=True, xsampa=xsampa))
|
| 663 |
+
|
| 664 |
+
@xsampaopt
|
| 665 |
+
def jt_weighted_feature_edit_distance(self, source, target, xsampa=False):
|
| 666 |
+
"""String edit distance with weighted features
|
| 667 |
+
|
| 668 |
+
The cost of changine an articulatory feature is weighted according to
|
| 669 |
+
the class of the feature and the subjective probability of the
|
| 670 |
+
feature changing in phonological alternation and loanword contexts.
|
| 671 |
+
These weights are stored in `Distance.weights`.
|
| 672 |
+
|
| 673 |
+
Args:
|
| 674 |
+
source (unicode): source string
|
| 675 |
+
target (uniocde): target string
|
| 676 |
+
xsampa (bool): source and target are X-SAMPA
|
| 677 |
+
|
| 678 |
+
Returns:
|
| 679 |
+
float: feature weighted string edit distance between `source` and
|
| 680 |
+
`target`
|
| 681 |
+
"""
|
| 682 |
+
return self.min_edit_distance(partial(self.weighted_deletion_cost, gl_wt=0.25),
|
| 683 |
+
partial(self.weighted_insertion_cost, gl_wt=0.25),
|
| 684 |
+
self.weighted_substitution_cost,
|
| 685 |
+
[[]],
|
| 686 |
+
self.fm.word_to_vector_list(source, numeric=True),
|
| 687 |
+
self.fm.word_to_vector_list(target, numeric=True))
|
| 688 |
+
|
| 689 |
+
@zerodiviszero
|
| 690 |
+
@xsampaopt
|
| 691 |
+
def weighted_feature_edit_distance_div_maxlen(self, source, target, xsampa=False):
|
| 692 |
+
"""String edit distance with weighted features, divided by maxlen
|
| 693 |
+
|
| 694 |
+
The cost of changine an articulatory feature is weighted according to
|
| 695 |
+
the class of the feature and the subjective probability of the
|
| 696 |
+
feature changing in phonological alternation and loanword contexts.
|
| 697 |
+
These weights are stored in `Distance.weights`.
|
| 698 |
+
|
| 699 |
+
Args:
|
| 700 |
+
source (unicode): source string
|
| 701 |
+
target (uniocde): target string
|
| 702 |
+
xsampa (bool): source and target are X-SAMPA
|
| 703 |
+
|
| 704 |
+
Returns:
|
| 705 |
+
float: feature weighted string edit distance between `source` and
|
| 706 |
+
`target` divided by the length of the longest of these
|
| 707 |
+
arguments
|
| 708 |
+
"""
|
| 709 |
+
source = self.fm.word_to_vector_list(source, numeric=True, xsampa=xsampa)
|
| 710 |
+
target = self.fm.word_to_vector_list(target, numeric=True, xsampa=xsampa)
|
| 711 |
+
maxlen = max(len(source), len(target))
|
| 712 |
+
return self.min_edit_distance(self.weighted_deletion_cost,
|
| 713 |
+
self.weighted_insertion_cost,
|
| 714 |
+
self.weighted_substitution_cost,
|
| 715 |
+
[[]],
|
| 716 |
+
source,
|
| 717 |
+
target) / maxlen
|
| 718 |
+
|
| 719 |
+
@zerodiviszero
|
| 720 |
+
@xsampaopt
|
| 721 |
+
def jt_weighted_feature_edit_distance_div_maxlen(self, source, target, xsampa=False):
|
| 722 |
+
"""String edit distance with weighted features, cheap insdel, divided by maxlen
|
| 723 |
+
|
| 724 |
+
The cost of changine an articulatory feature is weighted according to
|
| 725 |
+
the class of the feature and the subjective probability of the
|
| 726 |
+
feature changing in phonological alternation and loanword contexts.
|
| 727 |
+
These weights are stored in `Distance.weights`.
|
| 728 |
+
|
| 729 |
+
This is like `Distance.weighted_feature_edit_distance_div_maxlen` except
|
| 730 |
+
with low insdel costs (1/4 the cost of a complete substitution).
|
| 731 |
+
|
| 732 |
+
Args:
|
| 733 |
+
source (unicode): source string
|
| 734 |
+
target (uniocde): target string
|
| 735 |
+
xsampa (bool): source and target are X-SAMPA
|
| 736 |
+
|
| 737 |
+
Returns:
|
| 738 |
+
float: feature weighted string edit distance between `source` and
|
| 739 |
+
`target` divided by the length of the longest of these
|
| 740 |
+
arguments
|
| 741 |
+
"""
|
| 742 |
+
source = self.fm.word_to_vector_list(source, numeric=True)
|
| 743 |
+
target = self.fm.word_to_vector_list(target, numeric=True)
|
| 744 |
+
maxlen = max(len(source), len(target))
|
| 745 |
+
return self.min_edit_distance(partial(self.weighted_deletion_cost, gl_wt=0.25),
|
| 746 |
+
partial(self.weighted_insertion_cost, gl_wt=0.25),
|
| 747 |
+
self.weighted_substitution_cost,
|
| 748 |
+
[[]],
|
| 749 |
+
source,
|
| 750 |
+
target) / maxlen
|
| 751 |
+
|
| 752 |
+
def partial_hamming_substitution_cost(self, v1, v2):
|
| 753 |
+
"""Substitution cost for feature vectors computed in a manner sensitive to specification.
|
| 754 |
+
|
| 755 |
+
Substitution cost for feature vectors computed so that
|
| 756 |
+
specified-to-specified costs 1/|V| and specified-to-unspecified costs
|
| 757 |
+
1/2*|V|.
|
| 758 |
+
|
| 759 |
+
Args: v1 (list): feature vector v2 (list): feature vector
|
| 760 |
+
|
| 761 |
+
Returns: float: Special edit distance where substitutions are less
|
| 762 |
+
expensive of one of the features is not specified
|
| 763 |
+
"""
|
| 764 |
+
def subcost(ft1, ft2):
|
| 765 |
+
if ft1 == ft2:
|
| 766 |
+
return 0
|
| 767 |
+
elif ft1 == 0 or ft2 == 0:
|
| 768 |
+
return 0.5
|
| 769 |
+
else:
|
| 770 |
+
return 1
|
| 771 |
+
diffs = [subcost(ft1, ft2) for (ft1, ft2) in zip(v1, v2)]
|
| 772 |
+
return sum(diffs) / len(diffs)
|
| 773 |
+
|
| 774 |
+
@xsampaopt
|
| 775 |
+
def partial_hamming_feature_edit_distance(self, source, target, xsampa=False):
|
| 776 |
+
"""String edit distance with insdel cost = 1 and sub costs are 1/22 or 1/44 depending on specification.
|
| 777 |
+
|
| 778 |
+
This method implements a distance metric which is neither identical to
|
| 779 |
+
hamming distance nor to feature edit distance.
|
| 780 |
+
|
| 781 |
+
The insertion/deletion cost for segment is always 1. The cost of
|
| 782 |
+
substituting a specified feature for a specified feature is 1/|V| where
|
| 783 |
+
|V| is the number of dimensions in a feature vector. The cost of
|
| 784 |
+
substituting a feature specification for an unspecified feature is
|
| 785 |
+
1/2*|V|.
|
| 786 |
+
|
| 787 |
+
This function has no normalization and should obey the triangle
|
| 788 |
+
inequality and thus provide a true distance metric.
|
| 789 |
+
|
| 790 |
+
Args: source (unicode): source string target (unicode): target string
|
| 791 |
+
xsampa (bool): source and target are X-SAMPA
|
| 792 |
+
|
| 793 |
+
Returns: float: Partial hamming feature edit distance between `source`
|
| 794 |
+
and `target`
|
| 795 |
+
"""
|
| 796 |
+
source = self.fm.word_to_vector_list(source, numeric=True)
|
| 797 |
+
target = self.fm.word_to_vector_list(target, numeric=True)
|
| 798 |
+
return self.min_edit_distance(lambda v: 1,
|
| 799 |
+
lambda v: 1,
|
| 800 |
+
self.partial_hamming_substitution_cost,
|
| 801 |
+
[[]],
|
| 802 |
+
source,
|
| 803 |
+
target)
|
| 804 |
+
|
| 805 |
+
@zerodiviszero
|
| 806 |
+
@xsampaopt
|
| 807 |
+
def partial_hamming_feature_edit_distance_div_maxlen(self, source, target, xsampa=False):
|
| 808 |
+
"""String edit distance with insdel cost = 1 and sub costs are 1/22 or 1/44 depending on specification.
|
| 809 |
+
|
| 810 |
+
This method implements a distance metric which is neither identical to
|
| 811 |
+
hamming distance nor to feature edit distance and normalizes it by the
|
| 812 |
+
longest input.
|
| 813 |
+
|
| 814 |
+
The insertion/deletion cost for segment is always 1. The cost of
|
| 815 |
+
substituting a specified feature for a specified feature is 1/|V| where
|
| 816 |
+
|V| is the number of dimensions in a feature vector. The cost of
|
| 817 |
+
substituting a feature specification for an unspecified feature is
|
| 818 |
+
1/2*|V|.
|
| 819 |
+
|
| 820 |
+
This method is normalized and does not satisfy the triangle inequality.
|
| 821 |
+
It is thus not a true distance metric.
|
| 822 |
+
|
| 823 |
+
Args: source (unicode): source string target (unicode): target string
|
| 824 |
+
xsampa (bool): source and target are X-SAMPA
|
| 825 |
+
|
| 826 |
+
Returns: float: Normalized partial hamming feature edit distance between
|
| 827 |
+
`source` and `target`
|
| 828 |
+
"""
|
| 829 |
+
source = self.fm.word_to_vector_list(source, numeric=True)
|
| 830 |
+
target = self.fm.word_to_vector_list(target, numeric=True)
|
| 831 |
+
maxlen = max(len(source), len(target))
|
| 832 |
+
return self.min_edit_distance(lambda v: 1,
|
| 833 |
+
lambda v: 1,
|
| 834 |
+
self.partial_hamming_substitution_cost,
|
| 835 |
+
[[]],
|
| 836 |
+
source,
|
| 837 |
+
target) / maxlen
|
panphon/errors.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class SegmentError(Exception):
|
| 5 |
+
pass
|
panphon/featuretable.py
ADDED
|
@@ -0,0 +1,590 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
from typing import Any, Pattern
|
| 5 |
+
|
| 6 |
+
import os.path
|
| 7 |
+
import unicodedata
|
| 8 |
+
import collections
|
| 9 |
+
|
| 10 |
+
import numpy
|
| 11 |
+
import pkg_resources
|
| 12 |
+
|
| 13 |
+
import regex as re
|
| 14 |
+
import csv
|
| 15 |
+
|
| 16 |
+
from . import xsampa
|
| 17 |
+
from .segment import Segment
|
| 18 |
+
from functools import reduce
|
| 19 |
+
|
| 20 |
+
feature_sets = {
|
| 21 |
+
'spe+': (os.path.join('data', 'ipa_all.csv'),
|
| 22 |
+
os.path.join('data', 'feature_weights.csv'))
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
class SegmentSorter:
|
| 26 |
+
def __init__(self, segments,):
|
| 27 |
+
self._segments = segments
|
| 28 |
+
self._sorted=False
|
| 29 |
+
|
| 30 |
+
@property
|
| 31 |
+
def segments(self):
|
| 32 |
+
if not self._sorted:
|
| 33 |
+
self.sort_segments()
|
| 34 |
+
return self._segments
|
| 35 |
+
|
| 36 |
+
def sort_segments(self):
|
| 37 |
+
self.segments.sort(key=self.segment_key)
|
| 38 |
+
|
| 39 |
+
@staticmethod
|
| 40 |
+
def segment_key(segment_tuple):
|
| 41 |
+
segment_data=segment_tuple[1]
|
| 42 |
+
return (
|
| 43 |
+
segment_data['syl'], segment_data['son'], segment_data['cons'], segment_data['cont'],
|
| 44 |
+
segment_data['delrel'], segment_data['lat'], segment_data['nas'], segment_data['strid'],
|
| 45 |
+
segment_data['voi'], segment_data['sg'], segment_data['cg'], segment_data['ant'],
|
| 46 |
+
segment_data['cor'], segment_data['distr'], segment_data['lab'], segment_data['hi'],
|
| 47 |
+
segment_data['lo'], segment_data['back'], segment_data['round'], segment_data['velaric'],
|
| 48 |
+
segment_data['tense'], segment_data['long'], segment_data['hitone'], segment_data['hireg']
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class FeatureTable(object):
|
| 53 |
+
"""The basic PanPhon object for representing the features of sets of segments.
|
| 54 |
+
|
| 55 |
+
:param feature_set str: The set of fetures to be used by the FeatureTable object.
|
| 56 |
+
"""
|
| 57 |
+
TRIE_LEAF_MARKER = None
|
| 58 |
+
|
| 59 |
+
def __init__(self, feature_set: str='spe+'):
|
| 60 |
+
bases_fn, weights_fn = feature_sets[feature_set]
|
| 61 |
+
self.weights = self._read_weights(weights_fn)
|
| 62 |
+
self.segments, self.seg_dict, self.names = self._read_bases(bases_fn, self.weights)
|
| 63 |
+
self.seg_regex = self._build_seg_regex()
|
| 64 |
+
self.seg_trie = self._build_seg_trie()
|
| 65 |
+
self.longest_seg = max([len(x) for x in self.seg_dict.keys()])
|
| 66 |
+
self.xsampa = xsampa.XSampa()
|
| 67 |
+
|
| 68 |
+
self.sorted_segments = SegmentSorter(self.segments) #used for quick binary searches
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
@staticmethod
|
| 73 |
+
def normalize(data: str) -> str:
|
| 74 |
+
return unicodedata.normalize('NFD', data)
|
| 75 |
+
|
| 76 |
+
def _read_bases(self, fn: str, weights):
|
| 77 |
+
fn = pkg_resources.resource_filename(__name__, fn)
|
| 78 |
+
segments = []
|
| 79 |
+
with open(fn) as f:
|
| 80 |
+
reader = csv.reader(f)
|
| 81 |
+
header = next(reader)
|
| 82 |
+
names = header[1:]
|
| 83 |
+
for row in reader:
|
| 84 |
+
ipa = FeatureTable.normalize(row[0])
|
| 85 |
+
vals = [{'-': -1, '0': 0, '+': 1}[x] for x in row[1:]]
|
| 86 |
+
vec = Segment(names,
|
| 87 |
+
{n: v for (n, v) in zip(names, vals)},
|
| 88 |
+
weights=weights)
|
| 89 |
+
segments.append((ipa, vec))
|
| 90 |
+
seg_dict = dict(segments)
|
| 91 |
+
return segments, seg_dict, names
|
| 92 |
+
|
| 93 |
+
def _read_weights(self, weights_fn: str) -> list[float]:
|
| 94 |
+
weights_fn = pkg_resources.resource_filename(__name__, weights_fn)
|
| 95 |
+
with open(weights_fn) as f:
|
| 96 |
+
reader = csv.reader(f)
|
| 97 |
+
next(reader)
|
| 98 |
+
weights = [float(x) for x in next(reader)]
|
| 99 |
+
return weights
|
| 100 |
+
|
| 101 |
+
def _build_seg_regex(self) -> re.Pattern:
|
| 102 |
+
segs = sorted(self.seg_dict.keys(), key=lambda x: len(x), reverse=True)
|
| 103 |
+
return re.compile(r'(?P<all>{})'.format('|'.join(segs)))
|
| 104 |
+
|
| 105 |
+
def _build_seg_trie(self) -> dict:
|
| 106 |
+
trie = {}
|
| 107 |
+
for seg in self.seg_dict.keys():
|
| 108 |
+
node = trie
|
| 109 |
+
for char in seg:
|
| 110 |
+
if char not in node:
|
| 111 |
+
node[char] = {}
|
| 112 |
+
node = node[char]
|
| 113 |
+
node[self.TRIE_LEAF_MARKER] = None
|
| 114 |
+
return trie
|
| 115 |
+
|
| 116 |
+
def fts(self, ipa: str, normalize: bool=True) -> dict[str, int]:
|
| 117 |
+
if normalize:
|
| 118 |
+
ipa = FeatureTable.normalize(ipa)
|
| 119 |
+
if ipa in self.seg_dict:
|
| 120 |
+
return self.seg_dict[ipa]
|
| 121 |
+
else:
|
| 122 |
+
return {}
|
| 123 |
+
|
| 124 |
+
def longest_one_seg_prefix(self, word: str, normalize: bool=True) -> str:
|
| 125 |
+
"""Return longest Unicode IPA prefix of a word
|
| 126 |
+
|
| 127 |
+
Args:
|
| 128 |
+
word (unicode): input word as Unicode IPA string
|
| 129 |
+
normalize (bool): whether the word should be pre-normalized
|
| 130 |
+
|
| 131 |
+
Returns:
|
| 132 |
+
unicode: longest single-segment prefix of `word` in database
|
| 133 |
+
"""
|
| 134 |
+
if normalize:
|
| 135 |
+
word = FeatureTable.normalize(word)
|
| 136 |
+
last_found_length = 0
|
| 137 |
+
node = self.seg_trie
|
| 138 |
+
for pos in range(len(word) + 1):
|
| 139 |
+
if pos == len(word) or word[pos] not in node:
|
| 140 |
+
return word[:last_found_length]
|
| 141 |
+
node = node[word[pos]]
|
| 142 |
+
if self.TRIE_LEAF_MARKER in node:
|
| 143 |
+
last_found_length = pos + 1
|
| 144 |
+
return ''
|
| 145 |
+
|
| 146 |
+
def ipa_segs(self, word: str, normalize: bool=True) -> list[str]:
|
| 147 |
+
"""Returns a list of segments from a word
|
| 148 |
+
|
| 149 |
+
Args:
|
| 150 |
+
word (unicode): input word as Unicode IPA string
|
| 151 |
+
normalize (bool): whether to pre-normalize the word
|
| 152 |
+
|
| 153 |
+
Returns:
|
| 154 |
+
list: list of strings corresponding to segments found in `word`
|
| 155 |
+
"""
|
| 156 |
+
if normalize:
|
| 157 |
+
word = FeatureTable.normalize(word)
|
| 158 |
+
return self._segs(word, include_invalid=False, normalize=normalize)
|
| 159 |
+
|
| 160 |
+
def validate_word(self, word: str, normalize: bool=True):
|
| 161 |
+
"""Returns True if `word` consists exhaustively of valid IPA segments
|
| 162 |
+
|
| 163 |
+
Args:
|
| 164 |
+
word (unicode): input word as Unicode IPA string
|
| 165 |
+
normalize (bool): whether to pre-normalize the word
|
| 166 |
+
|
| 167 |
+
Returns:
|
| 168 |
+
bool: True if `word` can be divided exhaustively into IPA segments
|
| 169 |
+
that exist in the database
|
| 170 |
+
|
| 171 |
+
"""
|
| 172 |
+
return not self._segs(word, include_valid=False, include_invalid=True, normalize=normalize)
|
| 173 |
+
|
| 174 |
+
def word_fts(self, word: str, normalize: bool=True):
|
| 175 |
+
"""Return a list of Segment objects corresponding to the segments in
|
| 176 |
+
word.
|
| 177 |
+
|
| 178 |
+
Args:
|
| 179 |
+
word (unicode): word consisting of IPA segments
|
| 180 |
+
normalize (bool): whether to pre-normalize the word
|
| 181 |
+
|
| 182 |
+
Returns:
|
| 183 |
+
list: list of Segment objects corresponding to word
|
| 184 |
+
"""
|
| 185 |
+
return [self.fts(ipa, False) for ipa in self.ipa_segs(word, normalize)]
|
| 186 |
+
|
| 187 |
+
def word_array(self, ft_names: list[str], word: str, normalize: bool=True) -> numpy.ndarray:
|
| 188 |
+
"""Return a ndarray of features namd in ft_name for the segments in word
|
| 189 |
+
|
| 190 |
+
Args:
|
| 191 |
+
ft_names (list): strings naming subset of features in self.names
|
| 192 |
+
word (unicode): word to be analyzed
|
| 193 |
+
normalize (bool): whether to pre-normalize the word
|
| 194 |
+
|
| 195 |
+
Returns:
|
| 196 |
+
ndarray: segments in rows, features in columns as [-1, 0, 1]
|
| 197 |
+
"""
|
| 198 |
+
return numpy.array([s.numeric(ft_names) for s in self.word_fts(word, normalize)])
|
| 199 |
+
|
| 200 |
+
def bag_of_features(self, word: str, normalize: bool=True) -> numpy.ndarray:
|
| 201 |
+
"""Return a vector in which each dimension is the number of times a feature-value pair occurs in the word
|
| 202 |
+
|
| 203 |
+
Args:
|
| 204 |
+
word (unicode): word consisting of IPA segments
|
| 205 |
+
normalize (bool): whether to pre-normalize the word
|
| 206 |
+
|
| 207 |
+
Returns:
|
| 208 |
+
array: array of integers corresponding to a bag of feature-value pair counts
|
| 209 |
+
"""
|
| 210 |
+
# we changed here !!
|
| 211 |
+
# word_features = self.word_fts(word: str, normalize: bool=True)
|
| 212 |
+
word_features = self.word_fts(word, normalize=True)
|
| 213 |
+
features = [v + f for f in self.names for v in ['+', '0', '-']]
|
| 214 |
+
bag = collections.OrderedDict()
|
| 215 |
+
for f in features:
|
| 216 |
+
bag[f] = 0
|
| 217 |
+
vdict = {-1: '-', 0: '0', 1: '+'}
|
| 218 |
+
for w in word_features:
|
| 219 |
+
for (f, v) in w.items():
|
| 220 |
+
bag[vdict[v] + f] += 1
|
| 221 |
+
return numpy.array(list(bag.values()))
|
| 222 |
+
|
| 223 |
+
def seg_known(self, segment: str, normalize: bool=True) -> bool:
|
| 224 |
+
"""Return True if `segment` is in segment <=> features database
|
| 225 |
+
|
| 226 |
+
Args:
|
| 227 |
+
segment (unicode): consonant or vowel
|
| 228 |
+
normalize (bool): whether to pre-normalize the segment
|
| 229 |
+
|
| 230 |
+
Returns:
|
| 231 |
+
bool: True, if `segment` is in the database
|
| 232 |
+
"""
|
| 233 |
+
if normalize:
|
| 234 |
+
segment = FeatureTable.normalize(segment)
|
| 235 |
+
return segment in self.seg_dict
|
| 236 |
+
|
| 237 |
+
def segs_safe(self, word: str, normalize: bool=True):
|
| 238 |
+
"""Return a list of segments (as strings) from a word
|
| 239 |
+
|
| 240 |
+
Characters that are not valid segments are included in the list as
|
| 241 |
+
individual characters.
|
| 242 |
+
|
| 243 |
+
Args:
|
| 244 |
+
word (unicode): word as an IPA string
|
| 245 |
+
normalize (bool): whether to pre-normalize the word
|
| 246 |
+
|
| 247 |
+
Returns:
|
| 248 |
+
list: list of Unicode IPA strings corresponding to segments in
|
| 249 |
+
`word`
|
| 250 |
+
"""
|
| 251 |
+
if normalize:
|
| 252 |
+
word = FeatureTable.normalize(word)
|
| 253 |
+
return self._segs(word, include_invalid=True, normalize=normalize)
|
| 254 |
+
|
| 255 |
+
def _segs(self, word: str, *, include_valid: bool=True, include_invalid: bool, normalize: bool=True) -> list[str]:
|
| 256 |
+
if normalize:
|
| 257 |
+
word = FeatureTable.normalize(word)
|
| 258 |
+
segs = []
|
| 259 |
+
while word:
|
| 260 |
+
m = self.longest_one_seg_prefix(word, False)
|
| 261 |
+
if m:
|
| 262 |
+
if include_valid:
|
| 263 |
+
segs.append(m)
|
| 264 |
+
word = word[len(m):]
|
| 265 |
+
else:
|
| 266 |
+
if include_invalid:
|
| 267 |
+
segs.append(word[0])
|
| 268 |
+
word = word[1:]
|
| 269 |
+
return segs
|
| 270 |
+
|
| 271 |
+
def filter_segs(self, segs: list[str], normalize: bool=True) -> list[str]:
|
| 272 |
+
"""Given list of strings, return only those which are valid segments
|
| 273 |
+
|
| 274 |
+
Args:
|
| 275 |
+
segs (list): list of IPA Unicode strings
|
| 276 |
+
normalize (bool): whether to pre-normalize the segments
|
| 277 |
+
|
| 278 |
+
Return:
|
| 279 |
+
list: list of IPA Unicode strings identical to `segs` but with
|
| 280 |
+
invalid segments filtered out
|
| 281 |
+
"""
|
| 282 |
+
return list(filter(lambda seg: self.seg_known(seg, normalize), segs))
|
| 283 |
+
|
| 284 |
+
def filter_string(self, word: str, normalize: bool=True) -> str:
|
| 285 |
+
"""Return a string like the input but containing only legal IPA segments
|
| 286 |
+
|
| 287 |
+
Args:
|
| 288 |
+
word (unicode): input string to be filtered
|
| 289 |
+
normalize (bool): whether to pre-normalize the word (and return a normalized string)
|
| 290 |
+
|
| 291 |
+
Returns:
|
| 292 |
+
unicode: string identical to `word` but with invalid IPA segments
|
| 293 |
+
absent
|
| 294 |
+
|
| 295 |
+
"""
|
| 296 |
+
return ''.join(self.ipa_segs(word, normalize))
|
| 297 |
+
|
| 298 |
+
def fts_intersection(self, segs: list[str], normalize: bool=True) -> Segment:
|
| 299 |
+
"""Return a Segment object containing the features shared by all segments
|
| 300 |
+
|
| 301 |
+
Args:
|
| 302 |
+
segs (list): IPA segments
|
| 303 |
+
normalize (bool): whether to pre-normalize the segments
|
| 304 |
+
|
| 305 |
+
Returns:
|
| 306 |
+
Segment: the features shared by all segments in segs
|
| 307 |
+
"""
|
| 308 |
+
return reduce(lambda a, b: a & b,
|
| 309 |
+
[self.fts(s, normalize) for s in self.filter_segs(segs, normalize)])
|
| 310 |
+
|
| 311 |
+
def fts_match_all(self, fts: dict[str, int], inv: list[str], normalize: bool=True) -> bool:
|
| 312 |
+
"""Return `True` if all segments in `inv` matches the features in fts
|
| 313 |
+
|
| 314 |
+
Args:
|
| 315 |
+
fts (dict): a dictionary of features
|
| 316 |
+
inv (list): a collection of IPA segments represented as Unicode
|
| 317 |
+
strings
|
| 318 |
+
normalize (bool): whether to pre-normalize the segments
|
| 319 |
+
|
| 320 |
+
Returns:
|
| 321 |
+
bool: `True` if all segments in `inv` match the features in `fts`
|
| 322 |
+
"""
|
| 323 |
+
return all([self.fts(s, normalize) >= fts for s in inv])
|
| 324 |
+
|
| 325 |
+
def fts_match_any(self, fts: dict[str, int], inv: list[str], normalize: bool=True) -> bool:
|
| 326 |
+
"""Return `True` if any segments in `inv` matches the features in fts
|
| 327 |
+
|
| 328 |
+
Args:
|
| 329 |
+
fts (dict): a dictionary of features
|
| 330 |
+
inv (list): a collection of IPA segments represented as Unicode
|
| 331 |
+
strings
|
| 332 |
+
normalize (bool): whether to pre-normalize the segments
|
| 333 |
+
|
| 334 |
+
Returns:
|
| 335 |
+
bool: `True` if any segments in `inv` matches the features in `fts`
|
| 336 |
+
"""
|
| 337 |
+
return any([self.fts(s, normalize) >= fts for s in inv])
|
| 338 |
+
|
| 339 |
+
def fts_contrast(self, fs: dict[str, int], ft_name: str, inv: list[str], normalize: bool=True) -> bool:
|
| 340 |
+
"""Return `True` if there is a segment in `inv` that contrasts in feature
|
| 341 |
+
`ft_name`.
|
| 342 |
+
|
| 343 |
+
Args:
|
| 344 |
+
fs (dict): feature specifications used to filter `inv`.
|
| 345 |
+
ft_name (str): name of the feature where contrast must be present.
|
| 346 |
+
inv (list): collection of segments represented as Unicode strings.
|
| 347 |
+
normalize (bool): whether to pre-normalize the segments
|
| 348 |
+
|
| 349 |
+
Returns:
|
| 350 |
+
bool: `True` if two segments in `inv` are identical in features except
|
| 351 |
+
for feature `ft_name`
|
| 352 |
+
"""
|
| 353 |
+
inv_segs = filter(lambda x: x >= fs, map(lambda seg: self.fts(seg, normalize), inv))
|
| 354 |
+
for a in inv_segs:
|
| 355 |
+
for b in inv_segs:
|
| 356 |
+
if a != b:
|
| 357 |
+
if a.differing_specs(b) == [ft_name]:
|
| 358 |
+
return True
|
| 359 |
+
return False
|
| 360 |
+
|
| 361 |
+
def fts_count(self, fts: dict[str, int], inv: list[str], normalize: bool=True) -> int:
|
| 362 |
+
"""Return the count of segments in an inventory matching a given
|
| 363 |
+
feature mask.
|
| 364 |
+
|
| 365 |
+
Args:
|
| 366 |
+
fts (dict): feature mask given as a set of (value, feature) tuples
|
| 367 |
+
inv (list): inventory of segments (as Unicode IPA strings)
|
| 368 |
+
normalize (bool): whether to pre-normalize the segments
|
| 369 |
+
|
| 370 |
+
Returns:
|
| 371 |
+
int: number of segments in `inv` that match feature mask `fts`
|
| 372 |
+
"""
|
| 373 |
+
return len(list(filter(lambda s: self.fts(s, normalize) >= fts, inv)))
|
| 374 |
+
|
| 375 |
+
def match_pattern(self, pat: list[str], word: str, normalize: bool=True) -> list[dict[str, int]]:
|
| 376 |
+
"""Implements fixed-width pattern matching.
|
| 377 |
+
|
| 378 |
+
Matches just in case pattern is the same length (in segments) as the
|
| 379 |
+
word and each of the segments in the pattern is a featural subset of the
|
| 380 |
+
corresponding segment in the word. Matches return the corresponding list
|
| 381 |
+
of feature sets; failed matches return None.
|
| 382 |
+
|
| 383 |
+
Args:
|
| 384 |
+
pat (list): pattern consisting of a sequence of feature dicts
|
| 385 |
+
word (unicode): a Unicode IPA string consisting of zero or more
|
| 386 |
+
segments
|
| 387 |
+
normalize (bool): whether to pre-normalize the word
|
| 388 |
+
|
| 389 |
+
Returns:
|
| 390 |
+
list: corresponding list of feature dicts or, if there is no match,
|
| 391 |
+
None
|
| 392 |
+
"""
|
| 393 |
+
segs = self.word_fts(word, normalize)
|
| 394 |
+
if len(pat) != len(segs):
|
| 395 |
+
return None
|
| 396 |
+
else:
|
| 397 |
+
if all([s >= p for (s, p) in zip(segs, pat)]):
|
| 398 |
+
return segs
|
| 399 |
+
|
| 400 |
+
def match_pattern_seq(self, pat, const, normalize=True):
|
| 401 |
+
"""Implements limited pattern matching. Matches just in case pattern is
|
| 402 |
+
the same length (in segments) as the constituent and each of the
|
| 403 |
+
segments in the pattern is a featural subset of the corresponding
|
| 404 |
+
segment in the word.
|
| 405 |
+
|
| 406 |
+
Args:
|
| 407 |
+
pat (list): pattern consisting of a list of feature dicts, e.g.
|
| 408 |
+
[{'voi': 1}]
|
| 409 |
+
const (list): a sequence of Unicode IPA strings consisting of zero
|
| 410 |
+
or more segments.
|
| 411 |
+
normalize (bool): whether to pre-normalize the segments
|
| 412 |
+
|
| 413 |
+
Returns:
|
| 414 |
+
bool: `True` if `const` matches `pat`
|
| 415 |
+
"""
|
| 416 |
+
segs = [self.fts(s, normalize) for s in const]
|
| 417 |
+
if len(pat) != len(segs):
|
| 418 |
+
return False
|
| 419 |
+
else:
|
| 420 |
+
return all([s >= p for (s, p) in zip(segs, pat)])
|
| 421 |
+
|
| 422 |
+
def all_segs_matching_fts(self, ft_mask):
|
| 423 |
+
"""Return segments matching a feature mask, a dict of features
|
| 424 |
+
|
| 425 |
+
Args:
|
| 426 |
+
ft_mask (list): feature mask dict, e.g. {'voi': -1, 'cont': 1}.
|
| 427 |
+
|
| 428 |
+
Returns:
|
| 429 |
+
list: segments matching `ft_mask`, sorted in reverse order by length
|
| 430 |
+
"""
|
| 431 |
+
matching_segs = [ipa for (ipa, fts) in self.segments if fts >= ft_mask]
|
| 432 |
+
return sorted(matching_segs, key=lambda x: len(x), reverse=True)
|
| 433 |
+
|
| 434 |
+
def compile_regex_from_str(self, pat):
|
| 435 |
+
"""Given a string describing features masks for a sequence of segments,
|
| 436 |
+
return a compiled regex matching the corresponding strings.
|
| 437 |
+
|
| 438 |
+
Args:
|
| 439 |
+
pat (str): feature masks, each enclosed in square brackets, in
|
| 440 |
+
which the features are delimited by any standard delimiter.
|
| 441 |
+
|
| 442 |
+
Returns:
|
| 443 |
+
Pattern: regular expression pattern equivalent to `pat`
|
| 444 |
+
"""
|
| 445 |
+
s2n = {'-': -1, '0': 0, '+': 1}
|
| 446 |
+
seg_res = []
|
| 447 |
+
for mat in re.findall(r'\[[^]]+\]+', pat):
|
| 448 |
+
ft_mask = {k: s2n[v] for (v, k) in re.findall(r'([+-])(\w+)', mat)}
|
| 449 |
+
segs = self.all_segs_matching_fts(ft_mask)
|
| 450 |
+
seg_res.append('({})'.format('|'.join(segs)))
|
| 451 |
+
regexp = ''.join(seg_res)
|
| 452 |
+
return re.compile(regexp)
|
| 453 |
+
|
| 454 |
+
def segment_to_vector(self, seg, normalize=True):
|
| 455 |
+
"""Given a Unicode IPA segment, return a list of feature specificiations
|
| 456 |
+
in canonical order.
|
| 457 |
+
|
| 458 |
+
Args:
|
| 459 |
+
seg (unicode): IPA consonant or vowel
|
| 460 |
+
normalize: whether to pre-normalize the segment
|
| 461 |
+
|
| 462 |
+
Returns:
|
| 463 |
+
list: feature specifications ('+'/'-'/'0') in the order from
|
| 464 |
+
`FeatureTable.names`
|
| 465 |
+
"""
|
| 466 |
+
return self.fts(seg, normalize).strings()
|
| 467 |
+
|
| 468 |
+
def standardize_tones(self, word, nonstandard_tones=['¹','²','³','⁴','⁵']):
|
| 469 |
+
standard_tones = ['˩', '˨', '˧', '˦', '˥']
|
| 470 |
+
tone_map = dict(zip(nonstandard_tones, standard_tones))
|
| 471 |
+
standardized_word = ''.join(tone_map.get(char, char) for char in word)
|
| 472 |
+
return standardized_word
|
| 473 |
+
|
| 474 |
+
|
| 475 |
+
def word_to_vector_list(self, word, numeric=False, xsampa=False, nonstandard_tones=['¹','²','³','⁴','⁵'], normalize=True):
|
| 476 |
+
"""Return a list of feature vectors, given a Unicode IPA word.
|
| 477 |
+
|
| 478 |
+
Args:
|
| 479 |
+
word (unicode): string in IPA (or X-SAMPA, provided `xsampa` is True)
|
| 480 |
+
numeric (bool): if True, return features as numeric values instead
|
| 481 |
+
of strings
|
| 482 |
+
xsampa (bool): whether the word is in X-SAMPA instead of IPA
|
| 483 |
+
normalize: whether to pre-normalize the word (applies to IPA only)
|
| 484 |
+
nonstandard_tones (list): list of 5 nonstandard tones to be conveted
|
| 485 |
+
to IPA tone markers.
|
| 486 |
+
The order and numbering of the tones can be changed to reflect data.
|
| 487 |
+
Returns:
|
| 488 |
+
list: a list of lists of '+'/'-'/'0' or 1/-1/0
|
| 489 |
+
"""
|
| 490 |
+
if xsampa:
|
| 491 |
+
word = self.xsampa.convert(word)
|
| 492 |
+
if nonstandard_tones:
|
| 493 |
+
word=self.standardize_tones(word,nonstandard_tones)
|
| 494 |
+
segs = self.word_fts(word, normalize or xsampa)
|
| 495 |
+
|
| 496 |
+
if numeric:
|
| 497 |
+
tensor = [x.numeric() for x in segs]
|
| 498 |
+
else:
|
| 499 |
+
tensor = [x.strings() for x in segs]
|
| 500 |
+
return tensor
|
| 501 |
+
|
| 502 |
+
def _compare_vectors(self,vector1, vector2):
|
| 503 |
+
"""Compare two feature vectors digit by digit.
|
| 504 |
+
|
| 505 |
+
Args:
|
| 506 |
+
vector1 (list): First vector to compare.
|
| 507 |
+
vector2 (list): Second vector to compare.
|
| 508 |
+
|
| 509 |
+
Returns:
|
| 510 |
+
int: -1 if vector1 < vector2, 1 if vector1 > vector2, 0 if they are equal.
|
| 511 |
+
"""
|
| 512 |
+
for v1, v2 in zip(vector1, vector2):
|
| 513 |
+
if v1 < v2:
|
| 514 |
+
return -1
|
| 515 |
+
elif v1 > v2:
|
| 516 |
+
return 1
|
| 517 |
+
return 0 # Vectors are equal
|
| 518 |
+
|
| 519 |
+
def _binary_search(self, segment_list, target, fuzzy_search=False):
|
| 520 |
+
"""Binary search to find the segment matching the target vector.
|
| 521 |
+
|
| 522 |
+
Args:
|
| 523 |
+
segment_list (list): List of segments where each segment is a tuple (IPA, feature vector).
|
| 524 |
+
target (list): Target feature vector to search for.
|
| 525 |
+
fuzzy_search (bool): whether to search for the closest vector match if an exact match is not found.
|
| 526 |
+
If disabled and an exact match is not found, a None value is returned.
|
| 527 |
+
|
| 528 |
+
Returns:
|
| 529 |
+
str: The IPA segment matching the target vector, or None if not found.
|
| 530 |
+
"""
|
| 531 |
+
low, high = 0, len(segment_list) - 1
|
| 532 |
+
best_match_index = None
|
| 533 |
+
|
| 534 |
+
while low <= high:
|
| 535 |
+
mid = (low + high) // 2
|
| 536 |
+
word_vec = self.sorted_segments.segment_key(segment_list[mid])
|
| 537 |
+
comparison = self._compare_vectors(word_vec, target)
|
| 538 |
+
if comparison == 0:
|
| 539 |
+
best_match_index = mid
|
| 540 |
+
break
|
| 541 |
+
elif comparison < 0:
|
| 542 |
+
low = mid + 1
|
| 543 |
+
else:
|
| 544 |
+
high = mid - 1
|
| 545 |
+
|
| 546 |
+
if best_match_index is None and fuzzy_search:
|
| 547 |
+
# Used for fuzzy searching
|
| 548 |
+
best_match_index = mid
|
| 549 |
+
|
| 550 |
+
if best_match_index is not None:
|
| 551 |
+
# Check neighboring rows within the range of +-5
|
| 552 |
+
best_match = segment_list[best_match_index]
|
| 553 |
+
for offset in range(-9, 5):
|
| 554 |
+
neighbor_index = best_match_index + offset
|
| 555 |
+
if 0 <= neighbor_index < len(segment_list):
|
| 556 |
+
neighbor_segment = segment_list[neighbor_index]
|
| 557 |
+
if not self._compare_vectors(self.sorted_segments.segment_key(neighbor_segment),target):
|
| 558 |
+
# Check if the neighbor segment has a shorter name
|
| 559 |
+
if len(neighbor_segment[0]) < len(best_match[0]):
|
| 560 |
+
best_match = neighbor_segment
|
| 561 |
+
return best_match[0]
|
| 562 |
+
|
| 563 |
+
return None
|
| 564 |
+
|
| 565 |
+
def vector_list_to_word(self, tensor, xsampa=False,fuzzy_search=False):
|
| 566 |
+
"""Return a Unicode IPA word, given a list of feature vectors.
|
| 567 |
+
|
| 568 |
+
Args:
|
| 569 |
+
tensor (list): a list of lists of '+'/'-'/'0' or 1/-1/0
|
| 570 |
+
xsampa (bool): whether to return the word in X-SAMPA instead of IPA
|
| 571 |
+
fuzzy_search (bool): whether to search for the closest vector match if an exact match is not found.
|
| 572 |
+
If disabled and an exact match is not found, a `ValueError` is raised.
|
| 573 |
+
Returns:
|
| 574 |
+
unicode: string in IPA (or X-SAMPA, provided `xsampa` is True)
|
| 575 |
+
"""
|
| 576 |
+
|
| 577 |
+
|
| 578 |
+
|
| 579 |
+
word = ""
|
| 580 |
+
for vector in tensor:
|
| 581 |
+
match = self._binary_search(self.sorted_segments.segments, vector, fuzzy_search)
|
| 582 |
+
if match:
|
| 583 |
+
word += match
|
| 584 |
+
else:
|
| 585 |
+
raise ValueError(f"No matching segment found for vector: {vector}")
|
| 586 |
+
if xsampa:
|
| 587 |
+
word = self.xsampa.convert(word)
|
| 588 |
+
|
| 589 |
+
return word
|
| 590 |
+
|
panphon/permissive.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import absolute_import, print_function, unicode_literals
|
| 2 |
+
|
| 3 |
+
import codecs
|
| 4 |
+
import copy
|
| 5 |
+
import os.path
|
| 6 |
+
|
| 7 |
+
import pkg_resources
|
| 8 |
+
import yaml
|
| 9 |
+
|
| 10 |
+
import regex as re
|
| 11 |
+
import unicodecsv as csv
|
| 12 |
+
|
| 13 |
+
from . import _panphon, xsampa
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def flip(s):
|
| 17 |
+
return [(b, a) for (a, b) in s]
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def update_ft_set(seg, dia):
|
| 21 |
+
seg = dict(flip(seg))
|
| 22 |
+
seg.update(dia)
|
| 23 |
+
return flip(set(seg.items()))
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class PermissiveFeatureTable(_panphon.FeatureTable):
|
| 27 |
+
"""Encapsulate the segment <=> feature vector mapping implied by the files
|
| 28 |
+
data/ipa_all.csv and diacritic_definitions.yml. Uses a more permissive
|
| 29 |
+
algorithm for identifying base+diacritic combinations. To avoid a
|
| 30 |
+
combinatorial explosion, it never generates all of the base-diacritic-
|
| 31 |
+
modifier combinations, meaning it cannot easily make statements about the
|
| 32 |
+
whole set of segments."""
|
| 33 |
+
|
| 34 |
+
def __init__(self,
|
| 35 |
+
feature_set='spe+',
|
| 36 |
+
feature_model='strict',
|
| 37 |
+
ipa_bases=os.path.join('data', 'ipa_bases.csv'),
|
| 38 |
+
dias=os.path.join('data', 'diacritic_definitions.yml'),
|
| 39 |
+
):
|
| 40 |
+
"""Construct a PermissiveFeatureTable object
|
| 41 |
+
|
| 42 |
+
Args:
|
| 43 |
+
feature_set (str): feature system (for API compatibility)
|
| 44 |
+
feature_model (str): feature parsing model (for API compatibility)
|
| 45 |
+
ipa_bases (str): path from panphon root to CSV file definining
|
| 46 |
+
features of bases (unmodified consonants and
|
| 47 |
+
vowels)
|
| 48 |
+
dias (str): path from panphon root to YAML file containing rules for
|
| 49 |
+
diacritics and modifiers
|
| 50 |
+
"""
|
| 51 |
+
dias = pkg_resources.resource_filename(__name__, dias)
|
| 52 |
+
self.bases, self.names = self._read_ipa_bases(ipa_bases)
|
| 53 |
+
self.prefix_dias, self.postfix_dias = self._read_dias(dias)
|
| 54 |
+
self.pre_regex, self.post_regex, self.seg_regex = self._compile_seg_regexes(self.bases, self.prefix_dias, self.postfix_dias)
|
| 55 |
+
self.xsampa = xsampa.XSampa()
|
| 56 |
+
self.weights = self._read_weights()
|
| 57 |
+
|
| 58 |
+
def _read_ipa_bases(self, fn):
|
| 59 |
+
fn = pkg_resources.resource_filename(__name__, fn)
|
| 60 |
+
with open(fn, 'rb') as f:
|
| 61 |
+
reader = csv.reader(f, encoding='utf-8', delimiter=str(','))
|
| 62 |
+
names = next(reader)[1:]
|
| 63 |
+
bases = {}
|
| 64 |
+
for row in reader:
|
| 65 |
+
seg, vals = row[0], row[1:]
|
| 66 |
+
bases[seg] = (set(zip(vals, names)))
|
| 67 |
+
return bases, names
|
| 68 |
+
|
| 69 |
+
def _read_dias(self, fn):
|
| 70 |
+
prefix, postfix = {}, {}
|
| 71 |
+
with codecs.open(fn, 'r', 'utf-8') as f:
|
| 72 |
+
defs = yaml.load(f.read(), Loader=yaml.FullLoader)
|
| 73 |
+
for dia in defs['diacritics']:
|
| 74 |
+
if dia['position'] == 'pre':
|
| 75 |
+
prefix[dia['marker']] = dia['content']
|
| 76 |
+
else:
|
| 77 |
+
postfix[dia['marker']] = dia['content']
|
| 78 |
+
return prefix, postfix
|
| 79 |
+
|
| 80 |
+
def _compile_seg_regexes(self, bases, prefix, postfix):
|
| 81 |
+
pre_jnd = '|'.join(prefix.keys())
|
| 82 |
+
post_jnd = '|'.join(postfix.keys())
|
| 83 |
+
bases_jnd = '|'.join(bases.keys())
|
| 84 |
+
pre_re = '({})'.format(pre_jnd)
|
| 85 |
+
post_re = '({})'.format(post_jnd)
|
| 86 |
+
seg_re = '(?P<all>(?P<pre>({})*)(?P<base>{})(?P<post>({})*))'.format(pre_jnd, bases_jnd, post_jnd)
|
| 87 |
+
return re.compile(pre_re), re.compile(post_re), re.compile(seg_re)
|
| 88 |
+
|
| 89 |
+
def _build_seg_regex(self):
|
| 90 |
+
return self.seg_regex
|
| 91 |
+
|
| 92 |
+
def _read_weights(self, filename=os.path.join('data', 'feature_weights.csv')):
|
| 93 |
+
filename = pkg_resources.resource_filename(
|
| 94 |
+
__name__, filename)
|
| 95 |
+
with open(filename, 'rb') as f:
|
| 96 |
+
reader = csv.reader(f, encoding='utf-8')
|
| 97 |
+
next(reader)
|
| 98 |
+
weights = [float(x) for x in next(reader)]
|
| 99 |
+
return weights
|
| 100 |
+
|
| 101 |
+
def fts(self, segment):
|
| 102 |
+
"""Return features corresponding to segment as list of (value,
|
| 103 |
+
feature) tuples
|
| 104 |
+
|
| 105 |
+
Args:
|
| 106 |
+
segment (unicode): segment for which features are to be returned as
|
| 107 |
+
Unicode string
|
| 108 |
+
|
| 109 |
+
Returns:
|
| 110 |
+
list: None if `segment` cannot be parsed; otherwise, a list of the
|
| 111 |
+
features of `segment` as (value, feature) pairs
|
| 112 |
+
"""
|
| 113 |
+
match = self.seg_regex.match(segment)
|
| 114 |
+
if match:
|
| 115 |
+
pre, base, post = match.group('pre'), match.group('base'), match.group('post')
|
| 116 |
+
seg = copy.deepcopy(self.bases[base])
|
| 117 |
+
for m in reversed(pre):
|
| 118 |
+
seg = update_ft_set(seg, self.prefix_dias[m])
|
| 119 |
+
for m in post:
|
| 120 |
+
seg = update_ft_set(seg, self.postfix_dias[m])
|
| 121 |
+
return set(seg)
|
| 122 |
+
else:
|
| 123 |
+
return None
|
| 124 |
+
|
| 125 |
+
def fts_match(self, fts_mask, segment):
|
| 126 |
+
"""Evaluates whether a set of features 'match' a segment (are a subset
|
| 127 |
+
of that segment's features)
|
| 128 |
+
|
| 129 |
+
Args:
|
| 130 |
+
fts_mask (list): list of (value, feature) tuples
|
| 131 |
+
segment (unicode): IPA string corresponding to segment (consonant or
|
| 132 |
+
vowel)
|
| 133 |
+
Returns:
|
| 134 |
+
bool: None if `segment` cannot be parsed; True if the feature values
|
| 135 |
+
of `fts_mask` are a subset of those for `segment`
|
| 136 |
+
"""
|
| 137 |
+
fts_seg = self.fts(segment)
|
| 138 |
+
if fts_seg:
|
| 139 |
+
fts_mask = set(fts_mask)
|
| 140 |
+
return fts_mask <= fts_seg
|
| 141 |
+
else:
|
| 142 |
+
return None
|
| 143 |
+
|
| 144 |
+
def longest_one_seg_prefix(self, word):
|
| 145 |
+
"""Return longest IPA Unicode prefix of `word`
|
| 146 |
+
|
| 147 |
+
Args:
|
| 148 |
+
word (unicode): word as IPA string
|
| 149 |
+
|
| 150 |
+
Returns:
|
| 151 |
+
unicode: longest single-segment prefix of `word`
|
| 152 |
+
"""
|
| 153 |
+
match = self.seg_regex.match(word)
|
| 154 |
+
if match:
|
| 155 |
+
return match.group(0)
|
| 156 |
+
else:
|
| 157 |
+
return ''
|
| 158 |
+
|
| 159 |
+
def seg_known(self, segment):
|
| 160 |
+
"""Return True if the segment is valid
|
| 161 |
+
|
| 162 |
+
Args:
|
| 163 |
+
segment (unicode): a string which may or may not be a valid segment
|
| 164 |
+
|
| 165 |
+
Returns:
|
| 166 |
+
bool: True if segment can be parsed given the database of bases and
|
| 167 |
+
diacritics
|
| 168 |
+
"""
|
| 169 |
+
if self.seg_regex.match(segment):
|
| 170 |
+
return True
|
| 171 |
+
else:
|
| 172 |
+
return False
|
| 173 |
+
|
| 174 |
+
def filter_segs(self, segs):
|
| 175 |
+
"""Given list of strings, return only those which are valid segments.
|
| 176 |
+
|
| 177 |
+
Args:
|
| 178 |
+
segs (list): list of unicode values
|
| 179 |
+
|
| 180 |
+
Returns:
|
| 181 |
+
list: values in `segs` that are valid segments (according to the
|
| 182 |
+
definititions of bases and diacritics/modifiers known to the
|
| 183 |
+
object
|
| 184 |
+
"""
|
| 185 |
+
def whole_seg(seg):
|
| 186 |
+
m = self.seg_regex.match(seg)
|
| 187 |
+
if m and m.group(0) == seg:
|
| 188 |
+
return True
|
| 189 |
+
else:
|
| 190 |
+
return False
|
| 191 |
+
return list(filter(whole_seg, segs))
|
| 192 |
+
|
| 193 |
+
def segment_word_segments(self, word):
|
| 194 |
+
def n2s(s):
|
| 195 |
+
if s is None:
|
| 196 |
+
return ''
|
| 197 |
+
else:
|
| 198 |
+
return s
|
| 199 |
+
return ((n2s(m.group('pre')), n2s(m.group('base')), n2s(m.group('post')))
|
| 200 |
+
for m in self.seg_regex.finditer(word))
|
| 201 |
+
|
| 202 |
+
@property
|
| 203 |
+
def all_segs_matching_fts(self):
|
| 204 |
+
raise AttributeError("'PermissiveFeatureTable' object has no attribute 'all_segs_matching_fts'")
|
panphon/segment.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from collections.abc import Iterator, Iterable, Mapping
|
| 6 |
+
from typing import TypeVar
|
| 7 |
+
import regex as re
|
| 8 |
+
|
| 9 |
+
T = TypeVar('T')
|
| 10 |
+
|
| 11 |
+
# class Segment(Mapping[str, int]):
|
| 12 |
+
class Segment(Mapping):
|
| 13 |
+
"""Constructs a `Segment` object that models a phonological segment as a vector of features.
|
| 14 |
+
|
| 15 |
+
:param names list[str]: An ordered list of feature names.
|
| 16 |
+
:param feature dict[str, int]: name-feature pairs for specified features.
|
| 17 |
+
:param ftstr str: A string, each /(+|0|-)\w+/ sequence of which is interpreted as a feature specification.
|
| 18 |
+
:param weights list[float]: An ordered list of feature weights/saliences.
|
| 19 |
+
"""
|
| 20 |
+
def __init__(self, names: list[str], features: dict[str, int]={}, ftstr: str='', weights: "list[float]"=[]):
|
| 21 |
+
self.n2s = {-1: '-', 0: '0', 1: '+'}
|
| 22 |
+
self.s2n = {k: v for (v, k) in self.n2s.items()}
|
| 23 |
+
self.names = names
|
| 24 |
+
"""Set a feature specification"""
|
| 25 |
+
self.data = {}
|
| 26 |
+
for name in names:
|
| 27 |
+
if name in features:
|
| 28 |
+
self.data[name] = features[name]
|
| 29 |
+
else:
|
| 30 |
+
self.data[name] = 0
|
| 31 |
+
for m in re.finditer(r'(\+|0|-)(\w+)', ftstr):
|
| 32 |
+
v, k = m.groups()
|
| 33 |
+
self.data[k] = self.s2n[v]
|
| 34 |
+
if weights:
|
| 35 |
+
self.weights = weights
|
| 36 |
+
else:
|
| 37 |
+
self.weights = [1 for _ in names]
|
| 38 |
+
|
| 39 |
+
def __len__(self):
|
| 40 |
+
return len(self._features)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def __getitem__(self, key: str) -> int:
|
| 44 |
+
"""Get a feature specification"""
|
| 45 |
+
return self.data[key]
|
| 46 |
+
|
| 47 |
+
def __setitem__(self, key: str, value: int):
|
| 48 |
+
"""Set a feature specification"""
|
| 49 |
+
if key in self.names:
|
| 50 |
+
self.data[key] = value
|
| 51 |
+
else:
|
| 52 |
+
raise KeyError('Unknown feature name.')
|
| 53 |
+
|
| 54 |
+
def __repr__(self) -> str:
|
| 55 |
+
"""Return a string representation of a feature vector"""
|
| 56 |
+
pairs = [(self.n2s[self.data[k]], k) for k in self.names]
|
| 57 |
+
fts = ', '.join(['{}{}'.format(*pair) for pair in pairs])
|
| 58 |
+
return '<Segment [{}]>'.format(fts)
|
| 59 |
+
|
| 60 |
+
def __iter__(self) -> Iterator[str]:
|
| 61 |
+
"""Return an iterator over the feature names"""
|
| 62 |
+
return iter(self.names)
|
| 63 |
+
|
| 64 |
+
def items(self) -> list[tuple[str, int]]:
|
| 65 |
+
"""Return a list of the features as (name, value) pairs
|
| 66 |
+
|
| 67 |
+
:return: List of features as (name, value) pairs
|
| 68 |
+
:rtype: list[tuple[str, int]]
|
| 69 |
+
"""
|
| 70 |
+
return [(k, self.data[k]) for k in self.names]
|
| 71 |
+
|
| 72 |
+
def iteritems(self) -> Iterator[tuple[str, int]]:
|
| 73 |
+
"""Return an iterator over the features as (name, value) pairs
|
| 74 |
+
|
| 75 |
+
:return: Iterator over features as (name, value) pairs
|
| 76 |
+
:rtype: Iterator[tuple[str, int]]
|
| 77 |
+
"""
|
| 78 |
+
return ((k, self.data[k]) for k in self.names)
|
| 79 |
+
|
| 80 |
+
def update(self, features: dict[str, int]):
|
| 81 |
+
"""Update the objects features to match `features`.
|
| 82 |
+
|
| 83 |
+
Args:
|
| 84 |
+
features (dict): dictionary containing the new feature values
|
| 85 |
+
"""
|
| 86 |
+
self.data.update(features)
|
| 87 |
+
|
| 88 |
+
def match(self, ft_mask: Segment) -> bool:
|
| 89 |
+
"""Determine whether `self`'s features are a superset of `features`'s
|
| 90 |
+
|
| 91 |
+
Args:
|
| 92 |
+
features (dict): (name, value) pairs
|
| 93 |
+
|
| 94 |
+
Returns:
|
| 95 |
+
(bool): True if superset relationship holds else False
|
| 96 |
+
"""
|
| 97 |
+
return all([self.data[k] == v for (k, v) in ft_mask.items()])
|
| 98 |
+
|
| 99 |
+
def __ge__(self, other: Segment) -> bool:
|
| 100 |
+
"""Determine whether `self`'s features are a superset of `other`'s"""
|
| 101 |
+
return self.match(other)
|
| 102 |
+
|
| 103 |
+
def intersection(self, other: Segment) -> Segment:
|
| 104 |
+
"""Return dict of features shared by `self` and `other`
|
| 105 |
+
|
| 106 |
+
Args:
|
| 107 |
+
other (Segment): object with feature specifications
|
| 108 |
+
|
| 109 |
+
Returns:
|
| 110 |
+
Segment: (name, value) pairs for each shared feature
|
| 111 |
+
"""
|
| 112 |
+
data = dict(set(self.items()) & set(other.items()))
|
| 113 |
+
names = list(filter(lambda a: a in data, self.names))
|
| 114 |
+
return Segment(names, data)
|
| 115 |
+
|
| 116 |
+
def __and__(self, other: Segment) -> Segment:
|
| 117 |
+
"""Return Segment of features shared by `self` and `other`"""
|
| 118 |
+
return self.intersection(other)
|
| 119 |
+
|
| 120 |
+
def numeric(self, names: list[str]=[]) -> list[int]:
|
| 121 |
+
if not names:
|
| 122 |
+
names = self.names
|
| 123 |
+
"""Return feature values as a list of integers"""
|
| 124 |
+
return [self.data[k] for k in names]
|
| 125 |
+
|
| 126 |
+
def strings(self, names: list[str]=[]) -> list[str]:
|
| 127 |
+
"""Return feature values as a list of strings"""
|
| 128 |
+
if not names:
|
| 129 |
+
names = self.names
|
| 130 |
+
return list(map(lambda x: self.n2s[x], self.numeric()))
|
| 131 |
+
|
| 132 |
+
def distance(self, other: Segment) -> int:
|
| 133 |
+
"""Compute a distance between `self` and `other`
|
| 134 |
+
|
| 135 |
+
Args:
|
| 136 |
+
other (Segment): object to compare with `self`
|
| 137 |
+
|
| 138 |
+
Returns:
|
| 139 |
+
int: the sum of the absolute value of the difference between each
|
| 140 |
+
of the feature values in `self` and `other`.
|
| 141 |
+
"""
|
| 142 |
+
return sum(abs(a - b) for (a, b) in zip(self.numeric(), other.numeric()))
|
| 143 |
+
|
| 144 |
+
def norm_distance(self, other: Segment) -> float:
|
| 145 |
+
"""Compute a distance, normalized by vector length
|
| 146 |
+
|
| 147 |
+
Args:
|
| 148 |
+
other (Segment): object to compare with `self`
|
| 149 |
+
|
| 150 |
+
Returns:
|
| 151 |
+
float: the sum of the absolute value of the difference between
|
| 152 |
+
each of the feature values in `self` and `other`, divided
|
| 153 |
+
by the number of features per vector.
|
| 154 |
+
"""
|
| 155 |
+
return self.distance(other) / len(self.names)
|
| 156 |
+
|
| 157 |
+
def __sub__(self, other: Segment) -> float:
|
| 158 |
+
"""Distance between segments, normalized by vector length"""
|
| 159 |
+
return self.norm_distance(other)
|
| 160 |
+
|
| 161 |
+
def hamming_distance(self, other) -> int:
|
| 162 |
+
"""Compute Hamming distance between feature vectors
|
| 163 |
+
|
| 164 |
+
Args:
|
| 165 |
+
other (Segment): object to compare with `self`
|
| 166 |
+
|
| 167 |
+
Returns:
|
| 168 |
+
int: the unnormalized Hamming distance between the two vectors.
|
| 169 |
+
"""
|
| 170 |
+
return sum(int(a != b) for (a, b) in zip(self.numeric(), other.numeric()))
|
| 171 |
+
|
| 172 |
+
def norm_hamming_distance(self, other: Segment) -> float:
|
| 173 |
+
"""Compute Hamming distance, normalized by vector length
|
| 174 |
+
|
| 175 |
+
Args:
|
| 176 |
+
other (Segment): object to compare with `self`
|
| 177 |
+
|
| 178 |
+
Returns:
|
| 179 |
+
int: the normalized Hamming distance between the two vectors.
|
| 180 |
+
"""
|
| 181 |
+
return self.hamming_distance(other) / len(self.names)
|
| 182 |
+
|
| 183 |
+
def weighted_distance(self, other: Segment) -> float:
|
| 184 |
+
"""Compute weighted distance
|
| 185 |
+
|
| 186 |
+
Args:
|
| 187 |
+
other (Segment): object to compare with `self`
|
| 188 |
+
|
| 189 |
+
Returns:
|
| 190 |
+
float: the weighted distance between the two vectors
|
| 191 |
+
"""
|
| 192 |
+
return sum([abs(a - b) * c for (a, b, c)
|
| 193 |
+
in zip(self.numeric(), other.numeric(), self.weights)])
|
| 194 |
+
|
| 195 |
+
def norm_weighted_distance(self, other: Segment) -> float:
|
| 196 |
+
"""Compute weighted distance, normalized by vector length
|
| 197 |
+
|
| 198 |
+
Args:
|
| 199 |
+
other (Segment): object to compare with `self`
|
| 200 |
+
|
| 201 |
+
Returns:
|
| 202 |
+
float: the weighted distance between the two vectors, normalized by
|
| 203 |
+
vector length.
|
| 204 |
+
"""
|
| 205 |
+
return self.weighted_distance(other) / sum(self.weights)
|
| 206 |
+
|
| 207 |
+
def specified(self) -> dict[str, int]:
|
| 208 |
+
"""Return dictionary of features that are specified '+' or '-' (1 or -1)
|
| 209 |
+
|
| 210 |
+
Returns:
|
| 211 |
+
dict: each feature in `self` for which the value is not 0
|
| 212 |
+
"""
|
| 213 |
+
return {k: v for (k, v) in self.data.items() if v != 0}
|
| 214 |
+
|
| 215 |
+
def differing_specs(self, other: Segment) -> list[str]:
|
| 216 |
+
"""Return a list of feature names that differ in their specified values
|
| 217 |
+
|
| 218 |
+
Args:
|
| 219 |
+
other (Segment): object to compare with `self`
|
| 220 |
+
|
| 221 |
+
Returns:
|
| 222 |
+
list: the names of the features that differ in the two vectors
|
| 223 |
+
"""
|
| 224 |
+
return [k for (k, v) in self.items() if other[k] != v]
|
panphon/sonority.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import print_function, absolute_import, unicode_literals
|
| 2 |
+
|
| 3 |
+
from . import _panphon
|
| 4 |
+
from . import permissive
|
| 5 |
+
|
| 6 |
+
from ._panphon import FeatureTable, fts
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class BoolTree(object):
|
| 10 |
+
"""Simple decision tree specialized for sonority classes"""
|
| 11 |
+
def __init__(self, test=None, t_node=None, f_node=None):
|
| 12 |
+
"""Construct a BoolTree object
|
| 13 |
+
|
| 14 |
+
Args:
|
| 15 |
+
test (bool): test for whether to traverse the true-node or the
|
| 16 |
+
false-node (`BoolTree.t_node` or `BoolTree.f_node`)
|
| 17 |
+
t_node (BoolTree/Int): node to follow if test is `True`
|
| 18 |
+
f_node (BoolTree/Int): node to follow if test is `False`
|
| 19 |
+
"""
|
| 20 |
+
self.test = test
|
| 21 |
+
self.t_node = t_node
|
| 22 |
+
self.f_node = f_node
|
| 23 |
+
|
| 24 |
+
def get_value(self):
|
| 25 |
+
if self.test:
|
| 26 |
+
if isinstance(self.t_node, BoolTree):
|
| 27 |
+
return self.t_node.get_value()
|
| 28 |
+
else:
|
| 29 |
+
return self.t_node
|
| 30 |
+
else:
|
| 31 |
+
if isinstance(self.f_node, BoolTree):
|
| 32 |
+
return self.f_node.get_value()
|
| 33 |
+
else:
|
| 34 |
+
return self.f_node
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class Sonority(object):
|
| 38 |
+
"""Determine the sonority of a segment"""
|
| 39 |
+
def __init__(self, feature_set='spe+', feature_model='strict'):
|
| 40 |
+
"""Construct a Sonority object
|
| 41 |
+
|
| 42 |
+
Args:
|
| 43 |
+
feature_set (str): features set to be used by `FeatureTable`
|
| 44 |
+
feature_model (str): 'strict' or 'permissive' feature model
|
| 45 |
+
"""
|
| 46 |
+
fm = {'strict': _panphon.FeatureTable,
|
| 47 |
+
'permissive': permissive.PermissiveFeatureTable}
|
| 48 |
+
self.fm = fm[feature_model](feature_set=feature_set)
|
| 49 |
+
|
| 50 |
+
def sonority_from_fts(self, seg):
|
| 51 |
+
"""Given a segment as features, returns the sonority on a scale of 1
|
| 52 |
+
to 9.
|
| 53 |
+
|
| 54 |
+
Args:
|
| 55 |
+
seg (list): collection of (value, feature) pairs representing
|
| 56 |
+
a segment (vowel or consonant)
|
| 57 |
+
|
| 58 |
+
Returns:
|
| 59 |
+
int: sonority of `seg` between 1 and 9
|
| 60 |
+
"""
|
| 61 |
+
|
| 62 |
+
def match(m):
|
| 63 |
+
return self.fm.match(fts(m), seg)
|
| 64 |
+
|
| 65 |
+
minusHi = BoolTree(match('-hi'), 9, 8)
|
| 66 |
+
minusNas = BoolTree(match('-nas'), 6, 5)
|
| 67 |
+
plusVoi1 = BoolTree(match('+voi'), 4, 3)
|
| 68 |
+
plusVoi2 = BoolTree(match('+voi'), 2, 1)
|
| 69 |
+
plusCont = BoolTree(match('+cont'), plusVoi1, plusVoi2)
|
| 70 |
+
plusSon = BoolTree(match('+son'), minusNas, plusCont)
|
| 71 |
+
minusCons = BoolTree(match('-cons'), 7, plusSon)
|
| 72 |
+
plusSyl = BoolTree(match('+syl'), minusHi, minusCons)
|
| 73 |
+
return plusSyl.get_value()
|
| 74 |
+
|
| 75 |
+
def sonority(self, seg):
|
| 76 |
+
"""Given a segment as a Unicode IPA string, returns the sonority on
|
| 77 |
+
a scale of 1 to 9.
|
| 78 |
+
|
| 79 |
+
Args:
|
| 80 |
+
seg (unicode): IPA consonant or vowel
|
| 81 |
+
|
| 82 |
+
Returns:
|
| 83 |
+
int: sonority of `seg` between 1 and 9
|
| 84 |
+
"""
|
| 85 |
+
return self.sonority_from_fts(self.fm.fts(seg))
|
panphon/xsampa.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import absolute_import, print_function, unicode_literals
|
| 2 |
+
|
| 3 |
+
import regex as re
|
| 4 |
+
import unicodecsv as csv
|
| 5 |
+
import os.path
|
| 6 |
+
import pkg_resources
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class XSampa(object):
|
| 10 |
+
def __init__(self, delimiter=' '):
|
| 11 |
+
self.delimiter = delimiter
|
| 12 |
+
self.xs_regex, self.xs2ipa = self.read_xsampa_table()
|
| 13 |
+
|
| 14 |
+
def read_xsampa_table(self):
|
| 15 |
+
filename = os.path.join('data', 'ipa-xsampa.csv')
|
| 16 |
+
filename = pkg_resources.resource_filename(__name__, filename)
|
| 17 |
+
with open(filename, 'rb') as f:
|
| 18 |
+
xs2ipa = {x[1]: x[0] for x in csv.reader(f, encoding='utf-8')}
|
| 19 |
+
xs = sorted(xs2ipa.keys(), key=len, reverse=True)
|
| 20 |
+
xs_regex = re.compile('|'.join(list(map(re.escape, xs))))
|
| 21 |
+
return xs_regex, xs2ipa
|
| 22 |
+
|
| 23 |
+
def convert(self, xsampa):
|
| 24 |
+
def seg2ipa(seg):
|
| 25 |
+
ipa = []
|
| 26 |
+
while seg:
|
| 27 |
+
match = self.xs_regex.match(seg)
|
| 28 |
+
if match:
|
| 29 |
+
ipa.append(self.xs2ipa[match.group(0)])
|
| 30 |
+
seg = seg[len(match.group(0)):]
|
| 31 |
+
else:
|
| 32 |
+
seg = seg[1:]
|
| 33 |
+
return ''.join(ipa)
|
| 34 |
+
ipasegs = list(map(seg2ipa, xsampa.split(self.delimiter)))
|
| 35 |
+
return ''.join(ipasegs)
|