MLSpeech commited on
Commit
787be4d
·
verified ·
1 Parent(s): e5112c9

Vendor patched panphon (py3.8-compatible import)

Browse files
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)