ripple-down-rules 0.6.14__py3-none-any.whl → 0.6.15__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.
@@ -1,4 +1,4 @@
1
- __version__ = "0.6.14"
1
+ __version__ = "0.6.15"
2
2
 
3
3
  import logging
4
4
  logger = logging.Logger("rdr")
@@ -116,6 +116,10 @@ class CallableExpression(SubclassJSONSerializer):
116
116
  if user_input is None:
117
117
  user_input = build_user_input_from_conclusion(conclusion)
118
118
  self.conclusion: Optional[Any] = conclusion
119
+ if "def " in user_input:
120
+ self.user_defined_name = user_input.split('(')[0].replace('def ', '')
121
+ else:
122
+ self.user_defined_name = user_input
119
123
  self._user_input: str = encapsulate_user_input(user_input, self.get_encapsulating_function())
120
124
  if conclusion_type is not None:
121
125
  if is_iterable(conclusion_type):
@@ -214,6 +218,10 @@ class CallableExpression(SubclassJSONSerializer):
214
218
  Set the user input.
215
219
  """
216
220
  if value is not None:
221
+ if "def " in value:
222
+ self.user_defined_name = value.split('(')[0].replace('def ', '')
223
+ else:
224
+ self.user_defined_name = value
217
225
  self._user_input = encapsulate_user_input(value, self.get_encapsulating_function())
218
226
  self.scope = get_used_scope(self.user_input, self.scope)
219
227
  self.expression_tree = parse_string_to_expression(self.user_input)
@@ -55,7 +55,7 @@ def general_rdr_classify(classifiers_dict: Dict[str, Union[ModuleType, RippleDow
55
55
  if attribute_name in new_conclusions:
56
56
  temp_case_query = CaseQuery(case_cp, attribute_name, rdr.conclusion_type, rdr.mutually_exclusive)
57
57
  update_case(temp_case_query, new_conclusions)
58
- if len(new_conclusions) == 0 or len(classifiers_dict) == 1:
58
+ if len(new_conclusions) == 0 or len(classifiers_dict) == 1 and list(classifiers_dict.values())[0].mutually_exclusive:
59
59
  break
60
60
  return conclusions
61
61
 
ripple_down_rules/rdr.py CHANGED
@@ -36,7 +36,7 @@ except ImportError as e:
36
36
  RDRCaseViewer = None
37
37
  from .utils import draw_tree, make_set, SubclassJSONSerializer, make_list, get_type_from_string, \
38
38
  is_conflicting, extract_function_source, extract_imports, get_full_class_name, \
39
- is_iterable, str_to_snake_case, get_import_path_from_path, get_imports_from_types
39
+ is_iterable, str_to_snake_case, get_import_path_from_path, get_imports_from_types, render_tree
40
40
 
41
41
 
42
42
  class RippleDownRules(SubclassJSONSerializer, ABC):
@@ -108,6 +108,23 @@ class RippleDownRules(SubclassJSONSerializer, ABC):
108
108
  if value is not None:
109
109
  value.set_save_function(self.save)
110
110
 
111
+ def render_evaluated_rule_tree(self, filename: str) -> None:
112
+ evaluated_rules = self.get_evaluated_rule_tree()
113
+ if len(evaluated_rules) > 0:
114
+ render_tree(evaluated_rules[0], use_dot_exporter=True, filename=filename,
115
+ only_nodes=evaluated_rules)
116
+
117
+ def get_evaluated_rule_tree(self) -> List[Rule]:
118
+ """
119
+ Get the evaluated rule tree of the classifier.
120
+
121
+ :return: The evaluated rule tree.
122
+ """
123
+ if self.start_rule is None:
124
+ raise ValueError("The start rule is not set. Please set the start rule before getting the evaluated rule tree.")
125
+ evaluated_rule_tree = [r for r in [self.start_rule] + list(self.start_rule.descendants) if r.evaluated]
126
+ return evaluated_rule_tree
127
+
111
128
  def save(self, save_dir: Optional[str] = None, model_name: Optional[str] = None,
112
129
  package_name: Optional[str] = None) -> str:
113
130
  """
@@ -67,7 +67,8 @@ class RDRDecorator:
67
67
  self.use_generated_classifier = use_generated_classifier
68
68
  self.generated_classifier: Optional[Callable] = None
69
69
  self.ask_now = ask_now
70
- self.fitting_decorator = fitting_decorator
70
+ self.fitting_decorator = fitting_decorator if fitting_decorator is not None else \
71
+ lambda f: f # Default to no fitting decorator
71
72
  self.load()
72
73
 
73
74
  def decorator(self, func: Callable) -> Callable:
@@ -57,6 +57,21 @@ class Rule(NodeMixin, SubclassJSONSerializer, ABC):
57
57
  self._name: Optional[str] = None
58
58
  # generate a unique id for the rule using uuid4
59
59
  self.uid: str = uid if uid else str(uuid4().int)
60
+ self.evaluated: bool = False
61
+ self._user_defined_name: Optional[str] = None
62
+
63
+ @property
64
+ def user_defined_name(self) -> Optional[str]:
65
+ """
66
+ Get the user defined name of the rule, if it exists.
67
+ """
68
+ if self._user_defined_name is None:
69
+ if self.conditions and self.conditions.user_input and "def " in self.conditions.user_input:
70
+ # If the conditions have a user input, use it as the name
71
+ self._user_defined_name = self.conditions.user_input.split('(')[0].replace('def ', '').strip()
72
+ else:
73
+ self._user_defined_name = f"Rule_{self.uid}"
74
+ return self._user_defined_name
60
75
 
61
76
  @classmethod
62
77
  def from_case_query(cls, case_query: CaseQuery) -> Rule:
@@ -91,6 +106,10 @@ class Rule(NodeMixin, SubclassJSONSerializer, ABC):
91
106
  :param x: The case to evaluate the rule on.
92
107
  :return: The rule that fired or the last evaluated rule if no rule fired.
93
108
  """
109
+ if self.root is self:
110
+ for descendant in self.descendants:
111
+ descendant.evaluated = False
112
+ self.evaluated = True
94
113
  if not self.conditions:
95
114
  raise ValueError("Rule has no conditions")
96
115
  self.fired = self.conditions(x)
@@ -261,11 +280,40 @@ class Rule(NodeMixin, SubclassJSONSerializer, ABC):
261
280
  """
262
281
  self._name = new_name
263
282
 
283
+ @property
284
+ def semantic_condition_name(self) -> Optional[str]:
285
+ """
286
+ Get the name of the conditions of the rule, which is the user input of the conditions.
287
+ """
288
+ return self.expression_name(self.conditions)
289
+
290
+ @property
291
+ def semantic_conclusion_name(self) -> Optional[str]:
292
+ """
293
+ Get the name of the conclusion of the rule, which is the user input of the conclusion.
294
+ """
295
+ return self.expression_name(self.conclusion)
296
+
297
+ @staticmethod
298
+ def expression_name(expression: CallableExpression) -> str:
299
+ """
300
+ Get the name of the expression, which is the user input of the expression if it exists,
301
+ otherwise it is the conclusion or conditions of the rule.
302
+ """
303
+ if expression.user_defined_name is not None:
304
+ return expression.user_defined_name.strip()
305
+ elif expression.user_input and "def " in expression.user_input:
306
+ return expression.user_input.split('(')[0].replace('def ', '').strip()
307
+ elif expression.user_input:
308
+ return expression.user_input.strip()
309
+ else:
310
+ return str(expression)
311
+
264
312
  def __str__(self, sep="\n"):
265
313
  """
266
314
  Get the string representation of the rule, which is the conditions and the conclusion.
267
315
  """
268
- return f"{self.conditions}{sep}=> {self.conclusion}"
316
+ return f"{self.semantic_condition_name}{sep}=> {self.semantic_conclusion_name}"
269
317
 
270
318
  def __repr__(self):
271
319
  return self.__str__()
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import ast
4
3
  import builtins
5
4
  import copyreg
6
5
  import importlib
@@ -22,10 +21,10 @@ from types import NoneType
22
21
 
23
22
  from sqlalchemy.exc import NoInspectionAvailable
24
23
 
25
-
26
24
  try:
27
25
  import matplotlib
28
26
  from matplotlib import pyplot as plt
27
+
29
28
  Figure = plt.Figure
30
29
  except ImportError as e:
31
30
  matplotlib = None
@@ -40,13 +39,13 @@ except ImportError as e:
40
39
  logging.debug(f"{e}: networkx is not installed")
41
40
 
42
41
  import requests
43
- from anytree import Node, RenderTree
42
+ from anytree import Node, RenderTree, PreOrderIter
44
43
  from anytree.exporter import DotExporter
45
44
  from sqlalchemy import MetaData, inspect
46
45
  from sqlalchemy.orm import Mapped, registry, class_mapper, DeclarativeBase as SQLTable, Session
47
46
  from tabulate import tabulate
48
47
  from typing_extensions import Callable, Set, Any, Type, Dict, TYPE_CHECKING, get_type_hints, \
49
- get_origin, get_args, Tuple, Optional, List, Union, Self, ForwardRef, Sequence, Iterable
48
+ get_origin, get_args, Tuple, Optional, List, Union, Self, ForwardRef, Iterable
50
49
 
51
50
  if TYPE_CHECKING:
52
51
  from .datastructures.case import Case
@@ -127,7 +126,7 @@ def extract_imports(file_path: Optional[str] = None, tree: Optional[ast.AST] = N
127
126
  name = alias.name
128
127
  asname = alias.asname or name
129
128
  try:
130
- if package_name is not None and node.level > 0: # Handle relative imports
129
+ if package_name is not None and node.level > 0: # Handle relative imports
131
130
  module_rel_path = Path(os.path.join(file_path, *['..'] * node.level, module_name)).resolve()
132
131
  idx = str(module_rel_path).rfind(package_name)
133
132
  if idx != -1:
@@ -681,6 +680,7 @@ def is_builtin_type(tp):
681
680
  def is_typing_type(tp):
682
681
  return tp.__module__ == "typing"
683
682
 
683
+
684
684
  origin_type_to_hint = {
685
685
  list: List,
686
686
  set: Set,
@@ -688,6 +688,7 @@ origin_type_to_hint = {
688
688
  tuple: Tuple,
689
689
  }
690
690
 
691
+
691
692
  def extract_types(tp, seen: Set = None) -> Set[type]:
692
693
  """Recursively extract all base types from a type hint."""
693
694
  if seen is None:
@@ -1400,7 +1401,8 @@ def table_rows_as_str(row_dicts: List[Dict[str, Any]], columns_per_row: int = 20
1400
1401
  keys_values = [list(r[0]) + list(r[1]) if len(r) > 1 else r[0] for r in keys_values]
1401
1402
  all_table_rows = []
1402
1403
  row_values = [list(map(lambda v: str(v) if v is not None else "", row)) for row in keys_values]
1403
- row_values = [list(map(lambda v: v[:max_line_sze] + '...' if len(v) > max_line_sze else v, row)) for row in row_values]
1404
+ row_values = [list(map(lambda v: v[:max_line_sze] + '...' if len(v) > max_line_sze else v, row)) for row in
1405
+ row_values]
1404
1406
  row_values = [list(map(lambda v: v.lower() if v in ["True", "False"] else v, row)) for row in row_values]
1405
1407
  table = tabulate(row_values, tablefmt='simple_grid', maxcolwidths=[max_line_sze] * 2)
1406
1408
  all_table_rows.append(table)
@@ -1627,14 +1629,46 @@ def edge_attr_setter(parent, child):
1627
1629
  return ""
1628
1630
 
1629
1631
 
1632
+ class FilteredDotExporter(DotExporter):
1633
+ def __init__(self, root, include_nodes=None, **kwargs):
1634
+ self.include_nodes = include_nodes
1635
+ node_name_func = get_unique_node_names_func(root)
1636
+ self.include_node_names = [node_name_func(n) for n in self.include_nodes] if include_nodes else None
1637
+ super().__init__(root, **kwargs)
1638
+
1639
+ def __iter_nodes(self, indent, nodenamefunc, nodeattrfunc):
1640
+ for node in PreOrderIter(self.node, maxlevel=self.maxlevel):
1641
+ nodename = nodenamefunc(node)
1642
+ if self.include_nodes is not None and nodename not in self.include_node_names:
1643
+ continue
1644
+ nodeattr = nodeattrfunc(node)
1645
+ nodeattr = " [%s]" % nodeattr if nodeattr is not None else ""
1646
+ yield '%s"%s"%s;' % (indent, DotExporter.esc(nodename), nodeattr)
1647
+
1648
+ def __iter_edges(self, indent, nodenamefunc, edgeattrfunc, edgetypefunc):
1649
+ maxlevel = self.maxlevel - 1 if self.maxlevel else None
1650
+ for node in PreOrderIter(self.node, maxlevel=maxlevel):
1651
+ nodename = nodenamefunc(node)
1652
+ if self.include_nodes is not None and nodename not in self.include_node_names:
1653
+ continue
1654
+ for child in node.children:
1655
+ childname = nodenamefunc(child)
1656
+ edgeattr = edgeattrfunc(node, child)
1657
+ edgetype = edgetypefunc(node, child)
1658
+ edgeattr = " [%s]" % edgeattr if edgeattr is not None else ""
1659
+ yield '%s"%s" %s "%s"%s;' % (indent, DotExporter.esc(nodename), edgetype,
1660
+ DotExporter.esc(childname), edgeattr)
1661
+
1662
+
1630
1663
  def render_tree(root: Node, use_dot_exporter: bool = False,
1631
- filename: str = "scrdr"):
1664
+ filename: str = "scrdr", only_nodes: List[Node] = None):
1632
1665
  """
1633
1666
  Render the tree using the console and optionally export it to a dot file.
1634
1667
 
1635
1668
  :param root: The root node of the tree.
1636
1669
  :param use_dot_exporter: Whether to export the tree to a dot file.
1637
1670
  :param filename: The name of the file to export the tree to.
1671
+ :param only_nodes: A list of nodes to include in the dot export.
1638
1672
  """
1639
1673
  if not root:
1640
1674
  logging.warning("No rules to render")
@@ -1644,10 +1678,11 @@ def render_tree(root: Node, use_dot_exporter: bool = False,
1644
1678
  if use_dot_exporter:
1645
1679
  unique_node_names = get_unique_node_names_func(root)
1646
1680
 
1647
- de = DotExporter(root,
1648
- nodenamefunc=unique_node_names,
1649
- edgeattrfunc=edge_attr_setter
1650
- )
1681
+ de = FilteredDotExporter(root,
1682
+ include_nodes=only_nodes,
1683
+ nodenamefunc=unique_node_names,
1684
+ edgeattrfunc=edge_attr_setter
1685
+ )
1651
1686
  de.to_dotfile(f"{filename}{'.dot'}")
1652
1687
  de.to_picture(f"{filename}{'.png'}")
1653
1688
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ripple_down_rules
3
- Version: 0.6.14
3
+ Version: 0.6.15
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
@@ -677,6 +677,7 @@ License: GNU GENERAL PUBLIC LICENSE
677
677
  the library. If this is what you want to do, use the GNU Lesser General
678
678
  Public License instead of this License. But first, please read
679
679
  <https://www.gnu.org/licenses/why-not-lgpl.html>.
680
+ Project-URL: Homepage, https://github.com/AbdelrhmanBassiouny/ripple_down_rules
680
681
  Keywords: robotics,knowledge,reasoning,representation
681
682
  Classifier: Programming Language :: Python :: 3
682
683
  Description-Content-Type: text/markdown
@@ -1,13 +1,13 @@
1
- ripple_down_rules/__init__.py,sha256=K_u_cwcMMPTstWbeVPYK6ibSHNdps5WcwSGTSimjTiE,100
1
+ ripple_down_rules/__init__.py,sha256=lX1XrdZ7llw3tZk6e_RhQszLZMG9KS8H7GA6V7n4DlI,100
2
2
  ripple_down_rules/experts.py,sha256=4-dMIVeMzFXCLYl_XBG_P7_Xs4sZih9-vZxCIPri6dA,12958
3
- ripple_down_rules/helpers.py,sha256=X-pYuCsmuNraUFcIL450AsaOdGvGjotNPgSx8yu3NMg,4525
4
- ripple_down_rules/rdr.py,sha256=x19ElNV-v67XkbSVLg9lp0xwo6PRNebBJa6dJSFCqhU,56755
5
- ripple_down_rules/rdr_decorators.py,sha256=zeDVGVSIRGnXLB2kmnZ2DXP7sF6RAH8tXaDoDY_QFWQ,10608
6
- ripple_down_rules/rules.py,sha256=Dk4yGCy5oV10mOv5rRLcmtIu9J60WBPol9b7ELFn6fY,21522
3
+ ripple_down_rules/helpers.py,sha256=RUdfiSWMZjGwCxuCy44TcEJf2UNAFlPJusgHzuAs6qI,4583
4
+ ripple_down_rules/rdr.py,sha256=mtY_kBOjUXRZYz4QFaKN6C2O7liFpbMdnjUnFFnXbdU,57550
5
+ ripple_down_rules/rdr_decorators.py,sha256=j_bIv30nonWz-D5em7dYnV3htlklar2QwFRa5o48XYM,10707
6
+ ripple_down_rules/rules.py,sha256=ULIHRbVfKd2L3zzPnssmaeqInWHifPvwwa16MyzakPc,23548
7
7
  ripple_down_rules/start-code-server.sh,sha256=otClk7VmDgBOX2TS_cjws6K0UwvgAUJhoA0ugkPCLqQ,949
8
- ripple_down_rules/utils.py,sha256=vFoz5HcRgkf7_s-zdWSg54xI9PCMcdDzcdFGdas7KBA,62350
8
+ ripple_down_rules/utils.py,sha256=uOa_b3FoDnoTLp84RnMbeSyiTMgREtUOkOImQj8B5HQ,64204
9
9
  ripple_down_rules/datastructures/__init__.py,sha256=V2aNgf5C96Y5-IGghra3n9uiefpoIm_QdT7cc_C8cxQ,111
10
- ripple_down_rules/datastructures/callable_expression.py,sha256=pslqv9igc7d6s1TCKdJSfIdU98_L1BPKNmfLVlGdGHs,12965
10
+ ripple_down_rules/datastructures/callable_expression.py,sha256=ysK-4JmZ4oSUTJC7zpo_o77g4ONxPDEcIpSWggsnx3c,13320
11
11
  ripple_down_rules/datastructures/case.py,sha256=PJ7_-AdxYic6BO5z816piFODj6nU5J6Jt1YzTFH-dds,15510
12
12
  ripple_down_rules/datastructures/dataclasses.py,sha256=D-nrVEW_27njmDGkyiHRnq5lmqEdO8RHKnLb1mdnwrA,11054
13
13
  ripple_down_rules/datastructures/enums.py,sha256=ce7tqS0otfSTNAOwsnXlhsvIn4iW_Y_N3TNebF3YoZs,5700
@@ -17,8 +17,8 @@ ripple_down_rules/user_interface/ipython_custom_shell.py,sha256=yp-F8YRWGhj1PLB3
17
17
  ripple_down_rules/user_interface/object_diagram.py,sha256=FEa2HaYR9QmTE6NsOwBvZ0jqmu3DKyg6mig2VE5ZP4Y,4956
18
18
  ripple_down_rules/user_interface/prompt.py,sha256=JceEUGYsd0lIvd-v2y3D3swoo96_C0lxfp3CxM7Vfts,8900
19
19
  ripple_down_rules/user_interface/template_file_creator.py,sha256=kwBbFLyN6Yx2NTIHPSwOoytWgbJDYhgrUOVFw_jkDQ4,13522
20
- ripple_down_rules-0.6.14.dist-info/licenses/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
21
- ripple_down_rules-0.6.14.dist-info/METADATA,sha256=fiq0Jj43sSuWmfUtVfaKScjdcokmDDzxH-XHNUIxXc0,48214
22
- ripple_down_rules-0.6.14.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
- ripple_down_rules-0.6.14.dist-info/top_level.txt,sha256=VeoLhEhyK46M1OHwoPbCQLI1EifLjChqGzhQ6WEUqeM,18
24
- ripple_down_rules-0.6.14.dist-info/RECORD,,
20
+ ripple_down_rules-0.6.15.dist-info/licenses/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
21
+ ripple_down_rules-0.6.15.dist-info/METADATA,sha256=2x_pdEry-CrkjQ1lgQXmfGhjdK5HScMZOSf16a_yUfE,48294
22
+ ripple_down_rules-0.6.15.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
+ ripple_down_rules-0.6.15.dist-info/top_level.txt,sha256=VeoLhEhyK46M1OHwoPbCQLI1EifLjChqGzhQ6WEUqeM,18
24
+ ripple_down_rules-0.6.15.dist-info/RECORD,,