From 08f83ee9940bc5013cec6cf3aa029558c95ed61d Mon Sep 17 00:00:00 2001 From: cbelth Date: Tue, 4 Jun 2024 08:59:50 -0600 Subject: [PATCH 1/4] init PLP files --- algophon/models/PLP/.gitignore | 1 + algophon/models/PLP/__init__.py | 4 ++++ algophon/models/PLP/discrepancy.py | 4 ++++ algophon/models/PLP/grammar.py | 4 ++++ algophon/models/PLP/plp.py | 4 ++++ algophon/models/PLP/rule.py | 4 ++++ 6 files changed, 21 insertions(+) create mode 100644 algophon/models/PLP/.gitignore create mode 100644 algophon/models/PLP/__init__.py create mode 100644 algophon/models/PLP/discrepancy.py create mode 100644 algophon/models/PLP/grammar.py create mode 100644 algophon/models/PLP/plp.py create mode 100644 algophon/models/PLP/rule.py diff --git a/algophon/models/PLP/.gitignore b/algophon/models/PLP/.gitignore new file mode 100644 index 0000000..c18dd8d --- /dev/null +++ b/algophon/models/PLP/.gitignore @@ -0,0 +1 @@ +__pycache__/ diff --git a/algophon/models/PLP/__init__.py b/algophon/models/PLP/__init__.py new file mode 100644 index 0000000..6c2e26e --- /dev/null +++ b/algophon/models/PLP/__init__.py @@ -0,0 +1,4 @@ +from algophon.models.PLP.discrepancy import Discrepancy +from algophon.models.PLP.rule import Rule +from algophon.models.PLP.grammar import Grammar +from algophon.models.PLP.plp import PLP \ No newline at end of file diff --git a/algophon/models/PLP/discrepancy.py b/algophon/models/PLP/discrepancy.py new file mode 100644 index 0000000..3652ea8 --- /dev/null +++ b/algophon/models/PLP/discrepancy.py @@ -0,0 +1,4 @@ +class Discrepancy: + def __init__(self) -> object: + pass + # TODO \ No newline at end of file diff --git a/algophon/models/PLP/grammar.py b/algophon/models/PLP/grammar.py new file mode 100644 index 0000000..3f133f4 --- /dev/null +++ b/algophon/models/PLP/grammar.py @@ -0,0 +1,4 @@ +class Grammar: + def __init__(self) -> object: + pass + # TODO \ No newline at end of file diff --git a/algophon/models/PLP/plp.py b/algophon/models/PLP/plp.py new file mode 100644 index 0000000..876aea6 --- /dev/null +++ b/algophon/models/PLP/plp.py @@ -0,0 +1,4 @@ +class PLP: + def __init__(self) -> object: + pass + # TODO \ No newline at end of file diff --git a/algophon/models/PLP/rule.py b/algophon/models/PLP/rule.py new file mode 100644 index 0000000..e4f4ee2 --- /dev/null +++ b/algophon/models/PLP/rule.py @@ -0,0 +1,4 @@ +class Rule: + def __init__(self) -> object: + pass + # TODO \ No newline at end of file From 89c4aa8e8b91fc26da75f8b559df0ff519f6e4dc Mon Sep 17 00:00:00 2001 From: cbelth Date: Tue, 4 Jun 2024 09:36:03 -0600 Subject: [PATCH 2/4] make abstracr Rule --- algophon/models/D2L/__init__.py | 2 +- algophon/models/D2L/d2l.py | 8 ++-- algophon/models/D2L/rule.py | 64 +++-------------------------- algophon/models/__init__.py | 1 + algophon/models/rule.py | 73 +++++++++++++++++++++++++++++++++ tests/test_d2l.py | 58 +++++++++++++------------- 6 files changed, 113 insertions(+), 93 deletions(-) create mode 100644 algophon/models/rule.py diff --git a/algophon/models/D2L/__init__.py b/algophon/models/D2L/__init__.py index f5ca0ba..70bf893 100644 --- a/algophon/models/D2L/__init__.py +++ b/algophon/models/D2L/__init__.py @@ -1,4 +1,4 @@ from algophon.models.D2L.discrepancy import Discrepancy from algophon.models.D2L.tier import Tier -from algophon.models.D2L.rule import Rule +from algophon.models.D2L.rule import D2LRule from algophon.models.D2L.d2l import D2L \ No newline at end of file diff --git a/algophon/models/D2L/d2l.py b/algophon/models/D2L/d2l.py index 64fa57f..94c4910 100644 --- a/algophon/models/D2L/d2l.py +++ b/algophon/models/D2L/d2l.py @@ -4,7 +4,7 @@ from algophon import SegInv, SegStr, NatClass from algophon.symbols import UNDERSPECIFIED, BOUNDARIES -from algophon.models.D2L import Discrepancy, Rule, Tier +from algophon.models.D2L import Discrepancy, D2LRule, Tier from algophon.utils import tsp class D2L: @@ -191,7 +191,7 @@ def _train_setup(self, pairs: Iterable) -> set: return setup_pairs - def build_rule(self, pairs: set, delset: set=set(), tier=None, harmony: bool=True, discrepancy: Union[None, Discrepancy]=None) -> Rule: + def build_rule(self, pairs: set, delset: set=set(), tier=None, harmony: bool=True, discrepancy: Union[None, Discrepancy]=None) -> D2LRule: ''' Builds a rule recursively. @@ -208,13 +208,13 @@ def build_rule(self, pairs: set, delset: set=set(), tier=None, harmony: bool=Tru lctxts, rctxts = self._get_tier_adj_contexts(discrepancy=discrepancy, tier=tier) # compute ctxts # build left rule - left_rule = Rule(seginv=self.seginv, target=target, features=discrepancy.feature_diff, left_ctxts=lctxts, tier=tier, harmony=harmony) + left_rule = D2LRule(seginv=self.seginv, target=target, features=discrepancy.feature_diff, left_ctxts=lctxts, tier=tier, harmony=harmony) left_underextensions = left_rule.underextension_SRs(pairs=pairs) if len(left_underextensions) > 0: left_default_sr = sorted(left_underextensions.items(), reverse=True, key=lambda it: it[-1])[0][0] left_rule.set_defaults(dict((feat, left_default_sr.features[feat]) for feat in discrepancy.feature_diff)) # build right rule - right_rule = Rule(seginv=self.seginv, target=target, features=discrepancy.feature_diff, right_ctxts=rctxts, tier=tier, harmony=harmony) + right_rule = D2LRule(seginv=self.seginv, target=target, features=discrepancy.feature_diff, right_ctxts=rctxts, tier=tier, harmony=harmony) right_underextensions = right_rule.underextension_SRs(pairs=pairs) if len(right_underextensions) > 0: right_default_sr = sorted(right_underextensions.items(), reverse=True, key=lambda it: it[-1])[0][0] diff --git a/algophon/models/D2L/rule.py b/algophon/models/D2L/rule.py index 927d833..bf43500 100644 --- a/algophon/models/D2L/rule.py +++ b/algophon/models/D2L/rule.py @@ -5,9 +5,10 @@ from algophon import Seg, SegInv, NatClass, SegStr from algophon.symbols import FUNCTION_COMPOSITION, LWB, RWB, UNK, UNDERSPECIFIED from algophon.models.D2L import Tier +from algophon.models import Rule -class Rule: - def __init__(self, +class D2LRule(Rule): + def __init__(self, seginv: SegInv, target: set, features: set, @@ -21,9 +22,9 @@ def __init__(self, :target: a set of target (alternating) segments :features: a set of features that alternate :defaults: the values to use for :features: if no ctxt matches for a particular target. - :left_ctxts: (optional; default None) a set of right-adj (to target) segments that trigger rule application + :left_ctxts: (optional; default None) a set of left-adj (to target) segments that trigger rule application - Can be a set of specific segments or a NatClass object - :right_ctxts: (optional; default None) a set of left-adj (to target) segments that trigger a rule application + :right_ctxts: (optional; default None) a set of right-adj (to target) segments that trigger a rule application - Can be a set of specific segments or a NatClass object :tier: (optional; default None) a Tier object to apply the rule over :harmony: (optional; default True) specified whether rule enforces harmony or disharmony @@ -57,61 +58,6 @@ def __str__(self) -> str: else: return f'{adj_str} / __ {self.right_ctxts}{tier_str}' - def __repr__(self) -> str: - return self.__str__() - - def produce(self, ur: Union[str, SegStr]) -> SegStr: - ''' - Produces a SR for and input UR - - :ur: the UR to produce an SR for. Can be: - - space-separated str of IPA symbols - - SegStr object - - :return: a SegStr representing the predicted SR - ''' - if isinstance(ur, str): # convert str ur to SegStr - ur = SegStr(ur, seginv=self.seginv) - new_segs = list(ur._segs) # init new seg list - # apply predictions - for idx, seg in self._predictions(segstr=ur): - new_segs[idx] = seg - return SegStr(segs=new_segs, seginv=self.seginv) # return SegStr object - - # calling a Rule object amounts to calling its produce() method - __call__ = produce - - def accuracy(self, pairs: Iterable) -> float: - ''' - :pairs: an iterable of (UR, SR) pairs to compute accuracy for - - Computed over unique pairs - - :return: the accuracy of the rule's predictions of the :pairs: - ''' - n, m = self.tsp_stats(pairs=pairs) - return m / n if n > 0 else 0.0 - - def tsp_stats(self, pairs: Iterable) -> tuple[int, int]: - ''' - Computes n and m for the TSP w.r.t a set of pairs - - :pairs: an iterable of (UR, SR) pairs to compute the TSP stats for - - Computed over unique pairs - - :return: n and m - ''' - n, m = 0, 0 - for ur, sr in set(pairs): - if isinstance(ur, str): - ur = SegStr(ur, seginv=self.seginv) - if isinstance(sr, str): - sr = SegStr(sr, seginv=self.seginv) - for idx, pred_sr_seg in self._predictions(ur): - n += 1 - if sr[idx] == pred_sr_seg: - m += 1 - return n, m - def _predictions(self, segstr: SegStr) -> list: ''' :segstr: a SegStr to apply the rule to diff --git a/algophon/models/__init__.py b/algophon/models/__init__.py index e69de29..965aaa9 100644 --- a/algophon/models/__init__.py +++ b/algophon/models/__init__.py @@ -0,0 +1 @@ +from algophon.models.rule import Rule \ No newline at end of file diff --git a/algophon/models/rule.py b/algophon/models/rule.py new file mode 100644 index 0000000..291902b --- /dev/null +++ b/algophon/models/rule.py @@ -0,0 +1,73 @@ +from typing import Union, Iterable +from abc import abstractmethod + +from algophon import SegStr + +class Rule: + def __str__(self) -> str: + raise NotImplementedError(f'Method __str__ not implemented for {self} of type {type(self)}') + + def __repr__(self) -> str: + return self.__str__() + + def produce(self, ur: Union[str, SegStr]) -> SegStr: + ''' + Produces a SR for and input UR + + :ur: the UR to produce an SR for. Can be: + - space-separated str of IPA symbols + - SegStr object + + :return: a SegStr representing the predicted SR + ''' + if isinstance(ur, str): # convert str ur to SegStr + ur = SegStr(ur, seginv=self.seginv) + new_segs = list(ur._segs) # init new seg list + # apply predictions + for idx, seg in self._predictions(segstr=ur): + new_segs[idx] = seg + return SegStr(segs=new_segs, seginv=self.seginv) # return SegStr object + + # calling a Rule object amounts to calling its produce() method + __call__ = produce + + def accuracy(self, pairs: Iterable) -> float: + ''' + :pairs: an iterable of (UR, SR) pairs to compute accuracy for + - Computed over unique pairs + + :return: the accuracy of the rule's predictions of the :pairs: + ''' + n, m = self.tsp_stats(pairs=pairs) + return m / n if n > 0 else 0.0 + + def tsp_stats(self, pairs: Iterable) -> tuple[int, int]: + ''' + Computes n and m for the TSP w.r.t a set of pairs + + :pairs: an iterable of (UR, SR) pairs to compute the TSP stats for + - Computed over unique pairs + + :return: n and m + ''' + n, m = 0, 0 + for ur, sr in set(pairs): + if isinstance(ur, str): + ur = SegStr(ur, seginv=self.seginv) + if isinstance(sr, str): + sr = SegStr(sr, seginv=self.seginv) + for idx, pred_sr_seg in self._predictions(ur): + n += 1 + if sr[idx] == pred_sr_seg: + m += 1 + return n, m + + @abstractmethod + def _predictions(self, segstr: SegStr) -> list: + ''' + :segstr: a SegStr to apply the rule to + + :return: a list of the predictions that the rule makes over :segstr: + - Each item in the list is a tuple (index, new_seg) specifying each new_seg value predicted and at what index + ''' + raise NotImplementedError(f'Method _predictions not implemented for {self} of type {type(self)}') \ No newline at end of file diff --git a/tests/test_d2l.py b/tests/test_d2l.py index ce88c43..ce0a18b 100644 --- a/tests/test_d2l.py +++ b/tests/test_d2l.py @@ -3,7 +3,7 @@ sys.path.append('../') from algophon import SegInv, SegStr, NatClass -from algophon.models.D2L import Tier, Rule, D2L +from algophon.models.D2L import Tier, D2LRule, D2L from algophon.symbols import LWB, RWB, MORPHB, SYLB, UNDERSPECIFIED class TestD2L(unittest.TestCase): @@ -49,25 +49,25 @@ def test_tier_project(self): def test_rule_init(self): seginv = SegInv() - rule = Rule(seginv=seginv, target={'S'}, features={'ant', 'distr'}, left_ctxts=NatClass(feats={'+strid'}, seginv=seginv)) + rule = D2LRule(seginv=seginv, target={'S'}, features={'ant', 'distr'}, left_ctxts=NatClass(feats={'+strid'}, seginv=seginv)) assert(rule and rule.left_ctxts is not None and rule.right_ctxts is None) - rule = Rule(seginv=seginv, target={'S'}, features={'ant', 'distr'}, left_ctxts={'S', 's', 'ʃ'}) + rule = D2LRule(seginv=seginv, target={'S'}, features={'ant', 'distr'}, left_ctxts={'S', 's', 'ʃ'}) assert(rule and rule.left_ctxts is not None and rule.right_ctxts is None) - rule = Rule(seginv=seginv, target={'S'}, features={'ant', 'distr'}, right_ctxts=NatClass(feats={'+strid'}, seginv=seginv)) + rule = D2LRule(seginv=seginv, target={'S'}, features={'ant', 'distr'}, right_ctxts=NatClass(feats={'+strid'}, seginv=seginv)) assert(rule and rule.left_ctxts is None and rule.right_ctxts is not None) - rule = Rule(seginv=seginv, target={'S'}, features={'ant', 'distr'}, right_ctxts={'S', 's', 'ʃ'}) + rule = D2LRule(seginv=seginv, target={'S'}, features={'ant', 'distr'}, right_ctxts={'S', 's', 'ʃ'}) assert(rule and rule.left_ctxts is None and rule.right_ctxts is not None) try: - Rule(seginv=seginv, target={'S'}, features={'ant', 'distr'}, left_ctxts=NatClass(feats={'+strid'}, seginv=seginv), right_ctxts={'S', 's', 'ʃ'}) + D2LRule(seginv=seginv, target={'S'}, features={'ant', 'distr'}, left_ctxts=NatClass(feats={'+strid'}, seginv=seginv), right_ctxts={'S', 's', 'ʃ'}) assert(False) except ValueError as e: assert(e.__str__() == 'D2L Rule cannot have both left and right contexts.') assert(True) try: - Rule(seginv=seginv, features={'ant', 'distr'}, target={'S'}) + D2LRule(seginv=seginv, features={'ant', 'distr'}, target={'S'}) assert(False) except ValueError as e: assert(e.__str__() == 'D2L Rule must have either left or right contexts.') @@ -89,7 +89,7 @@ def test_rule__apply(self): tier = Tier(seginv=seginv, feats={'-syl'}) # harmony - rule = Rule(seginv=seginv, features={'son', 'nas'}, target={'C1', 'C2'}, left_ctxts=NatClass({'-syl'}, seginv=seginv), tier=tier) + rule = D2LRule(seginv=seginv, features={'son', 'nas'}, target={'C1', 'C2'}, left_ctxts=NatClass({'-syl'}, seginv=seginv), tier=tier) assert(rule._apply(seg=seginv['C1'], ctxt=seginv['m']) == 'm') assert(rule._apply(seg=seginv['C1'], ctxt=seginv['n']) == 'm') @@ -105,7 +105,7 @@ def test_rule__apply(self): # disharmony - rule = Rule(seginv=seginv, features={'son', 'nas'}, target={'C1', 'C2'}, left_ctxts=NatClass({'-syl'}, seginv=seginv), tier=tier, harmony=False) + rule = D2LRule(seginv=seginv, features={'son', 'nas'}, target={'C1', 'C2'}, left_ctxts=NatClass({'-syl'}, seginv=seginv), tier=tier, harmony=False) assert(rule._apply(seg=seginv['C1'], ctxt=seginv['m']) == 'b') assert(rule._apply(seg=seginv['C1'], ctxt=seginv['n']) == 'b') @@ -133,7 +133,7 @@ def test_rule__apply_default(self): seginv.add_custom('C2', features=features) # n ~ d # harmony - rule = Rule(seginv=seginv, features={'son', 'nas'}, defaults={'son': '-', 'nas': '-'}, target={'C1', 'C2'}, left_ctxts=NatClass({'-syl'}, seginv=seginv)) + rule = D2LRule(seginv=seginv, features={'son', 'nas'}, defaults={'son': '-', 'nas': '-'}, target={'C1', 'C2'}, left_ctxts=NatClass({'-syl'}, seginv=seginv)) assert(rule._apply_default(seg=seginv['C1']) == 'b') assert(rule._apply_default(seg=seginv['C2']) == 'd') @@ -155,7 +155,7 @@ def test_rule__predictions(self): # left-to-right (harmony) - rule = Rule(seginv=seginv, features={'son', 'nas'}, target={'C1', 'C2'}, left_ctxts=NatClass({'-syl'}, seginv=seginv), tier=tier) + rule = D2LRule(seginv=seginv, features={'son', 'nas'}, target={'C1', 'C2'}, left_ctxts=NatClass({'-syl'}, seginv=seginv), tier=tier) segstr = SegStr('b u m e t u C2 i l', seginv=seginv) assert(rule._predictions(segstr) == [(6, 'd')]) @@ -174,7 +174,7 @@ def test_rule__predictions(self): # right-to-left (harmony) - rule = Rule(seginv=seginv, features={'son', 'nas'}, target={'C1', 'C2'}, right_ctxts=NatClass({'-syl'}, seginv=seginv), tier=tier) + rule = D2LRule(seginv=seginv, features={'son', 'nas'}, target={'C1', 'C2'}, right_ctxts=NatClass({'-syl'}, seginv=seginv), tier=tier) segstr = SegStr('l i C2 u t e m u b', seginv=seginv) assert(rule._predictions(segstr) == [(2, 'd')]) @@ -193,7 +193,7 @@ def test_rule__predictions(self): # left-to-right (disharmony) - rule = Rule(seginv=seginv, features={'son', 'nas'}, target={'C1', 'C2'}, left_ctxts=NatClass({'-syl'}, seginv=seginv), tier=tier, harmony=False) + rule = D2LRule(seginv=seginv, features={'son', 'nas'}, target={'C1', 'C2'}, left_ctxts=NatClass({'-syl'}, seginv=seginv), tier=tier, harmony=False) segstr = SegStr('b u m e t u C2 i l', seginv=seginv) assert(rule._predictions(segstr) == [(6, 'n')]) @@ -212,7 +212,7 @@ def test_rule__predictions(self): # right-to-left (disharmony) - rule = Rule(seginv=seginv, features={'son', 'nas'}, target={'C1', 'C2'}, right_ctxts=NatClass({'-syl'}, seginv=seginv), tier=tier, harmony=False) + rule = D2LRule(seginv=seginv, features={'son', 'nas'}, target={'C1', 'C2'}, right_ctxts=NatClass({'-syl'}, seginv=seginv), tier=tier, harmony=False) segstr = SegStr('l i C2 u t e m u b', seginv=seginv) assert(rule._predictions(segstr) == [(2, 'n')]) @@ -248,13 +248,13 @@ def test_rule__predictions(self): strid = NatClass(feats={'+strid'}, seginv=seginv) tier = Tier(seginv=seginv, feats=strid) - rule = Rule(seginv=seginv, target={'S'}, features={'ant', 'distr'}, defaults={'ant': '+', 'distr': '-'}, left_ctxts=strid, tier=tier) + rule = D2LRule(seginv=seginv, target={'S'}, features={'ant', 'distr'}, defaults={'ant': '+', 'distr': '-'}, left_ctxts=strid, tier=tier) assert(rule._predictions(SegStr('ʃ o k u S i S', seginv=seginv)) == [(4, 'ʃ'), (6, 'ʃ')]) assert(rule._predictions(SegStr('u t S', seginv=seginv)) == [(2, 's')]) cons = NatClass(feats={'+cons'}, seginv=seginv) tier = Tier(seginv=seginv, feats=cons) - rule = Rule(seginv=seginv, target={'S'}, features={'ant', 'distr'}, left_ctxts=cons, tier=tier) + rule = D2LRule(seginv=seginv, target={'S'}, features={'ant', 'distr'}, left_ctxts=cons, tier=tier) assert(rule._predictions(SegStr('ʃ o k u S i S', seginv=seginv)) == [(4, 'S'), (6, 'S')]) assert(rule._predictions(SegStr('u t S', seginv=seginv)) == [(2, 's')]) @@ -274,7 +274,7 @@ def test_rule_produce(self): tier = Tier(seginv=seginv, feats={'-syl'}) # left-to-right (harmony) - rule = Rule(seginv=seginv, features={'son', 'nas'}, target={'C1', 'C2'}, left_ctxts=NatClass({'-syl'}, seginv=seginv), tier=tier) + rule = D2LRule(seginv=seginv, features={'son', 'nas'}, target={'C1', 'C2'}, left_ctxts=NatClass({'-syl'}, seginv=seginv), tier=tier) assert(rule('b u m e t u C2 i l') == 'b u m e t u d i l') assert(rule('b u m e C1 i l') == 'b u m e m i l') assert(rule('b u m e C1 i C2') == 'b u m e m i n') @@ -282,7 +282,7 @@ def test_rule_produce(self): assert(rule('b u m e') == 'b u m e') # right-to-left (harmony) - rule = Rule(seginv=seginv, features={'son', 'nas'}, target={'C1', 'C2'}, right_ctxts=NatClass({'-syl'}, seginv=seginv), tier=tier) + rule = D2LRule(seginv=seginv, features={'son', 'nas'}, target={'C1', 'C2'}, right_ctxts=NatClass({'-syl'}, seginv=seginv), tier=tier) assert(rule('l i C2 u t e m u b') == 'l i d u t e m u b') assert(rule('l i C1 e m u b') == 'l i m e m u b') assert(rule('C2 i C1 e m u b') == 'n i m e m u b') @@ -290,7 +290,7 @@ def test_rule_produce(self): assert(rule('b u m e') == 'b u m e') # left-to-right (disharmony) - rule = Rule(seginv=seginv, features={'son', 'nas'}, target={'C1', 'C2'}, left_ctxts=NatClass({'-syl'}, seginv=seginv), tier=tier, harmony=False) + rule = D2LRule(seginv=seginv, features={'son', 'nas'}, target={'C1', 'C2'}, left_ctxts=NatClass({'-syl'}, seginv=seginv), tier=tier, harmony=False) assert(rule('b u m e t u C2 i l') == 'b u m e t u n i l') assert(rule('b u m e C1 i l') == 'b u m e b i l') assert(rule('b u m e C1 i C2') == 'b u m e b i n') @@ -298,7 +298,7 @@ def test_rule_produce(self): assert(rule('b u m e') == 'b u m e') # right-to-left (disharmony) - rule = Rule(seginv=seginv, features={'son', 'nas'}, target={'C1', 'C2'}, right_ctxts=NatClass({'-syl'}, seginv=seginv), tier=tier, harmony=False) + rule = D2LRule(seginv=seginv, features={'son', 'nas'}, target={'C1', 'C2'}, right_ctxts=NatClass({'-syl'}, seginv=seginv), tier=tier, harmony=False) assert(rule('l i C2 u t e m u b') == 'l i n u t e m u b') assert(rule('l i C1 e m u b') == 'l i b e m u b') assert(rule('C2 i C1 e m u b') == 'n i b e m u b') @@ -322,17 +322,17 @@ def test_rule_tsp_stats(self): seginv['ʃ']['strid'] = '+' seginv['S']['strid'] = '+' - rule = Rule(seginv=seginv, target={'S'}, features={'ant', 'distr'}, left_ctxts=seginv.segs) + rule = D2LRule(seginv=seginv, target={'S'}, features={'ant', 'distr'}, left_ctxts=seginv.segs) assert(rule.tsp_stats(pairs) == (8, 1)) cons = NatClass(feats={'+cons'}, seginv=seginv) tier = Tier(seginv=seginv, feats=cons) - rule = Rule(seginv=seginv, target={'S'}, features={'ant', 'distr'}, left_ctxts=cons, tier=tier) + rule = D2LRule(seginv=seginv, target={'S'}, features={'ant', 'distr'}, left_ctxts=cons, tier=tier) assert(rule.tsp_stats(pairs) == (8, 2)) # this is diff from paper b.c. the feature specificaions involve distr, which is 0 for /k/ strid = NatClass(feats={'+strid'}, seginv=seginv) tier = Tier(seginv=seginv, feats=strid) - rule = Rule(seginv=seginv, target={'S'}, features={'ant', 'distr'}, defaults={'ant': '+', 'distr': '-'}, left_ctxts=strid, tier=tier) + rule = D2LRule(seginv=seginv, target={'S'}, features={'ant', 'distr'}, defaults={'ant': '+', 'distr': '-'}, left_ctxts=strid, tier=tier) assert(rule.tsp_stats(pairs) == (8, 8)) def test_rule_underextension_SRs(self): @@ -354,13 +354,13 @@ def test_rule_underextension_SRs(self): strid = NatClass(feats={'+strid'}, seginv=seginv) tier = Tier(seginv=seginv, feats=strid) - rule = Rule(seginv=seginv, target={'S'}, features={'ant', 'distr'}, left_ctxts=strid, tier=tier) + rule = D2LRule(seginv=seginv, target={'S'}, features={'ant', 'distr'}, left_ctxts=strid, tier=tier) assert(rule.underextension_SRs(pairs) == {'s': 3, 'ʃ': 1}) def test_rule_set_defaults(self): seginv = SegInv() seginv.add_segs({'s', 'ʃ'}) - rule = Rule(seginv=seginv, target={'S'}, features={'ant', 'distr'}, left_ctxts={'s', 'ʃ'}) + rule = D2LRule(seginv=seginv, target={'S'}, features={'ant', 'distr'}, left_ctxts={'s', 'ʃ'}) assert(rule.defaults is None) rule.set_defaults(defaults={'ant': seginv['s']['ant'], 'distr': seginv['s']['distr']}) assert(len(rule.defaults) == 2) @@ -394,24 +394,24 @@ def test_rule_errant_ctxts(self): seginv['ʃ']['distr'] = '+' seginv['S']['distr'] = '+' - rule = Rule(seginv=seginv, target={'S'}, features={'ant'}, left_ctxts=seginv.segs) + rule = D2LRule(seginv=seginv, target={'S'}, features={'ant'}, left_ctxts=seginv.segs) assert(rule.errant_ctxts(pairs) == {'u', 'i', 'a', 'o'}) cons = NatClass(feats={'+cons'}, seginv=seginv) tier = Tier(seginv=seginv, feats=cons) - rule = Rule(seginv=seginv, target={'S'}, features={'ant'}, left_ctxts=cons, tier=tier) + rule = D2LRule(seginv=seginv, target={'S'}, features={'ant'}, left_ctxts=cons, tier=tier) assert(rule.errant_ctxts(pairs) == {'n', 'k', 'g', 'ʃ'}) strid = NatClass(feats={'+strid'}, seginv=seginv) tier = Tier(seginv=seginv, feats=strid) - rule = Rule(seginv=seginv, target={'S'}, features={'ant'}, defaults={'ant': '+'}, left_ctxts=strid, tier=tier) + rule = D2LRule(seginv=seginv, target={'S'}, features={'ant'}, defaults={'ant': '+'}, left_ctxts=strid, tier=tier) assert(rule.errant_ctxts(pairs) == set()) def test_rule_set_ctxts(self): seginv = SegInv() seginv.add_segs({'a', 'e', 'i', 'o', 'u', 't', 'p'}) vowels = NatClass(feats={'+syl'}, seginv=seginv) - rule = Rule(seginv=seginv, target={'e', 'i'}, features={'back'}, left_ctxts={'a', 'e', 'i', 'o', 'u', 't', 'p'}) + rule = D2LRule(seginv=seginv, target={'e', 'i'}, features={'back'}, left_ctxts={'a', 'e', 'i', 'o', 'u', 't', 'p'}) assert(rule.left_ctxts == {'a', 'e', 'i', 'o', 'u', 't', 'p'}) rule.set_ctxts(ctxts=vowels) assert(rule.left_ctxts == vowels) From 347ae14fd66bdcbdea7de52915a39ea0df2f01fc Mon Sep 17 00:00:00 2001 From: cbelth Date: Tue, 4 Jun 2024 10:14:12 -0600 Subject: [PATCH 3/4] simplify D2L Discrepancy --- algophon/models/D2L/d2l.py | 2 +- algophon/models/D2L/discrepancy.py | 26 +++++--------------------- tests/test_d2l.py | 4 ++-- 3 files changed, 8 insertions(+), 24 deletions(-) diff --git a/algophon/models/D2L/d2l.py b/algophon/models/D2L/d2l.py index 94c4910..6858248 100644 --- a/algophon/models/D2L/d2l.py +++ b/algophon/models/D2L/d2l.py @@ -187,7 +187,7 @@ def _train_setup(self, pairs: Iterable) -> set: if self._discrepancy is None: # init discrepancy if it does not exist self._discrepancy = Discrepancy(feat_diff) # tabulate this pair's contribution to the discrepancy - self._discrepancy.tabulate(ur=ur, i=i, ur_seg=ur_seg, sr_seg=sr_seg) + self._discrepancy.tabulate(ur=ur, ur_seg=ur_seg, sr_seg=sr_seg) return setup_pairs diff --git a/algophon/models/D2L/discrepancy.py b/algophon/models/D2L/discrepancy.py index eea4ca8..6537090 100644 --- a/algophon/models/D2L/discrepancy.py +++ b/algophon/models/D2L/discrepancy.py @@ -10,34 +10,18 @@ def __init__(self, feature_diff: set) -> object: :feature_diff: the features that differ between alternating ur_seg ~ sr_seg pairs ''' self.alternations = set() # stores the (ur_seg ~ sr_seg) alternations corresponding to the discrepancy - self.instances = set() # stores each instance of the discrepancy + self.URs = set() # stores each UR with a discrepancy self.feature_diff = feature_diff - def __contains__(self, item: tuple) -> bool: - return item in self.alternations - - def __str__(self) -> str: - return self.feature_diff.__str__() - - def __repr__(self) -> str: - return self.__str__() - - def tabulate(self, ur: SegStr, i: int, ur_seg: Seg, sr_seg: Seg) -> None: + def tabulate(self, ur: SegStr, ur_seg: Seg, sr_seg: Seg) -> None: ''' :ur: the UR exibiting the discrepancy - :i: the index where the discrepancy occurs :ur_seg:, :sr_seg: the ur_seg ~ sr_seg alternation :return: None ''' self.alternations.add((ur_seg, sr_seg)) # update alternations - self.instances.add((ur, i, sr_seg)) # update instances - - def get_alternating(self) -> set: - ''' - :return: a set of all the underling and surface Seg objects that are involved in the alternation - ''' - return set(it[0] for it in self.alternations).union(it[1] for it in self.alternations) + self.URs.add(ur) # update URs set def get_alternating_UR_segs(self) -> set: ''' @@ -47,6 +31,6 @@ def get_alternating_UR_segs(self) -> set: def get_URs(self) -> set: ''' - :return: a set of all the URs that are involved in the alternation + :return: the set of all URs that are involved in the alternation ''' - return set(it[0] for it in self.instances) \ No newline at end of file + return self.URs \ No newline at end of file diff --git a/tests/test_d2l.py b/tests/test_d2l.py index ce0a18b..d0da2d1 100644 --- a/tests/test_d2l.py +++ b/tests/test_d2l.py @@ -439,7 +439,7 @@ def test_D2L__train_setup(self): assert(isinstance(list(setup_pairs)[0][1], SegStr)) assert(d2l._discrepancy is not None) assert(d2l._discrepancy.alternations == {('S', 's'), ('S', 'ʃ')}) - assert(len(d2l._discrepancy.instances) == 8) + assert(len(d2l._discrepancy.URs) == 6) # make sure duplicates are removed @@ -463,7 +463,7 @@ def test_D2L__train_setup(self): assert(isinstance(list(setup_pairs)[0][1], SegStr)) assert(d2l._discrepancy is not None) assert(d2l._discrepancy.alternations == {('S', 's'), ('S', 'ʃ')}) - assert(len(d2l._discrepancy.instances) == 8) + assert(len(d2l._discrepancy.URs) == 6) def test_D2L__get_tier_adj_contexts(self): pairs = [ From 4f6f130848e44df685182c0ea6a98fa0842b9ef6 Mon Sep 17 00:00:00 2001 From: cbelth Date: Tue, 4 Jun 2024 10:18:53 -0600 Subject: [PATCH 4/4] init some PLP methods --- algophon/models/PLP/__init__.py | 2 +- algophon/models/PLP/discrepancy.py | 4 +++ algophon/models/PLP/rule.py | 46 ++++++++++++++++++++++++++++-- 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/algophon/models/PLP/__init__.py b/algophon/models/PLP/__init__.py index 6c2e26e..0e5c554 100644 --- a/algophon/models/PLP/__init__.py +++ b/algophon/models/PLP/__init__.py @@ -1,4 +1,4 @@ from algophon.models.PLP.discrepancy import Discrepancy -from algophon.models.PLP.rule import Rule +from algophon.models.PLP.rule import PLPRule from algophon.models.PLP.grammar import Grammar from algophon.models.PLP.plp import PLP \ No newline at end of file diff --git a/algophon/models/PLP/discrepancy.py b/algophon/models/PLP/discrepancy.py index 3652ea8..eac0a4c 100644 --- a/algophon/models/PLP/discrepancy.py +++ b/algophon/models/PLP/discrepancy.py @@ -1,4 +1,8 @@ class Discrepancy: + ''' + A class for representing a discrepancy—i.e., a difference between URs and SRs. + ''' + def __init__(self) -> object: pass # TODO \ No newline at end of file diff --git a/algophon/models/PLP/rule.py b/algophon/models/PLP/rule.py index e4f4ee2..b8b992c 100644 --- a/algophon/models/PLP/rule.py +++ b/algophon/models/PLP/rule.py @@ -1,4 +1,46 @@ -class Rule: - def __init__(self) -> object: +from typing import Union + +from algophon import SegStr, NatClass +from algophon.models import Rule + +class PLPRule(Rule): + ''' + Implements an SPE-style rule A -> B / C __ D + ''' + + def __init__(self, + seginv, + target: set, + left_ctxt: Union[None, list[Union[set, NatClass]]]=None, + right_ctxt: Union[None, list[Union[set, NatClass]]]=None) -> object: + ''' + :seginv: a SegInv object + :target: a set of target (alternating) segments + :left_ctxts: (optional; default None) left ctxt that triggers rule application: / C __ + - a list of items, each being a set or NatClass object + - distance from :target: interpreted right to left: / [C_0, C_1, ...] __ + :right_ctxts: (optional; default None) right ctxt that triggers rule application: / __ D + - a list of items, each being a set or NatClass object + - distance from :target: interpreted left to right: / __ [D_0, D_1, ...] + ''' + # init variables + self.seginv = seginv + self.target = target + self.left_ctxt = left_ctxt + self.right_ctxt = right_ctxt + + # TODO + + def _predictions(self, segstr: SegStr) -> list: + ''' + :segstr: a SegStr to apply the rule to + + :return: a list of the predictions that the rule makes over :segstr: + - Each item in the list is a tuple (index, new_seg) specifying each new_seg value predicted and at what index + ''' + pass + # TODO + + def __str__(self) -> str: pass # TODO \ No newline at end of file