ripple-down-rules 0.4.3__tar.gz → 0.4.4__tar.gz

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.
Files changed (40) hide show
  1. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/PKG-INFO +1 -1
  2. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/pyproject.toml +1 -1
  3. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/src/ripple_down_rules/experts.py +3 -2
  4. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/src/ripple_down_rules/rdr.py +59 -26
  5. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/src/ripple_down_rules/rules.py +9 -2
  6. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/src/ripple_down_rules/utils.py +2 -1
  7. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/src/ripple_down_rules.egg-info/PKG-INFO +1 -1
  8. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/src/ripple_down_rules.egg-info/SOURCES.txt +2 -1
  9. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/test/test_object_diagram.py +5 -5
  10. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/test/test_rdr_world.py +1 -1
  11. ripple_down_rules-0.4.4/test/test_utils.py +23 -0
  12. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/LICENSE +0 -0
  13. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/README.md +0 -0
  14. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/setup.cfg +0 -0
  15. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/src/ripple_down_rules/__init__.py +0 -0
  16. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/src/ripple_down_rules/datasets.py +0 -0
  17. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/src/ripple_down_rules/datastructures/__init__.py +0 -0
  18. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/src/ripple_down_rules/datastructures/callable_expression.py +0 -0
  19. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/src/ripple_down_rules/datastructures/case.py +0 -0
  20. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/src/ripple_down_rules/datastructures/dataclasses.py +0 -0
  21. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/src/ripple_down_rules/datastructures/enums.py +0 -0
  22. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/src/ripple_down_rules/failures.py +0 -0
  23. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/src/ripple_down_rules/helpers.py +0 -0
  24. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/src/ripple_down_rules/rdr_decorators.py +0 -0
  25. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/src/ripple_down_rules/user_interface/__init__.py +0 -0
  26. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/src/ripple_down_rules/user_interface/gui.py +0 -0
  27. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/src/ripple_down_rules/user_interface/ipython_custom_shell.py +0 -0
  28. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/src/ripple_down_rules/user_interface/object_diagram.py +0 -0
  29. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/src/ripple_down_rules/user_interface/prompt.py +0 -0
  30. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/src/ripple_down_rules/user_interface/template_file_creator.py +0 -0
  31. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/src/ripple_down_rules.egg-info/dependency_links.txt +0 -0
  32. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/src/ripple_down_rules.egg-info/top_level.txt +0 -0
  33. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/test/test_json_serialization.py +0 -0
  34. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/test/test_on_mutagenic.py +0 -0
  35. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/test/test_rdr.py +0 -0
  36. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/test/test_rdr_alchemy.py +0 -0
  37. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/test/test_rdr_decorators.py +0 -0
  38. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/test/test_relational_rdr.py +0 -0
  39. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/test/test_relational_rdr_alchemy.py +0 -0
  40. {ripple_down_rules-0.4.3 → ripple_down_rules-0.4.4}/test/test_sql_model.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ripple_down_rules
3
- Version: 0.4.3
3
+ Version: 0.4.4
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
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "ripple_down_rules"
9
- version = "0.4.3"
9
+ version = "0.4.4"
10
10
  description = "Implements the various versions of Ripple Down Rules (RDR) for knowledge representation and reasoning."
11
11
  readme = "README.md"
12
12
  authors = [{ name = "Abdelrhman Bassiouny", email = "abassiou@uni-bremen.de" }]
@@ -103,7 +103,7 @@ class Human(Expert):
103
103
  def ask_for_conditions(self, case_query: CaseQuery,
104
104
  last_evaluated_rule: Optional[Rule] = None) \
105
105
  -> CallableExpression:
106
- if not self.use_loaded_answers:
106
+ if not self.use_loaded_answers and self.user_prompt.viewer is None:
107
107
  show_current_and_corner_cases(case_query.case, {case_query.attribute_name: case_query.target_value},
108
108
  last_evaluated_rule=last_evaluated_rule)
109
109
  return self._get_conditions(case_query)
@@ -148,7 +148,8 @@ class Human(Expert):
148
148
  scope=case_query.scope,
149
149
  mutually_exclusive=case_query.mutually_exclusive)
150
150
  else:
151
- show_current_and_corner_cases(case_query.case)
151
+ if self.user_prompt.viewer is None:
152
+ show_current_and_corner_cases(case_query.case)
152
153
  expert_input, expression = self.user_prompt.prompt_user_for_expression(case_query, PromptFor.Conclusion)
153
154
  self.all_expert_answers.append(expert_input)
154
155
  case_query.target = expression
@@ -22,7 +22,7 @@ from .rules import Rule, SingleClassRule, MultiClassTopRule, MultiClassStopRule
22
22
  from .user_interface.gui import RDRCaseViewer
23
23
  from .utils import draw_tree, make_set, copy_case, \
24
24
  SubclassJSONSerializer, make_list, get_type_from_string, \
25
- is_conflicting, update_case, get_imports_from_scope, extract_function_source
25
+ is_conflicting, update_case, get_imports_from_scope, extract_function_source, extract_imports
26
26
 
27
27
 
28
28
  class RippleDownRules(SubclassJSONSerializer, ABC):
@@ -41,6 +41,10 @@ class RippleDownRules(SubclassJSONSerializer, ABC):
41
41
  """
42
42
  The name of the generated python file.
43
43
  """
44
+ name: Optional[str] = None
45
+ """
46
+ The name of the classifier.
47
+ """
44
48
 
45
49
  def __init__(self, start_rule: Optional[Rule] = None, viewer: Optional[RDRCaseViewer] = None):
46
50
  """
@@ -138,6 +142,7 @@ class RippleDownRules(SubclassJSONSerializer, ABC):
138
142
  """
139
143
  if case_query is None:
140
144
  raise ValueError("The case query cannot be None.")
145
+ self.name = case_query.attribute_name if self.name is None else self.name
141
146
  if case_query.target is None:
142
147
  case_query_cp = copy(case_query)
143
148
  self.classify(case_query_cp.case, modify_case=True)
@@ -237,11 +242,15 @@ class RDRWithCodeWriter(RippleDownRules, ABC):
237
242
  all_func_names = condition_func_names + conclusion_func_names
238
243
  filepath = f"{package_dir}/{self.generated_python_defs_file_name}.py"
239
244
  functions_source = extract_function_source(filepath, all_func_names, include_signature=False)
245
+ # get the scope from the imports in the file
246
+ scope = extract_imports(filepath)
240
247
  for rule in [self.start_rule] + list(self.start_rule.descendants):
241
248
  if rule.conditions is not None:
242
249
  rule.conditions.user_input = functions_source[f"conditions_{rule.uid}"]
250
+ rule.conditions.scope = scope
243
251
  if rule.conclusion is not None:
244
252
  rule.conclusion.user_input = functions_source[f"conclusion_{rule.uid}"]
253
+ rule.conclusion.scope = scope
245
254
 
246
255
  @abstractmethod
247
256
  def write_rules_as_source_code_to_file(self, rule: Rule, file, parent_indent: str = "",
@@ -275,6 +284,7 @@ class RDRWithCodeWriter(RippleDownRules, ABC):
275
284
  imports += f"from .{self.generated_python_defs_file_name} import *\n"
276
285
  imports += f"from ripple_down_rules.rdr import {self.__class__.__name__}\n"
277
286
  f.write(imports + "\n\n")
287
+ f.write(f"attribute_name = '{self.attribute_name}'\n")
278
288
  f.write(f"conclusion_type = ({', '.join([ct.__name__ for ct in self.conclusion_type])},)\n")
279
289
  f.write(f"type_ = {self.__class__.__name__}\n")
280
290
  f.write(f"\n\n{func_def}")
@@ -384,6 +394,31 @@ class RDRWithCodeWriter(RippleDownRules, ABC):
384
394
  """
385
395
  return self.start_rule.conclusion_name
386
396
 
397
+ def _to_json(self) -> Dict[str, Any]:
398
+ return {"start_rule": self.start_rule.to_json(), "generated_python_file_name": self.generated_python_file_name,
399
+ "name": self.name}
400
+
401
+ @classmethod
402
+ def _from_json(cls, data: Dict[str, Any]) -> Self:
403
+ """
404
+ Create an instance of the class from a json
405
+ """
406
+ start_rule = cls.start_rule_type().from_json(data["start_rule"])
407
+ new_rdr = cls(start_rule)
408
+ if "generated_python_file_name" in data:
409
+ new_rdr.generated_python_file_name = data["generated_python_file_name"]
410
+ if "name" in data:
411
+ new_rdr.name = data["name"]
412
+ return new_rdr
413
+
414
+ @staticmethod
415
+ @abstractmethod
416
+ def start_rule_type() -> Type[Rule]:
417
+ """
418
+ :return: The type of the starting rule of the RDR classifier.
419
+ """
420
+ pass
421
+
387
422
 
388
423
  class SingleClassRDR(RDRWithCodeWriter):
389
424
 
@@ -478,16 +513,12 @@ class SingleClassRDR(RDRWithCodeWriter):
478
513
  return (type(self.default_conclusion),)
479
514
  return super().conclusion_type
480
515
 
481
- def _to_json(self) -> Dict[str, Any]:
482
- return {"start_rule": self.start_rule.to_json()}
483
-
484
- @classmethod
485
- def _from_json(cls, data: Dict[str, Any]) -> Self:
516
+ @staticmethod
517
+ def start_rule_type() -> Type[Rule]:
486
518
  """
487
- Create an instance of the class from a json
519
+ :return: The type of the starting rule of the RDR classifier.
488
520
  """
489
- start_rule = SingleClassRule.from_json(data["start_rule"])
490
- return cls(start_rule)
521
+ return SingleClassRule
491
522
 
492
523
 
493
524
  class MultiClassRDR(RDRWithCodeWriter):
@@ -693,16 +724,12 @@ class MultiClassRDR(RDRWithCodeWriter):
693
724
  """
694
725
  self.start_rule.alternative = MultiClassTopRule(conditions, conclusion, corner_case=corner_case)
695
726
 
696
- def _to_json(self) -> Dict[str, Any]:
697
- return {"start_rule": self.start_rule.to_json()}
698
-
699
- @classmethod
700
- def _from_json(cls, data: Dict[str, Any]) -> Self:
727
+ @staticmethod
728
+ def start_rule_type() -> Type[Rule]:
701
729
  """
702
- Create an instance of the class from a json
730
+ :return: The type of the starting rule of the RDR classifier.
703
731
  """
704
- start_rule = SingleClassRule.from_json(data["start_rule"])
705
- return cls(start_rule)
732
+ return MultiClassTopRule
706
733
 
707
734
 
708
735
  class GeneralRDR(RippleDownRules):
@@ -733,15 +760,15 @@ class GeneralRDR(RippleDownRules):
733
760
  super(GeneralRDR, self).__init__(**kwargs)
734
761
  self.all_figs: List[plt.Figure] = [sr.fig for sr in self.start_rules_dict.values()]
735
762
 
736
- def add_rdr(self, rdr: Union[SingleClassRDR, MultiClassRDR], attribute_name: Optional[str] = None):
763
+ def add_rdr(self, rdr: Union[SingleClassRDR, MultiClassRDR], case_query: Optional[CaseQuery] = None):
737
764
  """
738
765
  Add a ripple down rules classifier to the map of classifiers.
739
766
 
740
767
  :param rdr: The ripple down rules classifier to add.
741
- :param attribute_name: The name of the attribute that the classifier is classifying.
768
+ :param case_query: The case query to add the classifier for.
742
769
  """
743
- attribute_name = attribute_name if attribute_name else rdr.attribute_name
744
- self.start_rules_dict[attribute_name] = rdr
770
+ name = case_query.attribute_name if case_query else rdr.name
771
+ self.start_rules_dict[name] = rdr
745
772
 
746
773
  @property
747
774
  def start_rule(self) -> Optional[Union[SingleClassRule, MultiClassTopRule]]:
@@ -825,7 +852,6 @@ class GeneralRDR(RippleDownRules):
825
852
  :param expert: The expert to ask for differentiating features as new rule conditions.
826
853
  :return: The categories that the case belongs to.
827
854
  """
828
-
829
855
  case_query_cp = copy(case_query)
830
856
  self.classify(case_query_cp.case, modify_case=True)
831
857
  case_query_cp.update_target_value()
@@ -843,7 +869,7 @@ class GeneralRDR(RippleDownRules):
843
869
  """
844
870
  if case_query.attribute_name not in self.start_rules_dict:
845
871
  new_rdr = self.initialize_new_rdr_for_attribute(case_query)
846
- self.add_rdr(new_rdr, case_query.attribute_name)
872
+ self.add_rdr(new_rdr, case_query)
847
873
 
848
874
  @staticmethod
849
875
  def initialize_new_rdr_for_attribute(case_query: CaseQuery):
@@ -854,7 +880,9 @@ class GeneralRDR(RippleDownRules):
854
880
  else MultiClassRDR()
855
881
 
856
882
  def _to_json(self) -> Dict[str, Any]:
857
- return {"start_rules": {t: rdr.to_json() for t, rdr in self.start_rules_dict.items()}}
883
+ return {"start_rules": {name: rdr.to_json() for name, rdr in self.start_rules_dict.items()}
884
+ , "generated_python_file_name": self.generated_python_file_name,
885
+ "name": self.name}
858
886
 
859
887
  @classmethod
860
888
  def _from_json(cls, data: Dict[str, Any]) -> GeneralRDR:
@@ -864,7 +892,12 @@ class GeneralRDR(RippleDownRules):
864
892
  start_rules_dict = {}
865
893
  for k, v in data["start_rules"].items():
866
894
  start_rules_dict[k] = get_type_from_string(v['_type']).from_json(v)
867
- return cls(start_rules_dict)
895
+ new_rdr = cls(start_rules_dict)
896
+ if "generated_python_file_name" in data:
897
+ new_rdr.generated_python_file_name = data["generated_python_file_name"]
898
+ if "name" in data:
899
+ new_rdr.name = data["name"]
900
+ return new_rdr
868
901
 
869
902
  def update_from_python_file(self, package_dir: str) -> None:
870
903
  """
@@ -921,7 +954,7 @@ class GeneralRDR(RippleDownRules):
921
954
  """
922
955
  :return: The default generated python file name.
923
956
  """
924
- if self.start_rule is None or self.start_rule.corner_case is None:
957
+ if self.start_rule is None or self.start_rule.conclusion is None:
925
958
  return None
926
959
  if isinstance(self.start_rule.corner_case, Case):
927
960
  name = self.start_rule.corner_case._name
@@ -5,6 +5,7 @@ from abc import ABC, abstractmethod
5
5
  from uuid import uuid4
6
6
 
7
7
  from anytree import NodeMixin
8
+ from rospy import logwarn, logdebug
8
9
  from sqlalchemy.orm import DeclarativeBase as SQLTable
9
10
  from typing_extensions import List, Optional, Self, Union, Dict, Any, Tuple
10
11
 
@@ -151,7 +152,8 @@ class Rule(NodeMixin, SubclassJSONSerializer, ABC):
151
152
  json_serialization = {"conditions": self.conditions.to_json(),
152
153
  "conclusion": conclusion_to_json(self.conclusion),
153
154
  "parent": self.parent.json_serialization if self.parent else None,
154
- "corner_case": SubclassJSONSerializer.to_json_static(self.corner_case),
155
+ "corner_case": SubclassJSONSerializer.to_json_static(self.corner_case)
156
+ if self.corner_case else None,
155
157
  "conclusion_name": self.conclusion_name,
156
158
  "weight": self.weight,
157
159
  "uid": self.uid}
@@ -159,10 +161,15 @@ class Rule(NodeMixin, SubclassJSONSerializer, ABC):
159
161
 
160
162
  @classmethod
161
163
  def _from_json(cls, data: Dict[str, Any]) -> Rule:
164
+ try:
165
+ corner_case = Case.from_json(data["corner_case"])
166
+ except Exception as e:
167
+ logdebug("Failed to load corner case from json, setting it to None.")
168
+ corner_case = None
162
169
  loaded_rule = cls(conditions=CallableExpression.from_json(data["conditions"]),
163
170
  conclusion=CallableExpression.from_json(data["conclusion"]),
164
171
  parent=cls.from_json(data["parent"]),
165
- corner_case=Case.from_json(data["corner_case"]),
172
+ corner_case=corner_case,
166
173
  conclusion_name=data["conclusion_name"],
167
174
  weight=data["weight"],
168
175
  uid=data["uid"])
@@ -22,6 +22,7 @@ import requests
22
22
  from anytree import Node, RenderTree
23
23
  from anytree.exporter import DotExporter
24
24
  from matplotlib import pyplot as plt
25
+ from rospy import logwarn
25
26
  from sqlalchemy import MetaData, inspect
26
27
  from sqlalchemy.orm import Mapped, registry, class_mapper, DeclarativeBase as SQLTable, Session
27
28
  from tabulate import tabulate
@@ -129,7 +130,7 @@ def extract_imports(file_path):
129
130
  module = importlib.import_module(module_name)
130
131
  scope[asname] = getattr(module, name)
131
132
  except (ImportError, AttributeError) as e:
132
- print(f"Could not import {name} from {module_name}: {e}")
133
+ logwarn(f"Could not import {module_name}: {e} while extracting imports from {file_path}")
133
134
 
134
135
  return scope
135
136
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ripple_down_rules
3
- Version: 0.4.3
3
+ Version: 0.4.4
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
@@ -34,4 +34,5 @@ test/test_rdr_decorators.py
34
34
  test/test_rdr_world.py
35
35
  test/test_relational_rdr.py
36
36
  test/test_relational_rdr_alchemy.py
37
- test/test_sql_model.py
37
+ test/test_sql_model.py
38
+ test/test_utils.py
@@ -26,18 +26,18 @@ class ObjectDiagramTestCase(unittest.TestCase):
26
26
 
27
27
  @classmethod
28
28
  def setUpClass(cls):
29
- cls.cases, cls.targets = load_zoo_dataset(cache_file="../test_results/zoo")
29
+ cls.cases, cls.targets = load_zoo_dataset(cache_file=f"{os.path.dirname(__file__)}/test_results/zoo")
30
30
  cls.cq = CaseQuery(cls.cases[0], "species", (Species,), True, _target=cls.targets[0])
31
31
  cls.person = Person("Ahmed", Address("Cairo"))
32
32
 
33
33
  def test_generate_person_diagram(self):
34
34
  # Generate object diagram for the Person instance
35
35
  graph = generate_object_graph(self.person)
36
- graph.render('../test_results/person_object_diagram', view=False)
37
- self.assertTrue(os.path.isfile('../test_results/person_object_diagram.svg'))
36
+ graph.render(f'{os.path.dirname(__file__)}/test_results/person_object_diagram', view=False)
37
+ self.assertTrue(os.path.isfile(f'{os.path.dirname(__file__)}/test_results/person_object_diagram.svg'))
38
38
 
39
39
  def test_generate_case_query_diagram(self):
40
40
  # Generate object diagram for the CaseQuery instance
41
41
  graph = generate_object_graph(self.cq)
42
- graph.render('../test_results/case_query_object_diagram', view=False)
43
- self.assertTrue(os.path.isfile('../test_results/case_query_object_diagram.svg'))
42
+ graph.render(f'{os.path.dirname(__file__)}/test_results/case_query_object_diagram', view=False)
43
+ self.assertTrue(os.path.isfile(f'{os.path.dirname(__file__)}/test_results/case_query_object_diagram.svg'))
@@ -119,7 +119,7 @@ class TestRDRWorld(TestCase):
119
119
  cls.viewer = RDRCaseViewer()
120
120
 
121
121
  def test_view_rdr(self):
122
- self.get_view_rdr(use_loaded_answers=False, save_answers=False, append=False)
122
+ self.get_view_rdr(use_loaded_answers=True, save_answers=False, append=False)
123
123
 
124
124
  def test_save_and_load_view_rdr(self):
125
125
  view_rdr = self.get_view_rdr(use_loaded_answers=True, save_answers=False, append=False)
@@ -0,0 +1,23 @@
1
+ import os
2
+ from unittest import TestCase
3
+
4
+ from ripple_down_rules.utils import extract_imports, make_set
5
+
6
+
7
+ class UtilsTestCase(TestCase):
8
+
9
+ def test_extract_imports_from_file(self):
10
+ # Test with a file that has imports
11
+ file_path = "test_file.py"
12
+ with open(file_path, "w") as f:
13
+ f.write("import os\n")
14
+ f.write("from module import function\n")
15
+ f.write("from ripple_down_rules.utils import make_set\n")
16
+ f.write("print('Hello World')\n")
17
+
18
+ expected_scope = {"os": os, "make_set": make_set}
19
+ actual_imports = extract_imports(file_path)
20
+ self.assertEqual(expected_scope, actual_imports)
21
+
22
+ # Clean up
23
+ os.remove(file_path)