ripple-down-rules 0.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,260 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from enum import Enum
5
+
6
+ from anytree import NodeMixin
7
+ from typing_extensions import List, Optional, Self, Union, Dict, Any
8
+
9
+ from .datastructures import CallableExpression, Case, SQLTable
10
+ from .datastructures.enums import RDREdge, Stop
11
+ from .utils import SubclassJSONSerializer
12
+
13
+
14
+ class Rule(NodeMixin, SubclassJSONSerializer, ABC):
15
+ fired: Optional[bool] = None
16
+ """
17
+ Whether the rule has fired or not.
18
+ """
19
+
20
+ def __init__(self, conditions: Optional[CallableExpression] = None,
21
+ conclusion: Optional[CallableExpression] = None,
22
+ parent: Optional[Rule] = None,
23
+ corner_case: Optional[Union[Case, SQLTable]] = None,
24
+ weight: Optional[str] = None):
25
+ """
26
+ A rule in the ripple down rules classifier.
27
+
28
+ :param conditions: The conditions of the rule.
29
+ :param conclusion: The conclusion of the rule when the conditions are met.
30
+ :param parent: The parent rule of this rule.
31
+ :param corner_case: The corner case that this rule is based on/created from.
32
+ :param weight: The weight of the rule, which is the type of edge connecting the rule to its parent.
33
+ """
34
+ super(Rule, self).__init__()
35
+ self.conclusion = conclusion
36
+ self.corner_case = corner_case
37
+ self.parent = parent
38
+ self.weight: Optional[str] = weight
39
+ self.conditions = conditions if conditions else None
40
+ self.json_serialization: Optional[Dict[str, Any]] = None
41
+
42
+ def _post_detach(self, parent):
43
+ """
44
+ Called after this node is detached from the tree, useful when drawing the tree.
45
+
46
+ :param parent: The parent node from which this node was detached.
47
+ """
48
+ self.weight = None
49
+
50
+ def __call__(self, x: Case) -> Self:
51
+ return self.evaluate(x)
52
+
53
+ def evaluate(self, x: Case) -> Rule:
54
+ """
55
+ Check if the rule or its refinement or its alternative match the case,
56
+ by checking if the conditions are met, then return the rule that matches the case.
57
+
58
+ :param x: The case to evaluate the rule on.
59
+ :return: The rule that fired or the last evaluated rule if no rule fired.
60
+ """
61
+ if not self.conditions:
62
+ raise ValueError("Rule has no conditions")
63
+ self.fired = self.conditions(x)
64
+ return self.evaluate_next_rule(x)
65
+
66
+ @abstractmethod
67
+ def evaluate_next_rule(self, x: Case):
68
+ """
69
+ Evaluate the next rule after this rule is evaluated.
70
+ """
71
+ pass
72
+
73
+ @property
74
+ def name(self):
75
+ """
76
+ Get the name of the rule, which is the conditions and the conclusion.
77
+ """
78
+ return self.__str__()
79
+
80
+ def __str__(self, sep="\n"):
81
+ """
82
+ Get the string representation of the rule, which is the conditions and the conclusion.
83
+ """
84
+ return f"{self.conditions}{sep}=> {self.conclusion}"
85
+
86
+ def __repr__(self):
87
+ return self.__str__()
88
+
89
+
90
+ class HasAlternativeRule:
91
+ """
92
+ A mixin class for rules that have an alternative rule.
93
+ """
94
+ _alternative: Optional[Rule] = None
95
+ """
96
+ The alternative rule of the rule, which is evaluated when the rule doesn't fire.
97
+ """
98
+ furthest_alternative: Optional[List[Rule]] = None
99
+ """
100
+ The furthest alternative rule of the rule, which is the last alternative rule in the chain of alternative rules.
101
+ """
102
+ all_alternatives: Optional[List[Rule]] = None
103
+ """
104
+ All alternative rules of the rule, which is all the alternative rules in the chain of alternative rules.
105
+ """
106
+
107
+ @property
108
+ def alternative(self) -> Optional[Rule]:
109
+ return self._alternative
110
+
111
+ @alternative.setter
112
+ def alternative(self, new_rule: Rule):
113
+ """
114
+ Set the alternative rule of the rule. It is important that no rules should be retracted or changed,
115
+ only new rules should be added.
116
+ """
117
+ if self.furthest_alternative:
118
+ self.furthest_alternative[-1].alternative = new_rule
119
+ else:
120
+ new_rule.parent = self
121
+ new_rule.weight = RDREdge.Alternative.value if not new_rule.weight else new_rule.weight
122
+ self._alternative = new_rule
123
+ self.furthest_alternative = [new_rule]
124
+
125
+
126
+ class HasRefinementRule:
127
+ _refinement: Optional[HasAlternativeRule] = None
128
+ """
129
+ The refinement rule of the rule, which is evaluated when the rule fires.
130
+ """
131
+
132
+ @property
133
+ def refinement(self) -> Optional[Rule]:
134
+ return self._refinement
135
+
136
+ @refinement.setter
137
+ def refinement(self, new_rule: Rule):
138
+ """
139
+ Set the refinement rule of the rule. It is important that no rules should be retracted or changed,
140
+ only new rules should be added.
141
+ """
142
+ new_rule.top_rule = self
143
+ if self.refinement:
144
+ self.refinement.alternative = new_rule
145
+ else:
146
+ new_rule.parent = self
147
+ new_rule.weight = RDREdge.Refinement.value
148
+ self._refinement = new_rule
149
+
150
+
151
+ class SingleClassRule(Rule, HasAlternativeRule, HasRefinementRule):
152
+ """
153
+ A rule in the SingleClassRDR classifier, it can have a refinement or an alternative rule or both.
154
+ """
155
+
156
+ def evaluate_next_rule(self, x: Case) -> SingleClassRule:
157
+ if self.fired:
158
+ returned_rule = self.refinement(x) if self.refinement else self
159
+ else:
160
+ returned_rule = self.alternative(x) if self.alternative else self
161
+ return returned_rule if returned_rule.fired else self
162
+
163
+ def fit_rule(self, x: Case, target: CallableExpression, conditions: CallableExpression):
164
+ new_rule = SingleClassRule(conditions, target,
165
+ corner_case=x, parent=self)
166
+ if self.fired:
167
+ self.refinement = new_rule
168
+ else:
169
+ self.alternative = new_rule
170
+
171
+ def write_conclusion_as_source_code(self, parent_indent: str = "") -> str:
172
+ """
173
+ Get the source code representation of the conclusion of the rule.
174
+
175
+ :param parent_indent: The indentation of the parent rule.
176
+ """
177
+ if isinstance(self.conclusion, CallableExpression):
178
+ conclusion = self.conclusion.parsed_user_input
179
+ elif isinstance(self.conclusion, Enum):
180
+ conclusion = str(self.conclusion)
181
+ else:
182
+ conclusion = self.conclusion
183
+ return f"{parent_indent}{' ' * 4}return {conclusion}\n"
184
+
185
+ def write_condition_as_source_code(self, parent_indent: str = "") -> str:
186
+ """
187
+ Get the source code representation of the conditions of the rule.
188
+
189
+ :param parent_indent: The indentation of the parent rule.
190
+ """
191
+ if_clause = "elif" if self.weight == RDREdge.Alternative.value else "if"
192
+ return f"{parent_indent}{if_clause} {self.conditions.parsed_user_input}:\n"
193
+
194
+ def to_json(self) -> Dict[str, Any]:
195
+ self.json_serialization = {**SubclassJSONSerializer.to_json(self),
196
+ "conditions": self.conditions.to_json(),
197
+ "conclusion": self.conclusion.to_json(),
198
+ "parent": self.parent.json_serialization if self.parent else None,
199
+ "corner_case": self.corner_case.to_json() if self.corner_case else None,
200
+ "weight": self.weight,
201
+ "refinement": self.refinement.to_json() if self.refinement is not None else None,
202
+ "alternative": self.alternative.to_json() if self.alternative is not None else None}
203
+ return self.json_serialization
204
+
205
+ @classmethod
206
+ def _from_json(cls, data: Dict[str, Any]) -> SingleClassRule:
207
+ loaded_rule = cls(conditions=CallableExpression.from_json(data["conditions"]),
208
+ conclusion=CallableExpression.from_json(data["conclusion"]),
209
+ parent=SingleClassRule.from_json(data["parent"]),
210
+ corner_case=Case.from_json(data["corner_case"]),
211
+ weight=data["weight"])
212
+ loaded_rule.refinement = SingleClassRule.from_json(data["refinement"])
213
+ loaded_rule.alternative = SingleClassRule.from_json(data["alternative"])
214
+ return loaded_rule
215
+
216
+
217
+ class MultiClassStopRule(Rule, HasAlternativeRule):
218
+ """
219
+ A rule in the MultiClassRDR classifier, it can have an alternative rule and a top rule,
220
+ the conclusion of the rule is a Stop category meant to stop the parent conclusion from being made.
221
+ """
222
+ top_rule: Optional[MultiClassTopRule] = None
223
+ """
224
+ The top rule of the rule, which is the nearest ancestor that fired and this rule is a refinement of.
225
+ """
226
+
227
+ def __init__(self, *args, **kwargs):
228
+ super(MultiClassStopRule, self).__init__(*args, **kwargs)
229
+ self.conclusion = Stop.stop
230
+
231
+ def evaluate_next_rule(self, x: Case) -> Optional[Union[MultiClassStopRule, MultiClassTopRule]]:
232
+ if self.fired:
233
+ self.top_rule.fired = False
234
+ return self.top_rule.alternative
235
+ elif self.alternative:
236
+ return self.alternative(x)
237
+ else:
238
+ return self.top_rule.alternative
239
+
240
+
241
+ class MultiClassTopRule(Rule, HasRefinementRule, HasAlternativeRule):
242
+ """
243
+ A rule in the MultiClassRDR classifier, it can have a refinement and a next rule.
244
+ """
245
+
246
+ def __init__(self, *args, **kwargs):
247
+ super(MultiClassTopRule, self).__init__(*args, **kwargs)
248
+ self.weight = RDREdge.Next.value
249
+
250
+ def evaluate_next_rule(self, x: Case) -> Optional[Union[MultiClassStopRule, MultiClassTopRule]]:
251
+ if self.fired and self.refinement:
252
+ return self.refinement(x)
253
+ elif self.alternative: # Here alternative refers to next rule in MultiClassRDR
254
+ return self.alternative
255
+
256
+ def fit_rule(self, x: Case, target: CallableExpression, conditions: CallableExpression):
257
+ if self.fired and target != self.conclusion:
258
+ self.refinement = MultiClassStopRule(conditions, corner_case=x, parent=self)
259
+ elif not self.fired:
260
+ self.alternative = MultiClassTopRule(conditions, target, corner_case=x, parent=self)