ripple-down-rules 0.6.24__py3-none-any.whl → 0.6.25__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.
- ripple_down_rules/__init__.py +1 -1
- ripple_down_rules/datastructures/callable_expression.py +3 -2
- ripple_down_rules/datastructures/dataclasses.py +2 -2
- ripple_down_rules/datastructures/enums.py +4 -1
- ripple_down_rules/experts.py +2 -2
- ripple_down_rules/helpers.py +28 -7
- ripple_down_rules/rdr.py +68 -22
- ripple_down_rules/rules.py +160 -53
- ripple_down_rules/user_interface/prompt.py +6 -3
- ripple_down_rules/utils.py +16 -11
- {ripple_down_rules-0.6.24.dist-info → ripple_down_rules-0.6.25.dist-info}/METADATA +1 -1
- ripple_down_rules-0.6.25.dist-info/RECORD +24 -0
- ripple_down_rules-0.6.24.dist-info/RECORD +0 -24
- {ripple_down_rules-0.6.24.dist-info → ripple_down_rules-0.6.25.dist-info}/WHEEL +0 -0
- {ripple_down_rules-0.6.24.dist-info → ripple_down_rules-0.6.25.dist-info}/licenses/LICENSE +0 -0
- {ripple_down_rules-0.6.24.dist-info → ripple_down_rules-0.6.25.dist-info}/top_level.txt +0 -0
ripple_down_rules/__init__.py
CHANGED
@@ -95,7 +95,7 @@ class CallableExpression(SubclassJSONSerializer):
|
|
95
95
|
encapsulating_function_name: str = "_get_value"
|
96
96
|
|
97
97
|
def __init__(self, user_input: Optional[str] = None,
|
98
|
-
conclusion_type: Optional[Tuple[Type]] = None,
|
98
|
+
conclusion_type: Optional[Tuple[Type, ...]] = None,
|
99
99
|
expression_tree: Optional[AST] = None,
|
100
100
|
scope: Optional[Dict[str, Any]] = None,
|
101
101
|
conclusion: Optional[Any] = None,
|
@@ -176,7 +176,8 @@ class CallableExpression(SubclassJSONSerializer):
|
|
176
176
|
new_user_input = (f"{cond1_user_input}\n"
|
177
177
|
f"{cond2_user_input}\n"
|
178
178
|
f"return _cond1(case) and _cond2(case)")
|
179
|
-
return CallableExpression(new_user_input, conclusion_type=self.conclusion_type
|
179
|
+
return CallableExpression(new_user_input, conclusion_type=self.conclusion_type,
|
180
|
+
mutually_exclusive=self.mutually_exclusive)
|
180
181
|
|
181
182
|
def update_user_input_from_file(self, file_path: str, function_name: str):
|
182
183
|
"""
|
@@ -31,7 +31,7 @@ class CaseQuery:
|
|
31
31
|
"""
|
32
32
|
The name of the attribute.
|
33
33
|
"""
|
34
|
-
_attribute_types: Tuple[Type]
|
34
|
+
_attribute_types: Tuple[Type, ...]
|
35
35
|
"""
|
36
36
|
The type(s) of the attribute.
|
37
37
|
"""
|
@@ -139,7 +139,7 @@ class CaseQuery:
|
|
139
139
|
attribute_types_str = f"Union[{', '.join([t.__name__ for t in self.core_attribute_type])}]"
|
140
140
|
else:
|
141
141
|
attribute_types_str = self.core_attribute_type[0].__name__
|
142
|
-
if
|
142
|
+
if not self.mutually_exclusive:
|
143
143
|
return f"List[{attribute_types_str}]"
|
144
144
|
else:
|
145
145
|
return attribute_types_str
|
@@ -196,7 +196,10 @@ class RDREdge(Enum):
|
|
196
196
|
"""
|
197
197
|
Next edge, the edge that represents the next rule to be evaluated.
|
198
198
|
"""
|
199
|
-
|
199
|
+
Filter = "filter if"
|
200
|
+
"""
|
201
|
+
Filter edge, the edge that represents the filter condition.
|
202
|
+
"""
|
200
203
|
|
201
204
|
class ValueType(Enum):
|
202
205
|
Unary = auto()
|
ripple_down_rules/experts.py
CHANGED
@@ -41,14 +41,14 @@ class Expert(ABC):
|
|
41
41
|
A flag to indicate if the expert should use loaded answers or not.
|
42
42
|
"""
|
43
43
|
|
44
|
-
def __init__(self, use_loaded_answers: bool =
|
44
|
+
def __init__(self, use_loaded_answers: bool = False,
|
45
45
|
append: bool = False,
|
46
46
|
answers_save_path: Optional[str] = None):
|
47
47
|
self.all_expert_answers = []
|
48
48
|
self.use_loaded_answers = use_loaded_answers
|
49
49
|
self.append = append
|
50
50
|
self.answers_save_path = answers_save_path
|
51
|
-
if answers_save_path is not None:
|
51
|
+
if answers_save_path is not None and os.path.exists(answers_save_path + '.py'):
|
52
52
|
if use_loaded_answers:
|
53
53
|
self.load_answers(answers_save_path)
|
54
54
|
else:
|
ripple_down_rules/helpers.py
CHANGED
@@ -2,15 +2,14 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import os
|
4
4
|
from types import ModuleType
|
5
|
+
from typing import Tuple
|
5
6
|
|
6
|
-
from ripple_down_rules.datastructures.dataclasses import CaseFactoryMetaData
|
7
|
-
|
8
|
-
from .datastructures.case import create_case
|
9
|
-
from .datastructures.dataclasses import CaseQuery
|
10
7
|
from typing_extensions import Type, Optional, Callable, Any, Dict, TYPE_CHECKING, Union
|
11
8
|
|
12
|
-
from .
|
9
|
+
from .datastructures.case import create_case, Case
|
10
|
+
from .datastructures.dataclasses import CaseQuery
|
13
11
|
from .utils import calculate_precision_and_recall
|
12
|
+
from .utils import get_func_rdr_model_name, copy_case, make_set, update_case
|
14
13
|
|
15
14
|
if TYPE_CHECKING:
|
16
15
|
from .rdr import RippleDownRules
|
@@ -55,12 +54,14 @@ def general_rdr_classify(classifiers_dict: Dict[str, Union[ModuleType, RippleDow
|
|
55
54
|
if attribute_name in new_conclusions:
|
56
55
|
temp_case_query = CaseQuery(case_cp, attribute_name, rdr.conclusion_type, rdr.mutually_exclusive)
|
57
56
|
update_case(temp_case_query, new_conclusions)
|
58
|
-
if len(new_conclusions) == 0 or len(classifiers_dict) == 1 and list(classifiers_dict.values())[
|
57
|
+
if len(new_conclusions) == 0 or len(classifiers_dict) == 1 and list(classifiers_dict.values())[
|
58
|
+
0].mutually_exclusive:
|
59
59
|
break
|
60
60
|
return conclusions
|
61
61
|
|
62
62
|
|
63
|
-
def is_matching(classifier: Callable[[Any], Any], case_query: CaseQuery,
|
63
|
+
def is_matching(classifier: Callable[[Any], Any], case_query: CaseQuery,
|
64
|
+
pred_cat: Optional[Dict[str, Any]] = None) -> bool:
|
64
65
|
"""
|
65
66
|
:param classifier: The RDR classifier to check the prediction of.
|
66
67
|
:param case_query: The case query to check.
|
@@ -95,3 +96,23 @@ def load_or_create_func_rdr_model(func, model_dir: str, rdr_type: Type[RippleDow
|
|
95
96
|
else:
|
96
97
|
rdr = rdr_type(**rdr_kwargs)
|
97
98
|
return rdr
|
99
|
+
|
100
|
+
|
101
|
+
def get_an_updated_case_copy(case: Case, conclusion: Callable, attribute_name: str, conclusion_type: Tuple[Type, ...],
|
102
|
+
mutually_exclusive: bool) -> Case:
|
103
|
+
"""
|
104
|
+
:param case: The case to copy and update.
|
105
|
+
:param conclusion: The conclusion to add to the case.
|
106
|
+
:param attribute_name: The name of the attribute to update.
|
107
|
+
:param conclusion_type: The type of the conclusion to update.
|
108
|
+
:param mutually_exclusive: Whether the rule belongs to a mutually exclusive RDR.
|
109
|
+
:return: A copy of the case updated with the given conclusion.
|
110
|
+
"""
|
111
|
+
case_cp = copy_case(case)
|
112
|
+
temp_case_query = CaseQuery(case_cp, attribute_name, conclusion_type,
|
113
|
+
mutually_exclusive=mutually_exclusive)
|
114
|
+
output = conclusion(case_cp)
|
115
|
+
if not isinstance(output, Dict):
|
116
|
+
output = {attribute_name: output}
|
117
|
+
update_case(temp_case_query, output)
|
118
|
+
return case_cp
|
ripple_down_rules/rdr.py
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import importlib
|
4
|
+
import json
|
4
5
|
import os
|
5
6
|
from abc import ABC, abstractmethod
|
6
7
|
from copy import copy
|
7
8
|
from dataclasses import is_dataclass
|
8
9
|
from types import NoneType
|
9
|
-
import json
|
10
10
|
|
11
11
|
from ripple_down_rules.datastructures.dataclasses import CaseFactoryMetaData
|
12
12
|
from . import logger
|
@@ -29,16 +29,17 @@ from .datastructures.case import Case, CaseAttribute, create_case
|
|
29
29
|
from .datastructures.dataclasses import CaseQuery
|
30
30
|
from .datastructures.enums import MCRDRMode
|
31
31
|
from .experts import Expert, Human
|
32
|
-
from .helpers import is_matching, general_rdr_classify
|
33
|
-
from .rules import Rule, SingleClassRule, MultiClassTopRule, MultiClassStopRule
|
32
|
+
from .helpers import is_matching, general_rdr_classify, get_an_updated_case_copy
|
33
|
+
from .rules import Rule, SingleClassRule, MultiClassTopRule, MultiClassStopRule, MultiClassRefinementRule, \
|
34
|
+
MultiClassFilterRule
|
34
35
|
|
35
36
|
try:
|
36
37
|
from .user_interface.gui import RDRCaseViewer
|
37
38
|
except ImportError as e:
|
38
39
|
RDRCaseViewer = None
|
39
40
|
from .utils import draw_tree, make_set, SubclassJSONSerializer, make_list, get_type_from_string, \
|
40
|
-
|
41
|
-
is_iterable, str_to_snake_case, get_import_path_from_path, get_imports_from_types, render_tree
|
41
|
+
is_value_conflicting, extract_function_source, extract_imports, get_full_class_name, \
|
42
|
+
is_iterable, str_to_snake_case, get_import_path_from_path, get_imports_from_types, render_tree
|
42
43
|
|
43
44
|
|
44
45
|
class RippleDownRules(SubclassJSONSerializer, ABC):
|
@@ -500,25 +501,51 @@ class RDRWithCodeWriter(RippleDownRules, ABC):
|
|
500
501
|
conclusion_func_names = [f'conclusion_{rid}' for rid in rules_dict.keys()
|
501
502
|
if not isinstance(rules_dict[rid], MultiClassStopRule)]
|
502
503
|
all_func_names = condition_func_names + conclusion_func_names
|
504
|
+
rule_tree_file_path = f"{model_dir}/{self.generated_python_file_name}.py"
|
503
505
|
filepath = f"{model_dir}/{self.generated_python_defs_file_name}.py"
|
504
506
|
cases_path = f"{model_dir}/{self.generated_python_cases_file_name}.py"
|
505
507
|
cases_import_path = get_import_path_from_path(model_dir)
|
506
508
|
cases_import_path = f"{cases_import_path}.{self.generated_python_cases_file_name}" if cases_import_path \
|
507
509
|
else self.generated_python_cases_file_name
|
508
510
|
functions_source = extract_function_source(filepath, all_func_names, include_signature=False)
|
511
|
+
python_rule_tree_source = ""
|
512
|
+
with open(rule_tree_file_path, "r") as rule_tree_source:
|
513
|
+
python_rule_tree_source = rule_tree_source.read()
|
509
514
|
# get the scope from the imports in the file
|
510
515
|
scope = extract_imports(filepath, package_name=package_name)
|
516
|
+
rules_not_found = set()
|
511
517
|
for rule in [self.start_rule] + list(self.start_rule.descendants):
|
512
518
|
if rule.conditions is not None:
|
513
|
-
|
519
|
+
conditions_name = rule.generated_conditions_function_name
|
520
|
+
if conditions_name not in functions_source or conditions_name not in python_rule_tree_source:
|
521
|
+
rules_not_found.add(rule)
|
522
|
+
continue
|
523
|
+
rule.conditions.user_input = functions_source[conditions_name]
|
514
524
|
rule.conditions.scope = scope
|
515
525
|
if os.path.exists(cases_path):
|
516
526
|
module = importlib.import_module(cases_import_path, package=package_name)
|
517
527
|
importlib.reload(module)
|
518
528
|
rule.corner_case_metadata = module.__dict__.get(f"corner_case_{rule.uid}", None)
|
519
|
-
if
|
520
|
-
|
529
|
+
if not isinstance(rule, MultiClassStopRule):
|
530
|
+
conclusion_name = rule.generated_conclusion_function_name
|
531
|
+
if conclusion_name not in functions_source or conclusion_name not in python_rule_tree_source:
|
532
|
+
rules_not_found.add(rule)
|
533
|
+
rule.conclusion.user_input = functions_source[conclusion_name]
|
521
534
|
rule.conclusion.scope = scope
|
535
|
+
for rule in rules_not_found:
|
536
|
+
if isinstance(rule, MultiClassTopRule):
|
537
|
+
import pdb; pdb.set_trace()
|
538
|
+
rule.parent.set_immediate_alternative(rule.alternative)
|
539
|
+
if rule.refinement is not None:
|
540
|
+
ref_rules = [ref_rule for ref_rule in [rule.refinement] + list(rule.refinement.descendants)]
|
541
|
+
for ref_rule in ref_rules:
|
542
|
+
del ref_rule
|
543
|
+
else:
|
544
|
+
rule.parent.refinement = rule.alternative
|
545
|
+
if rule.alternative is not None:
|
546
|
+
rule.alternative = None
|
547
|
+
rule.parent = None
|
548
|
+
del rule
|
522
549
|
|
523
550
|
@abstractmethod
|
524
551
|
def write_rules_as_source_code_to_file(self, rule: Rule, file, parent_indent: str = "",
|
@@ -597,7 +624,7 @@ class RDRWithCodeWriter(RippleDownRules, ABC):
|
|
597
624
|
"""
|
598
625
|
pass
|
599
626
|
|
600
|
-
def _get_types_to_import(self) -> Tuple[Set[Type], Set[Type], Set[Type]]:
|
627
|
+
def _get_types_to_import(self) -> Tuple[Set[Union[Type, Callable]], Set[Type], Set[Type]]:
|
601
628
|
"""
|
602
629
|
:return: The types of the main, defs, and corner cases files of the RDR classifier that will be imported.
|
603
630
|
"""
|
@@ -930,6 +957,9 @@ class MultiClassRDR(RDRWithCodeWriter):
|
|
930
957
|
if rule.alternative:
|
931
958
|
self.write_rules_as_source_code_to_file(rule.alternative, filename, parent_indent, defs_file=defs_file,
|
932
959
|
cases_file=cases_file, package_name=package_name)
|
960
|
+
elif isinstance(rule, MultiClassTopRule):
|
961
|
+
with open(filename, "a") as file:
|
962
|
+
file.write(f"{parent_indent}return conclusions\n")
|
933
963
|
|
934
964
|
@property
|
935
965
|
def conclusion_type_hint(self) -> str:
|
@@ -939,8 +969,9 @@ class MultiClassRDR(RDRWithCodeWriter):
|
|
939
969
|
else:
|
940
970
|
return f"Set[Union[{', '.join(conclusion_types)}]]"
|
941
971
|
|
942
|
-
def _get_types_to_import(self) -> Tuple[Set[Type], Set[Type], Set[Type]]:
|
972
|
+
def _get_types_to_import(self) -> Tuple[Set[Union[Type, Callable]], Set[Type], Set[Type]]:
|
943
973
|
main_types, defs_types, cases_types = super()._get_types_to_import()
|
974
|
+
main_types.add(get_an_updated_case_copy)
|
944
975
|
main_types.update({Set, make_set})
|
945
976
|
defs_types.update({List, Set})
|
946
977
|
return main_types, defs_types, cases_types
|
@@ -972,28 +1003,43 @@ class MultiClassRDR(RDRWithCodeWriter):
|
|
972
1003
|
Stop a wrong conclusion by adding a stopping rule.
|
973
1004
|
"""
|
974
1005
|
rule_conclusion = evaluated_rule.conclusion(case_query.case)
|
975
|
-
|
976
|
-
|
977
|
-
|
1006
|
+
stop: bool = False
|
1007
|
+
add_filter_rule: bool = False
|
1008
|
+
if is_value_conflicting(rule_conclusion, case_query.target_value):
|
1009
|
+
if make_set(case_query.target_value).issubset(rule_conclusion):
|
1010
|
+
add_filter_rule = True
|
1011
|
+
else:
|
1012
|
+
stop = True
|
1013
|
+
elif make_set(case_query.core_attribute_type).issubset(make_set(evaluated_rule.conclusion.conclusion_type)):
|
1014
|
+
if make_set(case_query.target_value).issubset(rule_conclusion):
|
1015
|
+
add_filter_rule = True
|
1016
|
+
|
1017
|
+
if not stop:
|
978
1018
|
self.add_conclusion(rule_conclusion)
|
1019
|
+
if stop or add_filter_rule:
|
1020
|
+
refinement_type = MultiClassStopRule if stop else MultiClassFilterRule
|
1021
|
+
self.stop_or_filter_conclusion(case_query, expert, evaluated_rule, refinement_type=refinement_type)
|
979
1022
|
|
980
|
-
def
|
981
|
-
|
1023
|
+
def stop_or_filter_conclusion(self, case_query: CaseQuery,
|
1024
|
+
expert: Expert, evaluated_rule: MultiClassTopRule,
|
1025
|
+
refinement_type: Type[MultiClassRefinementRule] = MultiClassStopRule):
|
982
1026
|
"""
|
983
1027
|
Stop a conclusion by adding a stopping rule.
|
984
1028
|
|
985
1029
|
:param case_query: The case query to stop the conclusion for.
|
986
1030
|
:param expert: The expert to ask for differentiating features as new rule conditions.
|
987
1031
|
:param evaluated_rule: The evaluated rule to ask the expert about.
|
1032
|
+
:param refinement_type: The refinement type to use.
|
988
1033
|
"""
|
989
1034
|
conditions = expert.ask_for_conditions(case_query, evaluated_rule)
|
990
|
-
evaluated_rule.fit_rule(case_query)
|
991
|
-
if
|
992
|
-
self.
|
993
|
-
|
994
|
-
|
995
|
-
|
996
|
-
|
1035
|
+
evaluated_rule.fit_rule(case_query, refinement_type=refinement_type)
|
1036
|
+
if refinement_type is MultiClassStopRule:
|
1037
|
+
if self.mode == MCRDRMode.StopPlusRule:
|
1038
|
+
self.stop_rule_conditions = conditions
|
1039
|
+
if self.mode == MCRDRMode.StopPlusRuleCombined:
|
1040
|
+
new_top_rule_conditions = conditions.combine_with(evaluated_rule.conditions)
|
1041
|
+
case_query.conditions = new_top_rule_conditions
|
1042
|
+
self.add_top_rule(case_query)
|
997
1043
|
|
998
1044
|
def add_rule_for_case(self, case_query: CaseQuery, expert: Expert):
|
999
1045
|
"""
|
ripple_down_rules/rules.py
CHANGED
@@ -3,7 +3,6 @@ from __future__ import annotations
|
|
3
3
|
import logging
|
4
4
|
import re
|
5
5
|
from abc import ABC, abstractmethod
|
6
|
-
from pathlib import Path
|
7
6
|
from types import NoneType
|
8
7
|
from uuid import uuid4
|
9
8
|
|
@@ -15,7 +14,8 @@ from .datastructures.callable_expression import CallableExpression
|
|
15
14
|
from .datastructures.case import Case
|
16
15
|
from .datastructures.dataclasses import CaseFactoryMetaData, CaseQuery
|
17
16
|
from .datastructures.enums import RDREdge, Stop
|
18
|
-
from .utils import SubclassJSONSerializer, conclusion_to_json, get_full_class_name,
|
17
|
+
from .utils import SubclassJSONSerializer, conclusion_to_json, get_full_class_name, get_type_from_string
|
18
|
+
from .helpers import get_an_updated_case_copy
|
19
19
|
|
20
20
|
|
21
21
|
class Rule(NodeMixin, SubclassJSONSerializer, ABC):
|
@@ -23,6 +23,10 @@ class Rule(NodeMixin, SubclassJSONSerializer, ABC):
|
|
23
23
|
"""
|
24
24
|
Whether the rule has fired or not.
|
25
25
|
"""
|
26
|
+
mutually_exclusive: bool
|
27
|
+
"""
|
28
|
+
Whether the rule is mutually exclusive with other rules.
|
29
|
+
"""
|
26
30
|
|
27
31
|
def __init__(self, conditions: Optional[CallableExpression] = None,
|
28
32
|
conclusion: Optional[CallableExpression] = None,
|
@@ -60,6 +64,14 @@ class Rule(NodeMixin, SubclassJSONSerializer, ABC):
|
|
60
64
|
self.evaluated: bool = False
|
61
65
|
self._user_defined_name: Optional[str] = None
|
62
66
|
|
67
|
+
def get_an_updated_case_copy(self, case: Case) -> Case:
|
68
|
+
"""
|
69
|
+
:param case: The case to copy and update.
|
70
|
+
:return: A copy of the case updated with this rule conclusion.
|
71
|
+
"""
|
72
|
+
return get_an_updated_case_copy(case, self.conclusion, self.conclusion_name, self.conclusion.conclusion_type,
|
73
|
+
self.mutually_exclusive)
|
74
|
+
|
63
75
|
@property
|
64
76
|
def color(self) -> str:
|
65
77
|
if self.evaluated:
|
@@ -78,22 +90,27 @@ class Rule(NodeMixin, SubclassJSONSerializer, ABC):
|
|
78
90
|
if self._user_defined_name is None:
|
79
91
|
if self.conditions and self.conditions.user_input and "def " in self.conditions.user_input:
|
80
92
|
# If the conditions have a user input, use it as the name
|
81
|
-
|
93
|
+
func_name = self.conditions.user_input.split('(')[0].replace('def ', '').strip()
|
94
|
+
if func_name == self.conditions.encapsulating_function_name:
|
95
|
+
self._user_defined_name = str(self.conditions)
|
96
|
+
else:
|
97
|
+
self._user_defined_name = func_name
|
82
98
|
else:
|
83
99
|
self._user_defined_name = f"Rule_{self.uid}"
|
84
100
|
return self._user_defined_name
|
85
101
|
|
86
102
|
@classmethod
|
87
|
-
def from_case_query(cls, case_query: CaseQuery) -> Rule:
|
103
|
+
def from_case_query(cls, case_query: CaseQuery, parent: Optional[Rule] = None) -> Rule:
|
88
104
|
"""
|
89
105
|
Create a SingleClassRule from a CaseQuery.
|
90
106
|
|
91
107
|
:param case_query: The CaseQuery to create the rule from.
|
108
|
+
:param parent: The parent rule of this rule.
|
92
109
|
:return: A SingleClassRule instance.
|
93
110
|
"""
|
94
111
|
corner_case_metadata = CaseFactoryMetaData.from_case_query(case_query)
|
95
112
|
return cls(conditions=case_query.conditions, conclusion=case_query.target,
|
96
|
-
corner_case=case_query.case, parent=
|
113
|
+
corner_case=case_query.case, parent=parent,
|
97
114
|
corner_case_metadata=corner_case_metadata,
|
98
115
|
conclusion_name=case_query.attribute_name)
|
99
116
|
|
@@ -175,6 +192,14 @@ class Rule(NodeMixin, SubclassJSONSerializer, ABC):
|
|
175
192
|
f.write(conclusion_func.strip() + "\n\n\n")
|
176
193
|
return conclusion_func_call
|
177
194
|
|
195
|
+
@property
|
196
|
+
def generated_conclusion_function_name(self) -> str:
|
197
|
+
return f"conclusion_{self.uid}"
|
198
|
+
|
199
|
+
@property
|
200
|
+
def generated_conditions_function_name(self) -> str:
|
201
|
+
return f"conditions_{self.uid}"
|
202
|
+
|
178
203
|
def get_conclusion_as_source_code(self, conclusion: Any, parent_indent: str = "") -> Tuple[Optional[str], str]:
|
179
204
|
"""
|
180
205
|
Convert the conclusion of a rule to source code.
|
@@ -187,23 +212,24 @@ class Rule(NodeMixin, SubclassJSONSerializer, ABC):
|
|
187
212
|
# This means the conclusion is a definition that should be written and then called
|
188
213
|
conclusion_lines = conclusion.split('\n')
|
189
214
|
# use regex to replace the function name
|
190
|
-
new_function_name = f"def
|
215
|
+
new_function_name = f"def {self.generated_conclusion_function_name}"
|
191
216
|
conclusion_lines[0] = re.sub(r"def (\w+)", new_function_name, conclusion_lines[0])
|
192
217
|
# add type hint
|
193
|
-
if
|
194
|
-
|
218
|
+
if not self.conclusion.mutually_exclusive:
|
219
|
+
type_names = [t.__name__ for t in self.conclusion.conclusion_type if t not in [list, set]]
|
220
|
+
if len(type_names) == 1:
|
221
|
+
hint = f"List[{type_names[0]}]"
|
222
|
+
else:
|
223
|
+
hint = f"List[Union[{', '.join(type_names)}]]"
|
195
224
|
else:
|
196
|
-
if
|
197
|
-
|
198
|
-
|
199
|
-
|
225
|
+
if NoneType in self.conclusion.conclusion_type:
|
226
|
+
type_names = [t.__name__ for t in self.conclusion.conclusion_type if t is not NoneType]
|
227
|
+
hint = f"Optional[{', '.join(type_names)}]"
|
228
|
+
elif len(self.conclusion.conclusion_type) == 1:
|
229
|
+
hint = self.conclusion.conclusion_type[0].__name__
|
200
230
|
else:
|
201
|
-
|
202
|
-
|
203
|
-
hint = f"Optional[{', '.join(type_names)}]"
|
204
|
-
else:
|
205
|
-
type_names = [t.__name__ for t in self.conclusion.conclusion_type]
|
206
|
-
hint = f"Union[{', '.join(type_names)}]"
|
231
|
+
type_names = [t.__name__ for t in self.conclusion.conclusion_type]
|
232
|
+
hint = f"Union[{', '.join(type_names)}]"
|
207
233
|
conclusion_lines[0] = conclusion_lines[0].replace("):", f") -> {hint}:")
|
208
234
|
func_call = f"{parent_indent} return {new_function_name.replace('def ', '')}(case)\n"
|
209
235
|
return "\n".join(conclusion_lines).strip(' '), func_call
|
@@ -225,7 +251,7 @@ class Rule(NodeMixin, SubclassJSONSerializer, ABC):
|
|
225
251
|
# This means the conditions are a definition that should be written and then called
|
226
252
|
conditions_lines = self.conditions.user_input.split('\n')
|
227
253
|
# use regex to replace the function name
|
228
|
-
new_function_name = f"def
|
254
|
+
new_function_name = f"def {self.generated_conditions_function_name}"
|
229
255
|
conditions_lines[0] = re.sub(r"def (\w+)", new_function_name, conditions_lines[0])
|
230
256
|
# add type hint
|
231
257
|
conditions_lines[0] = conditions_lines[0].replace('):', ') -> bool:')
|
@@ -307,10 +333,11 @@ class Rule(NodeMixin, SubclassJSONSerializer, ABC):
|
|
307
333
|
Get the name of the expression, which is the user input of the expression if it exists,
|
308
334
|
otherwise it is the conclusion or conditions of the rule.
|
309
335
|
"""
|
310
|
-
if expression.user_defined_name is not None:
|
336
|
+
if expression.user_defined_name is not None and expression.user_defined_name != expression.encapsulating_function_name:
|
311
337
|
return expression.user_defined_name.strip()
|
312
|
-
|
313
|
-
|
338
|
+
func_name = expression.user_input.split('(')[0].replace('def ', '').strip() if "def " in expression.user_input else None
|
339
|
+
if func_name is not None and func_name != expression.encapsulating_function_name:
|
340
|
+
return func_name
|
314
341
|
elif expression.user_input:
|
315
342
|
return expression.user_input.strip()
|
316
343
|
else:
|
@@ -347,6 +374,9 @@ class HasAlternativeRule:
|
|
347
374
|
def alternative(self) -> Optional[Rule]:
|
348
375
|
return self._alternative
|
349
376
|
|
377
|
+
def set_immediate_alternative(self, alternative: Optional[Rule]):
|
378
|
+
self._alternative = alternative
|
379
|
+
|
350
380
|
@alternative.setter
|
351
381
|
def alternative(self, new_rule: Rule):
|
352
382
|
"""
|
@@ -382,12 +412,11 @@ class HasRefinementRule:
|
|
382
412
|
"""
|
383
413
|
if new_rule is None:
|
384
414
|
return
|
385
|
-
new_rule.top_rule = self
|
386
415
|
if self.refinement:
|
387
416
|
self.refinement.alternative = new_rule
|
388
417
|
else:
|
389
418
|
new_rule.parent = self
|
390
|
-
new_rule.weight = RDREdge.Refinement.value
|
419
|
+
new_rule.weight = RDREdge.Refinement.value if not isinstance(new_rule, MultiClassFilterRule) else new_rule.weight
|
391
420
|
self._refinement = new_rule
|
392
421
|
|
393
422
|
|
@@ -396,6 +425,8 @@ class SingleClassRule(Rule, HasAlternativeRule, HasRefinementRule):
|
|
396
425
|
A rule in the SingleClassRDR classifier, it can have a refinement or an alternative rule or both.
|
397
426
|
"""
|
398
427
|
|
428
|
+
mutually_exclusive: bool = True
|
429
|
+
|
399
430
|
def evaluate_next_rule(self, x: Case) -> SingleClassRule:
|
400
431
|
if self.fired:
|
401
432
|
returned_rule = self.refinement(x) if self.refinement else self
|
@@ -431,28 +462,15 @@ class SingleClassRule(Rule, HasAlternativeRule, HasRefinementRule):
|
|
431
462
|
return "elif" if self.weight == RDREdge.Alternative.value else "if"
|
432
463
|
|
433
464
|
|
434
|
-
class
|
465
|
+
class MultiClassRefinementRule(Rule, HasAlternativeRule, ABC):
|
435
466
|
"""
|
436
|
-
A rule in the MultiClassRDR classifier, it can have an alternative rule and a top rule
|
437
|
-
the conclusion of the rule is a Stop category meant to stop the parent conclusion from being made.
|
467
|
+
A rule in the MultiClassRDR classifier, it can have an alternative rule and a top rule.
|
438
468
|
"""
|
439
469
|
top_rule: Optional[MultiClassTopRule] = None
|
440
470
|
"""
|
441
471
|
The top rule of the rule, which is the nearest ancestor that fired and this rule is a refinement of.
|
442
472
|
"""
|
443
|
-
|
444
|
-
def __init__(self, *args, **kwargs):
|
445
|
-
super(MultiClassStopRule, self).__init__(*args, **kwargs)
|
446
|
-
self.conclusion = CallableExpression(conclusion_type=(Stop,), conclusion=Stop.stop)
|
447
|
-
|
448
|
-
def evaluate_next_rule(self, x: Case) -> Optional[Union[MultiClassStopRule, MultiClassTopRule]]:
|
449
|
-
if self.fired:
|
450
|
-
self.top_rule.fired = False
|
451
|
-
return self.top_rule.alternative
|
452
|
-
elif self.alternative:
|
453
|
-
return self.alternative(x)
|
454
|
-
else:
|
455
|
-
return self.top_rule.alternative
|
473
|
+
mutually_exclusive: bool = False
|
456
474
|
|
457
475
|
def _to_json(self) -> Dict[str, Any]:
|
458
476
|
self.json_serialization = {**Rule._to_json(self),
|
@@ -460,27 +478,106 @@ class MultiClassStopRule(Rule, HasAlternativeRule):
|
|
460
478
|
return self.json_serialization
|
461
479
|
|
462
480
|
@classmethod
|
463
|
-
def _from_json(cls, data: Dict[str, Any]) ->
|
464
|
-
loaded_rule = super(
|
481
|
+
def _from_json(cls, data: Dict[str, Any]) -> MultiClassRefinementRule:
|
482
|
+
loaded_rule = super(MultiClassRefinementRule, cls)._from_json(data)
|
465
483
|
# The following is done to prevent re-initialization of the top rule,
|
466
484
|
# so the top rule that is already initialized is passed in the data instead of its json serialization.
|
467
485
|
loaded_rule.top_rule = data['top_rule']
|
468
486
|
if data['alternative'] is not None:
|
469
487
|
data['alternative']['top_rule'] = data['top_rule']
|
470
|
-
|
488
|
+
loaded_rule.alternative = SubclassJSONSerializer.from_json(data["alternative"])
|
471
489
|
return loaded_rule
|
472
490
|
|
491
|
+
def _if_statement_source_code_clause(self) -> str:
|
492
|
+
return "elif" if self.weight == RDREdge.Alternative.value else "if"
|
493
|
+
|
494
|
+
|
495
|
+
class MultiClassStopRule(MultiClassRefinementRule):
|
496
|
+
"""
|
497
|
+
A rule in the MultiClassRDR classifier, it can have an alternative rule and a top rule,
|
498
|
+
the conclusion of the rule is a Stop category meant to stop the parent conclusion from being made.
|
499
|
+
"""
|
500
|
+
|
501
|
+
def __init__(self, *args, **kwargs):
|
502
|
+
super(MultiClassRefinementRule, self).__init__(*args, **kwargs)
|
503
|
+
self.conclusion = CallableExpression(conclusion_type=(Stop,), conclusion=Stop.stop)
|
504
|
+
|
505
|
+
def evaluate_next_rule(self, x: Case) -> Optional[Union[MultiClassRefinementRule, MultiClassTopRule]]:
|
506
|
+
if self.fired:
|
507
|
+
self.top_rule.fired = False
|
508
|
+
return self.top_rule.alternative
|
509
|
+
elif self.alternative:
|
510
|
+
return self.alternative(x)
|
511
|
+
else:
|
512
|
+
return self.top_rule.alternative
|
513
|
+
|
473
514
|
def get_conclusion_as_source_code(self, conclusion: Any, parent_indent: str = "") -> Tuple[None, str]:
|
474
515
|
return None, f"{parent_indent}{' ' * 4}pass\n"
|
475
516
|
|
476
|
-
|
477
|
-
|
517
|
+
|
518
|
+
class MultiClassFilterRule(MultiClassRefinementRule, HasRefinementRule):
|
519
|
+
"""
|
520
|
+
A rule in the MultiClassRDR classifier, it can have an alternative rule and a top rule,
|
521
|
+
the conclusion of the rule is a Filter category meant to filter the parent conclusion.
|
522
|
+
"""
|
523
|
+
|
524
|
+
def __init__(self, *args, **kwargs):
|
525
|
+
super(MultiClassRefinementRule, self).__init__(*args, **kwargs)
|
526
|
+
self.weight = RDREdge.Filter.value
|
527
|
+
|
528
|
+
def evaluate_next_rule(self, x: Case) -> Optional[Union[MultiClassRefinementRule, MultiClassTopRule]]:
|
529
|
+
if self.fired:
|
530
|
+
if self.refinement:
|
531
|
+
case_cp = x
|
532
|
+
if isinstance(self.refinement, MultiClassFilterRule):
|
533
|
+
case_cp = self.get_an_updated_case_copy(case_cp)
|
534
|
+
return self.refinement(case_cp)
|
535
|
+
else:
|
536
|
+
return self.top_rule.alternative
|
537
|
+
elif self.alternative:
|
538
|
+
return self.alternative(x)
|
539
|
+
else:
|
540
|
+
return self.top_rule.alternative
|
541
|
+
|
542
|
+
def get_conclusion_as_source_code(self, conclusion: Any, parent_indent: str = "") -> Tuple[None, str]:
|
543
|
+
func, func_call = super().get_conclusion_as_source_code(str(conclusion), parent_indent=parent_indent)
|
544
|
+
conclusion_str = func_call.replace("return ", "").strip()
|
545
|
+
conclusion_str = conclusion_str.replace("(case)", "(case_cp)")
|
546
|
+
|
547
|
+
parent_to_filter = self.get_parent_to_filter()
|
548
|
+
statement = (
|
549
|
+
f"\n{parent_indent} case_cp = get_an_updated_case_copy(case, {parent_to_filter.generated_conclusion_function_name},"
|
550
|
+
f" attribute_name, conclusion_type, mutually_exclusive)")
|
551
|
+
statement += f"\n{parent_indent} conclusions.update(make_set({conclusion_str}))\n"
|
552
|
+
return func, statement
|
553
|
+
|
554
|
+
def get_parent_to_filter(self, parent: Union[None, MultiClassRefinementRule, MultiClassTopRule] = None) \
|
555
|
+
-> Union[MultiClassFilterRule, MultiClassTopRule]:
|
556
|
+
parent = self.parent if parent is None else parent
|
557
|
+
if isinstance(parent, (MultiClassFilterRule, MultiClassTopRule)) and parent.fired:
|
558
|
+
return parent
|
559
|
+
else:
|
560
|
+
return parent.parent
|
561
|
+
|
562
|
+
def _to_json(self) -> Dict[str, Any]:
|
563
|
+
self.json_serialization = super(MultiClassFilterRule, self)._to_json()
|
564
|
+
self.json_serialization['refinement'] = self.refinement.to_json() if self.refinement is not None else None
|
565
|
+
return self.json_serialization
|
566
|
+
|
567
|
+
@classmethod
|
568
|
+
def _from_json(cls, data: Dict[str, Any]) -> MultiClassFilterRule:
|
569
|
+
loaded_rule = super(MultiClassFilterRule, cls)._from_json(data)
|
570
|
+
if data['refinement'] is not None:
|
571
|
+
data['refinement']['top_rule'] = data['top_rule']
|
572
|
+
loaded_rule.refinement = cls.from_json(data["refinement"]) if data["refinement"] is not None else None
|
573
|
+
return loaded_rule
|
478
574
|
|
479
575
|
|
480
576
|
class MultiClassTopRule(Rule, HasRefinementRule, HasAlternativeRule):
|
481
577
|
"""
|
482
578
|
A rule in the MultiClassRDR classifier, it can have a refinement and a next rule.
|
483
579
|
"""
|
580
|
+
mutually_exclusive: bool = False
|
484
581
|
|
485
582
|
def __init__(self, *args, **kwargs):
|
486
583
|
super(MultiClassTopRule, self).__init__(*args, **kwargs)
|
@@ -488,16 +585,27 @@ class MultiClassTopRule(Rule, HasRefinementRule, HasAlternativeRule):
|
|
488
585
|
|
489
586
|
def evaluate_next_rule(self, x: Case) -> Optional[Union[MultiClassStopRule, MultiClassTopRule]]:
|
490
587
|
if self.fired and self.refinement:
|
491
|
-
|
588
|
+
case_cp = x
|
589
|
+
if isinstance(self.refinement, MultiClassFilterRule):
|
590
|
+
case_cp = self.get_an_updated_case_copy(case_cp)
|
591
|
+
return self.refinement(case_cp)
|
492
592
|
elif self.alternative: # Here alternative refers to next rule in MultiClassRDR
|
493
593
|
return self.alternative
|
594
|
+
return None
|
494
595
|
|
495
|
-
def fit_rule(self, case_query: CaseQuery):
|
596
|
+
def fit_rule(self, case_query: CaseQuery, refinement_type: Optional[Type[MultiClassRefinementRule]] = None):
|
496
597
|
if self.fired and case_query.target != self.conclusion:
|
497
|
-
|
598
|
+
if refinement_type in [None, MultiClassStopRule]:
|
599
|
+
new_rule = MultiClassStopRule(case_query.conditions, corner_case=case_query.case,
|
600
|
+
parent=self)
|
601
|
+
elif refinement_type is MultiClassFilterRule:
|
602
|
+
new_rule = MultiClassFilterRule.from_case_query(case_query, parent=self)
|
603
|
+
else:
|
604
|
+
raise ValueError(f"Unknown refinement type {refinement_type}")
|
605
|
+
new_rule.top_rule = self
|
606
|
+
self.refinement = new_rule
|
498
607
|
elif not self.fired:
|
499
|
-
self.alternative = MultiClassTopRule(case_query
|
500
|
-
corner_case=case_query.case, parent=self)
|
608
|
+
self.alternative = MultiClassTopRule.from_case_query(case_query, parent=self)
|
501
609
|
|
502
610
|
def _to_json(self) -> Dict[str, Any]:
|
503
611
|
self.json_serialization = {**Rule._to_json(self),
|
@@ -512,7 +620,8 @@ class MultiClassTopRule(Rule, HasRefinementRule, HasAlternativeRule):
|
|
512
620
|
# so the top rule that is already initialized is passed in the data instead of its json serialization.
|
513
621
|
if data['refinement'] is not None:
|
514
622
|
data['refinement']['top_rule'] = loaded_rule
|
515
|
-
|
623
|
+
data_type = get_type_from_string(data["refinement"]["_type"])
|
624
|
+
loaded_rule.refinement = data_type.from_json(data["refinement"])
|
516
625
|
loaded_rule.alternative = MultiClassTopRule.from_json(data["alternative"])
|
517
626
|
return loaded_rule
|
518
627
|
|
@@ -521,8 +630,6 @@ class MultiClassTopRule(Rule, HasRefinementRule, HasAlternativeRule):
|
|
521
630
|
conclusion_str = func_call.replace("return ", "").strip()
|
522
631
|
|
523
632
|
statement = f"{parent_indent} conclusions.update(make_set({conclusion_str}))\n"
|
524
|
-
if self.alternative is None:
|
525
|
-
statement += f"{parent_indent}return conclusions\n"
|
526
633
|
return func, statement
|
527
634
|
|
528
635
|
def _if_statement_source_code_clause(self) -> str:
|
@@ -50,6 +50,7 @@ class UserPrompt:
|
|
50
50
|
:return: A callable expression that takes a case and executes user expression on it.
|
51
51
|
"""
|
52
52
|
prev_user_input: Optional[str] = None
|
53
|
+
user_input_to_modify: Optional[str] = None
|
53
54
|
callable_expression: Optional[CallableExpression] = None
|
54
55
|
while True:
|
55
56
|
with self.shell_lock:
|
@@ -69,12 +70,14 @@ class UserPrompt:
|
|
69
70
|
conclusion_type = bool if prompt_for == PromptFor.Conditions else case_query.attribute_type
|
70
71
|
callable_expression = CallableExpression(user_input, conclusion_type, expression_tree=expression_tree,
|
71
72
|
scope=case_query.scope,
|
72
|
-
|
73
|
+
mutually_exclusive=case_query.mutually_exclusive)
|
73
74
|
try:
|
74
75
|
result = callable_expression(case_query.case)
|
75
|
-
if len(make_list(result)) == 0
|
76
|
+
if len(make_list(result)) == 0 and (user_input_to_modify is not None
|
77
|
+
and (prev_user_input != user_input_to_modify)):
|
78
|
+
user_input_to_modify = prev_user_input
|
76
79
|
self.print_func(f"{Fore.YELLOW}The given expression gave an empty result for case {case_query.name}."
|
77
|
-
f" Please modify!{Style.RESET_ALL}")
|
80
|
+
f" Please accept or modify!{Style.RESET_ALL}")
|
78
81
|
continue
|
79
82
|
break
|
80
83
|
except Exception as e:
|
ripple_down_rules/utils.py
CHANGED
@@ -8,6 +8,7 @@ import json
|
|
8
8
|
import logging
|
9
9
|
import os
|
10
10
|
import re
|
11
|
+
import shutil
|
11
12
|
import sys
|
12
13
|
import threading
|
13
14
|
import uuid
|
@@ -21,10 +22,10 @@ from subprocess import check_call
|
|
21
22
|
from tempfile import NamedTemporaryFile
|
22
23
|
from textwrap import dedent
|
23
24
|
from types import NoneType
|
24
|
-
import shutil
|
25
25
|
|
26
26
|
import six
|
27
27
|
from sqlalchemy.exc import NoInspectionAvailable
|
28
|
+
from src.pycram.ros import logwarn
|
28
29
|
|
29
30
|
try:
|
30
31
|
import matplotlib
|
@@ -45,7 +46,6 @@ except ImportError as e:
|
|
45
46
|
|
46
47
|
import requests
|
47
48
|
from anytree import Node, RenderTree, PreOrderIter
|
48
|
-
from anytree.exporter import DotExporter
|
49
49
|
from sqlalchemy import MetaData, inspect
|
50
50
|
from sqlalchemy.orm import Mapped, registry, class_mapper, DeclarativeBase as SQLTable, Session
|
51
51
|
from tabulate import tabulate
|
@@ -182,8 +182,8 @@ def extract_function_source(file_path: str,
|
|
182
182
|
if (len(functions_source) >= len(function_names)) and (not len(function_names) == 0):
|
183
183
|
break
|
184
184
|
if len(functions_source) < len(function_names):
|
185
|
-
|
186
|
-
|
185
|
+
logwarn(f"Could not find all functions in {file_path}: {function_names} not found, "
|
186
|
+
f"functions not found: {set(function_names) - set(functions_source.keys())}")
|
187
187
|
if return_line_numbers:
|
188
188
|
return functions_source, line_numbers
|
189
189
|
return functions_source
|
@@ -287,7 +287,7 @@ def update_case(case_query: CaseQuery, conclusions: Dict[str, Any]):
|
|
287
287
|
case_query.case.update(conclusions)
|
288
288
|
|
289
289
|
|
290
|
-
def
|
290
|
+
def is_value_conflicting(conclusion: Any, target: Any) -> bool:
|
291
291
|
"""
|
292
292
|
:param conclusion: The conclusion to check.
|
293
293
|
:param target: The target to compare the conclusion with.
|
@@ -847,10 +847,12 @@ def get_relative_import(target_file_path, imported_module_path: Optional[str] =
|
|
847
847
|
imported_file_name = Path(imported_module_path).name
|
848
848
|
target_file_name = Path(target_file_path).name
|
849
849
|
if package_name is not None:
|
850
|
-
target_path = Path(
|
850
|
+
target_path = Path(
|
851
|
+
get_path_starting_from_latest_encounter_of(str(target_path), package_name, [target_file_name]))
|
851
852
|
imported_path = Path(imported_module_path).resolve()
|
852
853
|
if package_name is not None:
|
853
|
-
imported_path = Path(
|
854
|
+
imported_path = Path(
|
855
|
+
get_path_starting_from_latest_encounter_of(str(imported_path), package_name, [imported_file_name]))
|
854
856
|
|
855
857
|
# Compute relative path from target to imported module
|
856
858
|
rel_path = os.path.relpath(imported_path.parent, target_path.parent)
|
@@ -927,8 +929,8 @@ def get_imports_from_types(type_objs: Iterable[Type],
|
|
927
929
|
continue
|
928
930
|
if name == "NoneType":
|
929
931
|
module = "types"
|
930
|
-
if module is None or module == 'builtins' or module.startswith('_')\
|
931
|
-
|
932
|
+
if module is None or module == 'builtins' or module.startswith('_') \
|
933
|
+
or module in sys.builtin_module_names or module in excluded_modules or "<" in module \
|
932
934
|
or name in exclueded_names:
|
933
935
|
continue
|
934
936
|
if module == "typing":
|
@@ -1218,7 +1220,8 @@ class SubclassJSONSerializer:
|
|
1218
1220
|
return cls._from_json(data)
|
1219
1221
|
for subclass in recursive_subclasses(SubclassJSONSerializer):
|
1220
1222
|
if get_full_class_name(subclass) == data["_type"]:
|
1221
|
-
subclass_data = deepcopy(data)
|
1223
|
+
# subclass_data = deepcopy(data)
|
1224
|
+
subclass_data = data
|
1222
1225
|
subclass_data.pop("_type")
|
1223
1226
|
return subclass._from_json(subclass_data)
|
1224
1227
|
|
@@ -1414,7 +1417,7 @@ def table_rows_as_str(row_dicts: List[Dict[str, Any]], columns_per_row: int = 20
|
|
1414
1417
|
terminal_width = shutil.get_terminal_size((80, 20)).columns
|
1415
1418
|
# Step 2: Dynamically calculate max width per column (simple approximation)
|
1416
1419
|
max_col_width = terminal_width // len(row_values[0])
|
1417
|
-
table = tabulate(row_values, tablefmt='simple_grid', maxcolwidths=max_col_width)#[max_line_sze] * 2)
|
1420
|
+
table = tabulate(row_values, tablefmt='simple_grid', maxcolwidths=max_col_width) # [max_line_sze] * 2)
|
1418
1421
|
all_table_rows.append(table)
|
1419
1422
|
return "\n".join(all_table_rows)
|
1420
1423
|
|
@@ -1640,6 +1643,8 @@ def edge_attr_setter(parent, child):
|
|
1640
1643
|
|
1641
1644
|
|
1642
1645
|
_RE_ESC = re.compile(r'["\\]')
|
1646
|
+
|
1647
|
+
|
1643
1648
|
class FilteredDotExporter(object):
|
1644
1649
|
|
1645
1650
|
def __init__(self, node, include_nodes=None, graph="digraph", name="tree", options=None,
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: ripple_down_rules
|
3
|
-
Version: 0.6.
|
3
|
+
Version: 0.6.25
|
4
4
|
Summary: Implements the various versions of Ripple Down Rules (RDR) for knowledge representation and reasoning.
|
5
5
|
Author-email: Abdelrhman Bassiouny <abassiou@uni-bremen.de>
|
6
6
|
License: GNU GENERAL PUBLIC LICENSE
|
@@ -0,0 +1,24 @@
|
|
1
|
+
ripple_down_rules/__init__.py,sha256=hhuChAaI4pksG6FgW6-mLaJhifqkw_2Dy21ccjHfIFs,100
|
2
|
+
ripple_down_rules/experts.py,sha256=MYK1-vuvU1Stp82YZa8TcwOzvriIiYb0WrPFpWUNnXc,13005
|
3
|
+
ripple_down_rules/helpers.py,sha256=X1psHOqrb4_xYN4ssQNB8S9aRKKsqgihAyWJurN0dqk,5499
|
4
|
+
ripple_down_rules/rdr.py,sha256=avufEkijvJQCji1S5O1zaDSS7aIoP7Yjefy_4uK1TII,62125
|
5
|
+
ripple_down_rules/rdr_decorators.py,sha256=TRhbaB_ZIXN0n8Up19NI43_mMjmTm24qo8axAAOzbxM,11728
|
6
|
+
ripple_down_rules/rules.py,sha256=N4dEx-xyqxGZpoEYzRd9P5u97_DcDEVLY_UiNhZ4E7g,28726
|
7
|
+
ripple_down_rules/start-code-server.sh,sha256=otClk7VmDgBOX2TS_cjws6K0UwvgAUJhoA0ugkPCLqQ,949
|
8
|
+
ripple_down_rules/utils.py,sha256=CD-J33gKHUSGU-hZdVvlGtCvsuID7b1pirv5ejpkEP0,73891
|
9
|
+
ripple_down_rules/datastructures/__init__.py,sha256=V2aNgf5C96Y5-IGghra3n9uiefpoIm_QdT7cc_C8cxQ,111
|
10
|
+
ripple_down_rules/datastructures/callable_expression.py,sha256=P3o-z54Jt4rtIczeFWiuHFTNqMzYEOm94OyOP535D6Q,13378
|
11
|
+
ripple_down_rules/datastructures/case.py,sha256=PJ7_-AdxYic6BO5z816piFODj6nU5J6Jt1YzTFH-dds,15510
|
12
|
+
ripple_down_rules/datastructures/dataclasses.py,sha256=kI3Kv8GiVR8igMgA_BlKN6djUYxC2mLecvyh19pqQQA,10998
|
13
|
+
ripple_down_rules/datastructures/enums.py,sha256=R9AkhMKTDErOSZ8J6gEdh2lQ0Bjsxs22eMBtCPrXosI,5804
|
14
|
+
ripple_down_rules/user_interface/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
15
|
+
ripple_down_rules/user_interface/gui.py,sha256=druufu9cVeVUajPW-RqGW3ZiEbgdgNBQD2CLhadQo18,27486
|
16
|
+
ripple_down_rules/user_interface/ipython_custom_shell.py,sha256=yp-F8YRWGhj1PLB33HE6vJkdYWFN5Zn2244S2DUWRTM,6576
|
17
|
+
ripple_down_rules/user_interface/object_diagram.py,sha256=FEa2HaYR9QmTE6NsOwBvZ0jqmu3DKyg6mig2VE5ZP4Y,4956
|
18
|
+
ripple_down_rules/user_interface/prompt.py,sha256=e5FzVfiIagwKTK3WCKsHvWaWZ4kb8FP8X-SgieTln6E,9156
|
19
|
+
ripple_down_rules/user_interface/template_file_creator.py,sha256=kwBbFLyN6Yx2NTIHPSwOoytWgbJDYhgrUOVFw_jkDQ4,13522
|
20
|
+
ripple_down_rules-0.6.25.dist-info/licenses/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
|
21
|
+
ripple_down_rules-0.6.25.dist-info/METADATA,sha256=OCHGDEQ_jkpOq8iUIZONmvsMGXHPdsfzfuTSZOJUb_k,48294
|
22
|
+
ripple_down_rules-0.6.25.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
23
|
+
ripple_down_rules-0.6.25.dist-info/top_level.txt,sha256=VeoLhEhyK46M1OHwoPbCQLI1EifLjChqGzhQ6WEUqeM,18
|
24
|
+
ripple_down_rules-0.6.25.dist-info/RECORD,,
|
@@ -1,24 +0,0 @@
|
|
1
|
-
ripple_down_rules/__init__.py,sha256=jTt_MwLEVf7kkbt3WSvk31qBr2ijODPDLmZruci6_Yk,100
|
2
|
-
ripple_down_rules/experts.py,sha256=4-dMIVeMzFXCLYl_XBG_P7_Xs4sZih9-vZxCIPri6dA,12958
|
3
|
-
ripple_down_rules/helpers.py,sha256=RUdfiSWMZjGwCxuCy44TcEJf2UNAFlPJusgHzuAs6qI,4583
|
4
|
-
ripple_down_rules/rdr.py,sha256=tZkMThyj0leALxM_cXWZN_PyzM1FHiXjWl62EcgUOSU,59495
|
5
|
-
ripple_down_rules/rdr_decorators.py,sha256=TRhbaB_ZIXN0n8Up19NI43_mMjmTm24qo8axAAOzbxM,11728
|
6
|
-
ripple_down_rules/rules.py,sha256=z_-_oEaqJas_9DUYdNVIk5LVMXPsymYG5n51XJDzF-s,23641
|
7
|
-
ripple_down_rules/start-code-server.sh,sha256=otClk7VmDgBOX2TS_cjws6K0UwvgAUJhoA0ugkPCLqQ,949
|
8
|
-
ripple_down_rules/utils.py,sha256=p-gdR5KGvBOs2EK4PMRWol8zB87FQ7OkmYAMR23cy9g,73833
|
9
|
-
ripple_down_rules/datastructures/__init__.py,sha256=V2aNgf5C96Y5-IGghra3n9uiefpoIm_QdT7cc_C8cxQ,111
|
10
|
-
ripple_down_rules/datastructures/callable_expression.py,sha256=ysK-4JmZ4oSUTJC7zpo_o77g4ONxPDEcIpSWggsnx3c,13320
|
11
|
-
ripple_down_rules/datastructures/case.py,sha256=PJ7_-AdxYic6BO5z816piFODj6nU5J6Jt1YzTFH-dds,15510
|
12
|
-
ripple_down_rules/datastructures/dataclasses.py,sha256=D-nrVEW_27njmDGkyiHRnq5lmqEdO8RHKnLb1mdnwrA,11054
|
13
|
-
ripple_down_rules/datastructures/enums.py,sha256=ce7tqS0otfSTNAOwsnXlhsvIn4iW_Y_N3TNebF3YoZs,5700
|
14
|
-
ripple_down_rules/user_interface/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
15
|
-
ripple_down_rules/user_interface/gui.py,sha256=druufu9cVeVUajPW-RqGW3ZiEbgdgNBQD2CLhadQo18,27486
|
16
|
-
ripple_down_rules/user_interface/ipython_custom_shell.py,sha256=yp-F8YRWGhj1PLB33HE6vJkdYWFN5Zn2244S2DUWRTM,6576
|
17
|
-
ripple_down_rules/user_interface/object_diagram.py,sha256=FEa2HaYR9QmTE6NsOwBvZ0jqmu3DKyg6mig2VE5ZP4Y,4956
|
18
|
-
ripple_down_rules/user_interface/prompt.py,sha256=JceEUGYsd0lIvd-v2y3D3swoo96_C0lxfp3CxM7Vfts,8900
|
19
|
-
ripple_down_rules/user_interface/template_file_creator.py,sha256=kwBbFLyN6Yx2NTIHPSwOoytWgbJDYhgrUOVFw_jkDQ4,13522
|
20
|
-
ripple_down_rules-0.6.24.dist-info/licenses/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
|
21
|
-
ripple_down_rules-0.6.24.dist-info/METADATA,sha256=FwK1XSbEuJqEeKBJdlDBNFWnOZ_7EfPIPQmdX4mYkt4,48294
|
22
|
-
ripple_down_rules-0.6.24.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
23
|
-
ripple_down_rules-0.6.24.dist-info/top_level.txt,sha256=VeoLhEhyK46M1OHwoPbCQLI1EifLjChqGzhQ6WEUqeM,18
|
24
|
-
ripple_down_rules-0.6.24.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|