ripple-down-rules 0.5.4__tar.gz → 0.5.7__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 (153) hide show
  1. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/PKG-INFO +16 -2
  2. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/README.md +13 -0
  3. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/doc/intro.md +46 -21
  4. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/examples/part_containment_rdr/robot_rdr.py +3 -2
  5. ripple_down_rules-0.5.7/images/scrdr.dot +127 -0
  6. ripple_down_rules-0.5.7/images/scrdr.png +0 -0
  7. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/requirements.txt +2 -1
  8. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/src/ripple_down_rules/__init__.py +1 -1
  9. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/src/ripple_down_rules/datastructures/callable_expression.py +16 -9
  10. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/src/ripple_down_rules/datastructures/case.py +10 -4
  11. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/src/ripple_down_rules/datastructures/dataclasses.py +62 -3
  12. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/src/ripple_down_rules/experts.py +12 -2
  13. ripple_down_rules-0.5.7/src/ripple_down_rules/helpers.py +97 -0
  14. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/src/ripple_down_rules/rdr.py +148 -101
  15. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/src/ripple_down_rules/rdr_decorators.py +54 -26
  16. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/src/ripple_down_rules/rules.py +63 -13
  17. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/src/ripple_down_rules/user_interface/gui.py +10 -7
  18. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/src/ripple_down_rules/user_interface/ipython_custom_shell.py +1 -1
  19. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/src/ripple_down_rules/user_interface/object_diagram.py +9 -1
  20. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/src/ripple_down_rules/user_interface/template_file_creator.py +25 -24
  21. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/src/ripple_down_rules/utils.py +260 -78
  22. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/src/ripple_down_rules.egg-info/PKG-INFO +16 -2
  23. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/src/ripple_down_rules.egg-info/SOURCES.txt +16 -2
  24. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/src/ripple_down_rules.egg-info/requires.txt +2 -1
  25. ripple_down_rules-0.5.7/test/conf/world/base_config.py +48 -0
  26. ripple_down_rules-0.5.7/test/conf/world/handles_and_containers.py +50 -0
  27. ripple_down_rules-0.5.7/test/conftest.py +112 -0
  28. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/datasets.py +82 -0
  29. ripple_down_rules-0.5.7/test/factories/world/__init__.py +0 -0
  30. ripple_down_rules-0.5.7/test/factories/world/handles_and_containers.py +22 -0
  31. ripple_down_rules-0.5.7/test/test_generated_rdrs/__init__.py +1 -0
  32. ripple_down_rules-0.5.7/test/test_helpers/__init__.py +0 -0
  33. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_helpers/helpers.py +27 -14
  34. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_on_mutagenic.py +12 -8
  35. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_rdr.py +53 -33
  36. ripple_down_rules-0.5.7/test/test_rdr_helpers_rdrs.py +65 -0
  37. ripple_down_rules-0.5.7/test/test_rdr_world/__init__.py +0 -0
  38. ripple_down_rules-0.5.7/test/test_rdr_world/conftest.py +147 -0
  39. ripple_down_rules-0.5.7/test/test_rdr_world/test_rdr_world.py +52 -0
  40. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_results/datasets_physical_object_is_a_robot/physical_object_is_a_robot_output__scrdr.py +2 -3
  41. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_results/datasets_physical_object_is_a_robot/physical_object_is_a_robot_rdr.py +3 -3
  42. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_results/datasets_physical_object_select_objects_that_are_parts_of_robot/physical_object_select_objects_that_are_parts_of_robot_output__mcrdr.py +2 -3
  43. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_results/datasets_physical_object_select_objects_that_are_parts_of_robot/physical_object_select_objects_that_are_parts_of_robot_rdr.py +3 -3
  44. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_results/datasets_physical_object_select_objects_that_are_parts_of_robot/rdr_metadata/datasets_physical_object_select_objects_that_are_parts_of_robot.json +4 -4
  45. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_template_file_creator.py +7 -7
  46. ripple_down_rules-0.5.7/test/test_user_interface/__init__.py +0 -0
  47. ripple_down_rules-0.5.7/test/test_user_interface/test_prompt.py +26 -0
  48. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_user_interface/test_qt_gui_inline.py +4 -1
  49. ripple_down_rules-0.5.7/test/test_utils.py +38 -0
  50. ripple_down_rules-0.5.4/src/ripple_down_rules/helpers.py +0 -51
  51. ripple_down_rules-0.5.4/test/test_rdr_world.py +0 -191
  52. ripple_down_rules-0.5.4/test/test_utils.py +0 -23
  53. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/.github/workflows/build_and_deploy_doc.yml +0 -0
  54. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/.github/workflows/ci.yml +0 -0
  55. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/.github/workflows/publish-to-test-pypi.yml +0 -0
  56. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/.idea/shelf/Uncommitted_changes_before_Checkout_at_2_4_25,_6_32_PM_[Changes]/shelved.patch +0 -0
  57. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/.idea/shelf/Uncommitted_changes_before_Checkout_at_2_4_25,_6_32_PM_[Changes]1/shelved.patch +0 -0
  58. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/LICENSE +0 -0
  59. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/doc/_config.yml +0 -0
  60. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/doc/_toc.yml +0 -0
  61. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/doc/bibliography.md +0 -0
  62. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/doc/references.bib +0 -0
  63. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/doc/requirements.txt +0 -0
  64. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/examples/__init__.py +0 -0
  65. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/examples/animal_species.py +0 -0
  66. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/examples/part_containment_rdr/__init__.py +0 -0
  67. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/examples/part_containment_rdr/rdr_metadata/part_containment_rdr.json +0 -0
  68. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/examples/part_containment_rdr/robot_contained_objects_mcrdr.py +0 -0
  69. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/examples/part_containment_rdr/robot_contained_objects_mcrdr_defs.py +0 -0
  70. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/examples/relational_example.py +0 -0
  71. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/images/thinking_pr2.jpg +0 -0
  72. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/pyproject.toml +0 -0
  73. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/pytest.ini +0 -0
  74. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/requirements-dev-ci.txt +0 -0
  75. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/requirements-dev.txt +0 -0
  76. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/requirements-gui.txt +0 -0
  77. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/requirements-viz.txt +0 -0
  78. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/results/complete_mcrdr_extra.dot +0 -0
  79. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/results/complete_mcrdr_extra.png +0 -0
  80. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/results/complete_mcrdr_stop_only.dot +0 -0
  81. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/results/complete_mcrdr_stop_only.png +0 -0
  82. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/results/complete_mcrdr_stop_plus_rule.dot +0 -0
  83. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/results/complete_mcrdr_stop_plus_rule.png +0 -0
  84. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/results/complete_scrdr.dot +0 -0
  85. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/results/complete_scrdr.png +0 -0
  86. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/results/complete_scrdr_2.dot +0 -0
  87. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/results/complete_scrdr_2.png +0 -0
  88. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/results/complete_scrdr_3.dot +0 -0
  89. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/results/complete_scrdr_3.png +0 -0
  90. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/results/grdr_Habitat.dot +0 -0
  91. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/results/grdr_Habitat.png +0 -0
  92. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/results/grdr_Species.dot +0 -0
  93. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/results/grdr_Species.png +0 -0
  94. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/results/mcrdr_extra.dot +0 -0
  95. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/results/mcrdr_extra.png +0 -0
  96. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/results/mcrdr_extra_classify.dot +0 -0
  97. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/results/mcrdr_extra_classify.png +0 -0
  98. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/results/mcrdr_stop_plus_rule_combined.dot +0 -0
  99. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/results/mcrdr_stop_plus_rule_combined.png +0 -0
  100. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/results/partial_mcrdr_extra.dot +0 -0
  101. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/results/partial_mcrdr_extra.png +0 -0
  102. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/results/relational_scrdr_classify.dot +0 -0
  103. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/results/relational_scrdr_classify.png +0 -0
  104. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/setup.cfg +0 -0
  105. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/src/ripple_down_rules/datastructures/__init__.py +0 -0
  106. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/src/ripple_down_rules/datastructures/enums.py +0 -0
  107. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/src/ripple_down_rules/start-code-server.sh +0 -0
  108. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/src/ripple_down_rules/user_interface/__init__.py +0 -0
  109. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/src/ripple_down_rules/user_interface/prompt.py +0 -0
  110. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/src/ripple_down_rules.egg-info/dependency_links.txt +0 -0
  111. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/src/ripple_down_rules.egg-info/top_level.txt +0 -0
  112. {ripple_down_rules-0.5.4/test/test_generated_rdrs → ripple_down_rules-0.5.7/test/conf}/__init__.py +0 -0
  113. {ripple_down_rules-0.5.4/test/test_helpers → ripple_down_rules-0.5.7/test/conf/world}/__init__.py +0 -0
  114. {ripple_down_rules-0.5.4/test/test_user_interface → ripple_down_rules-0.5.7/test/factories}/__init__.py +0 -0
  115. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_expert_answers/correct_drawer_rdr_expert_answers_fit.json +0 -0
  116. ripple_down_rules-0.5.4/test/test_expert_answers/view_rdr_expert_answers_fit.json → ripple_down_rules-0.5.7/test/test_expert_answers/drawer_cabinet_expert_answers_fit.json +0 -0
  117. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_expert_answers/grdr_expert_answers_classify.json +0 -0
  118. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_expert_answers/grdr_expert_answers_fit.json +0 -0
  119. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_expert_answers/grdr_expert_answers_fit_extra.json +0 -0
  120. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_expert_answers/grdr_expert_answers_fit_no_targets.json +0 -0
  121. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_expert_answers/mcrdr_expert_answers_classify.json +0 -0
  122. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_expert_answers/mcrdr_expert_answers_fit_no_targets.json +0 -0
  123. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_expert_answers/mcrdr_expert_answers_stop_only_fit.json +0 -0
  124. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_expert_answers/mcrdr_extra_expert_answers_classify.json +0 -0
  125. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_expert_answers/mcrdr_extra_expert_answers_fit.json +0 -0
  126. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_expert_answers/mcrdr_multi_line_expert_answers_fit.json +0 -0
  127. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_expert_answers/mcrdr_stop_only_answers_fit.json +0 -0
  128. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_expert_answers/mcrdr_stop_plus_rule_answers_fit.json +0 -0
  129. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_expert_answers/mcrdr_stop_plus_rule_combined_expert_answers_fit.json +0 -0
  130. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_expert_answers/mcrdr_stop_plus_rule_expert_answers_fit.json +0 -0
  131. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_expert_answers/mutagenic_expert_answers.json +0 -0
  132. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_expert_answers/relational_scrdr_expert_answers_classify.json +0 -0
  133. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_expert_answers/scrdr_expert_answers_classify.json +0 -0
  134. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_expert_answers/scrdr_expert_answers_fit.json +0 -0
  135. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_expert_answers/scrdr_expert_answers_fit_no_targets.json +0 -0
  136. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_expert_answers/scrdr_multi_line_expert_answers_fit.json +0 -0
  137. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_expert_answers/scrdr_world_expert_answers_fit.json +0 -0
  138. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_helpers/object_diagram_case_query.png +0 -0
  139. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_helpers/object_diagram_person.png +0 -0
  140. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_json_serialization.py +0 -0
  141. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_object_diagram.py +0 -0
  142. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_rdr_alchemy.py +0 -0
  143. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_rdr_decorators.py +0 -0
  144. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_relational_rdr.py +0 -0
  145. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_relational_rdr_alchemy.py +0 -0
  146. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_results/datasets_physical_object_is_a_robot/__init__.py +0 -0
  147. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_results/datasets_physical_object_is_a_robot/physical_object_is_a_robot_output__scrdr_defs.py +2 -2
  148. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_results/datasets_physical_object_is_a_robot/rdr_metadata/datasets_physical_object_is_a_robot.json +0 -0
  149. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_results/datasets_physical_object_select_objects_that_are_parts_of_robot/__init__.py +0 -0
  150. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_results/datasets_physical_object_select_objects_that_are_parts_of_robot/physical_object_select_objects_that_are_parts_of_robot_output__mcrdr_defs.py +3 -3
  151. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_sql_model.py +0 -0
  152. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_user_interface/test_ipython.py +0 -0
  153. {ripple_down_rules-0.5.4 → ripple_down_rules-0.5.7}/test/test_user_interface/test_ipython_copilot.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ripple_down_rules
3
- Version: 0.5.4
3
+ Version: 0.5.7
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
@@ -693,7 +693,8 @@ Requires-Dist: colorama
693
693
  Requires-Dist: pygments
694
694
  Requires-Dist: sqlalchemy
695
695
  Requires-Dist: pandas
696
- Requires-Dist: pyparsing
696
+ Requires-Dist: pyparsing>=3.2.3
697
+ Requires-Dist: omegaconf
697
698
  Provides-Extra: viz
698
699
  Requires-Dist: networkx>=3.1; extra == "viz"
699
700
  Requires-Dist: matplotlib>=3.7.5; extra == "viz"
@@ -799,6 +800,19 @@ cat = grdr.classify(all_cases[50])['species']
799
800
  assert cat == targets[50]
800
801
  ```
801
802
 
803
+ When prompted to write a rule, I wrote the following inside the template function that the Ripple Down Rules created:
804
+ ```python
805
+ return case.milk == 1
806
+ ```
807
+ then
808
+ ```python
809
+ return case.aquatic == 1
810
+ ```
811
+
812
+ The rule tree generated from fitting all the dataset will look like this:
813
+ ![species_rdr](https://raw.githubusercontent.com/AbdelrhmanBassiouny/ripple_down_rules/main/images/scrdr.png)
814
+
815
+
802
816
  ### Relational Example
803
817
 
804
818
  By relational, I mean that each rule conclusion is not a constant value, but is related to the case being classified,
@@ -95,6 +95,19 @@ cat = grdr.classify(all_cases[50])['species']
95
95
  assert cat == targets[50]
96
96
  ```
97
97
 
98
+ When prompted to write a rule, I wrote the following inside the template function that the Ripple Down Rules created:
99
+ ```python
100
+ return case.milk == 1
101
+ ```
102
+ then
103
+ ```python
104
+ return case.aquatic == 1
105
+ ```
106
+
107
+ The rule tree generated from fitting all the dataset will look like this:
108
+ ![species_rdr](https://raw.githubusercontent.com/AbdelrhmanBassiouny/ripple_down_rules/main/images/scrdr.png)
109
+
110
+
98
111
  ### Relational Example
99
112
 
100
113
  By relational, I mean that each rule conclusion is not a constant value, but is related to the case being classified,
@@ -13,27 +13,39 @@ SCRDR, MCRDR, and GRDR logic were inspired from the book:
13
13
  ["Ripple Down Rules: An Alternative to Machine Learning"](https://www.taylorfrancis.com/books/mono/10.1201/9781003126157/ripple-rules-paul-compton-byeong-ho-kang) by Paul Compton, Byeong Ho Kang.
14
14
 
15
15
 
16
- Additionally, to the main ripple down rules benefits, this package also features:
17
-
18
- - Ontology and Rule base as one entity
19
- - Both use same python data structures.
20
- - Update in data structures reflects in Rule base.
21
- - Both are under one versioning system, the rule base is generated as a python module that becomes part of your
22
- project.
23
- - First, second, and higher order logic is achieved by freely writing python code.
24
- - Rule conditions and conclusions are python functions that can do anything that python can do.
25
- - Automatic maintenance of rule base.
26
- - When rules are added contradictions are detected and prompted to the user to add a refinement rule for
27
- to resolve the contradiction.
28
- - Transparent Editable rule base
29
- - Rules are understandable python code that can be manually edited and reloaded.
30
- - Interface that makes coders feel at home by using their favorite IDE.
31
- - Uses latest technologies like LLMs to help in writing the rules (Inherited feature from IDEs).
32
- - Auto completions and suggestions in the IDE.
33
- - Ask AI available with the press of a button to help in writing the rule.
34
- - Rules can be for instances on very specific scenarios and can be very general on super class relations.
35
- - Accompanied by a GUI with an object explorer panel and an expandable/retractable object diagram to help in
36
- writing the rules about the object at hand.
16
+ ## 🚀 Enhanced Ripple-Down Rules Engine Key Features
17
+
18
+ ### 🧠 Ontology + Rule Base as One Entity
19
+ - 🧬 Unified data structure: Ontology and rules use the same Python data structures.
20
+ - 🔄 Automatic sync: Updates to the ontology instantly reflect in the rule base.
21
+ - 📦 Version controlled: The rule base is a Python module, versioned with your project.
22
+
23
+ ### 🔁 Supports First, Second & Higher-Order Logic
24
+ - 🧩 Unlimited expressiveness: Rule conditions and conclusions are plain Python functions anything Python can do, your rules can too!
25
+
26
+ ### 🛡️ Automatic Rule Base Maintenance
27
+ - ⚠️ Contradiction detection: New rules are auto-checked for logical conflicts.
28
+ - 🔧 Prompted refinements: If a contradiction arises, you're guided to add a refinement rule.
29
+
30
+ ### 📝 Transparent & Editable Rule Base
31
+ - 📖 Readable rules: Rules are clean, understandable Python code.
32
+ - 🔄 Reload-friendly: Easily edit and reload rules manually as needed.
33
+
34
+ ### 💻 Developer-Centric Interface
35
+ - 👨‍💻 Feels like home: Seamless integration with your favorite IDE.
36
+ - Modern coding experience: Auto-completion and suggestions supported via IDE plugins.
37
+
38
+ ### 🤖 LLM-Powered Rule Writing
39
+ - 💡 AI-assisted authoring: Ask AI for help or suggestions directly within the IDE.
40
+ - ⚡ Smart completion: Context-aware completions streamline rule writing.
41
+
42
+ ### 🎯 Flexible Rule Specificity
43
+ - 🧪 Instance-level precision: Write rules for highly specific object scenarios.
44
+ - 🏛️ Generalization-ready: Create broad rules for superclass relationships.
45
+
46
+ ### 🖼️ GUI for Rule Exploration
47
+ - 🧭 Object Explorer Panel: Navigate and inspect objects easily.
48
+ - 🧯 Interactive Diagram: Expandable/collapsible object diagram to guide rule creation visually.
37
49
 
38
50
  This work aims to provide a flexible and powerful rule-based system that can be used in various applications,
39
51
  from simple classification tasks to complex decision-making systems. Furthermore, one of the main goals is to
@@ -186,6 +198,19 @@ cat = grdr.classify(all_cases[50])['species']
186
198
  assert cat == targets[50]
187
199
  ```
188
200
 
201
+ When prompted to write a rule, I wrote the following inside the template function that the Ripple Down Rules created:
202
+ ```python
203
+ return case.milk == 1
204
+ ```
205
+ then
206
+ ```python
207
+ return case.aquatic == 1
208
+ ```
209
+
210
+ The rule tree generated from fitting all the dataset will look like this:
211
+ ![species_rdr](https://raw.githubusercontent.com/AbdelrhmanBassiouny/ripple_down_rules/main/images/scrdr.png)
212
+
213
+
189
214
  ### Relational Example
190
215
 
191
216
  By relational, I mean that each rule conclusion is not a constant value, but is related to the case being classified,
@@ -1,5 +1,6 @@
1
1
  from typing_extensions import Dict, Any
2
- from ripple_down_rules.rdr import GeneralRDR
2
+
3
+ from ripple_down_rules.helpers import general_rdr_classify
3
4
  from ripple_down_rules.datastructures.case import Case, create_case
4
5
  from __main__ import Robot
5
6
  from . import robot_contained_objects_mcrdr as contained_objects_classifier
@@ -12,4 +13,4 @@ classifiers_dict['contained_objects'] = contained_objects_classifier
12
13
  def classify(case: Robot) -> Dict[str, Any]:
13
14
  if not isinstance(case, Case):
14
15
  case = create_case(case, max_recursion_idx=3)
15
- return GeneralRDR._classify(classifiers_dict, case)
16
+ return general_rdr_classify(classifiers_dict, case)
@@ -0,0 +1,127 @@
1
+ digraph tree {
2
+ "def _get_value(case):
3
+ return case.milk == 1
4
+ => def _get_value(case):
5
+ return Species.mammal";
6
+ "def _get_value(case):
7
+ return case.aquatic == 1
8
+ => def _get_value(case):
9
+ return Species.fish";
10
+ "def _get_value(case):
11
+ return case.feathers == 1
12
+ => def _get_value(case):
13
+ return Species.bird";
14
+ "def _get_value(case):
15
+ return case.backbone == 0 and case.breathes == 0
16
+ => def _get_value(case):
17
+ return Species.molusc";
18
+ "def _get_value(case):
19
+ return case.eggs == 1 and case.backbone == 0
20
+ => def _get_value(case):
21
+ return Species.insect";
22
+ "def _get_value(case):
23
+ return case.backbone == 1 and case.tail == 1
24
+ => def _get_value(case):
25
+ return Species.reptile";
26
+ "def _get_value(case):
27
+ return case.backbone == 0 and case.eggs == 0
28
+ => def _get_value(case):
29
+ return Species.molusc";
30
+ "def _get_value(case):
31
+ return case.legs == 0
32
+ => def _get_value(case):
33
+ return Species.molusc";
34
+ "def _get_value(case):
35
+ return case.fins == 0
36
+ => def _get_value(case):
37
+ return Species.molusc";
38
+ "def _get_value(case):
39
+ return case.feathers == 1
40
+ => def _get_value(case):
41
+ return Species.bird_1";
42
+ "def _get_value(case):
43
+ return case.backbone == 1
44
+ => def _get_value(case):
45
+ return Species.amphibian";
46
+ "def _get_value(case):
47
+ return case.breathes == 0
48
+ => def _get_value(case):
49
+ return Species.reptile";
50
+ "def _get_value(case):
51
+ return case.milk == 1
52
+ => def _get_value(case):
53
+ return Species.mammal" -> "def _get_value(case):
54
+ return case.aquatic == 1
55
+ => def _get_value(case):
56
+ return Species.fish" [style="bold", label=" else if"];
57
+ "def _get_value(case):
58
+ return case.aquatic == 1
59
+ => def _get_value(case):
60
+ return Species.fish" -> "def _get_value(case):
61
+ return case.feathers == 1
62
+ => def _get_value(case):
63
+ return Species.bird" [style="bold", label=" else if"];
64
+ "def _get_value(case):
65
+ return case.aquatic == 1
66
+ => def _get_value(case):
67
+ return Species.fish" -> "def _get_value(case):
68
+ return case.fins == 0
69
+ => def _get_value(case):
70
+ return Species.molusc" [style="bold", label=" except if"];
71
+ "def _get_value(case):
72
+ return case.feathers == 1
73
+ => def _get_value(case):
74
+ return Species.bird" -> "def _get_value(case):
75
+ return case.backbone == 0 and case.breathes == 0
76
+ => def _get_value(case):
77
+ return Species.molusc" [style="bold", label=" else if"];
78
+ "def _get_value(case):
79
+ return case.backbone == 0 and case.breathes == 0
80
+ => def _get_value(case):
81
+ return Species.molusc" -> "def _get_value(case):
82
+ return case.eggs == 1 and case.backbone == 0
83
+ => def _get_value(case):
84
+ return Species.insect" [style="bold", label=" else if"];
85
+ "def _get_value(case):
86
+ return case.eggs == 1 and case.backbone == 0
87
+ => def _get_value(case):
88
+ return Species.insect" -> "def _get_value(case):
89
+ return case.backbone == 1 and case.tail == 1
90
+ => def _get_value(case):
91
+ return Species.reptile" [style="bold", label=" else if"];
92
+ "def _get_value(case):
93
+ return case.eggs == 1 and case.backbone == 0
94
+ => def _get_value(case):
95
+ return Species.insect" -> "def _get_value(case):
96
+ return case.legs == 0
97
+ => def _get_value(case):
98
+ return Species.molusc" [style="bold", label=" except if"];
99
+ "def _get_value(case):
100
+ return case.backbone == 1 and case.tail == 1
101
+ => def _get_value(case):
102
+ return Species.reptile" -> "def _get_value(case):
103
+ return case.backbone == 0 and case.eggs == 0
104
+ => def _get_value(case):
105
+ return Species.molusc" [style="bold", label=" else if"];
106
+ "def _get_value(case):
107
+ return case.fins == 0
108
+ => def _get_value(case):
109
+ return Species.molusc" -> "def _get_value(case):
110
+ return case.feathers == 1
111
+ => def _get_value(case):
112
+ return Species.bird_1" [style="bold", label=" except if"];
113
+ "def _get_value(case):
114
+ return case.feathers == 1
115
+ => def _get_value(case):
116
+ return Species.bird_1" -> "def _get_value(case):
117
+ return case.backbone == 1
118
+ => def _get_value(case):
119
+ return Species.amphibian" [style="bold", label=" else if"];
120
+ "def _get_value(case):
121
+ return case.backbone == 1
122
+ => def _get_value(case):
123
+ return Species.amphibian" -> "def _get_value(case):
124
+ return case.breathes == 0
125
+ => def _get_value(case):
126
+ return Species.reptile" [style="bold", label=" except if"];
127
+ }
Binary file
@@ -10,4 +10,5 @@ colorama
10
10
  pygments
11
11
  sqlalchemy
12
12
  pandas
13
- pyparsing
13
+ pyparsing>=3.2.3
14
+ omegaconf
@@ -1,4 +1,4 @@
1
- __version__ = "0.5.4"
1
+ __version__ = "0.5.7"
2
2
 
3
3
  import logging
4
4
  logger = logging.Logger("rdr")
@@ -93,7 +93,6 @@ class CallableExpression(SubclassJSONSerializer):
93
93
  A callable that is constructed from a string statement written by an expert.
94
94
  """
95
95
  encapsulating_function_name: str = "_get_value"
96
- encapsulating_function: str = f"def {encapsulating_function_name}(case):"
97
96
 
98
97
  def __init__(self, user_input: Optional[str] = None,
99
98
  conclusion_type: Optional[Tuple[Type]] = None,
@@ -117,7 +116,7 @@ class CallableExpression(SubclassJSONSerializer):
117
116
  if user_input is None:
118
117
  user_input = build_user_input_from_conclusion(conclusion)
119
118
  self.conclusion: Optional[Any] = conclusion
120
- self._user_input: str = encapsulate_user_input(user_input, self.encapsulating_function)
119
+ self._user_input: str = encapsulate_user_input(user_input, self.get_encapsulating_function())
121
120
  if conclusion_type is not None:
122
121
  if is_iterable(conclusion_type):
123
122
  conclusion_type = tuple(conclusion_type)
@@ -132,6 +131,13 @@ class CallableExpression(SubclassJSONSerializer):
132
131
  self.visitor.visit(self.expression_tree)
133
132
  self.mutually_exclusive: bool = mutually_exclusive
134
133
 
134
+ @classmethod
135
+ def get_encapsulating_function(cls, postfix: str = '') -> str:
136
+ """
137
+ Get the encapsulating function that is used to wrap the user input.
138
+ """
139
+ return f"def {cls.encapsulating_function_name}{postfix}(case):"
140
+
135
141
  def __call__(self, case: Any, **kwargs) -> Any:
136
142
  try:
137
143
  if self.user_input is not None:
@@ -161,8 +167,8 @@ class CallableExpression(SubclassJSONSerializer):
161
167
  """
162
168
  Combine this callable expression with another callable expression using the 'and' operator.
163
169
  """
164
- cond1_user_input = self.user_input.replace(self.encapsulating_function, "def _cond1(case):")
165
- cond2_user_input = other.user_input.replace(self.encapsulating_function, "def _cond2(case):")
170
+ cond1_user_input = self.user_input.replace(self.get_encapsulating_function(), "def _cond1(case):")
171
+ cond2_user_input = other.user_input.replace(self.get_encapsulating_function(), "def _cond2(case):")
166
172
  new_user_input = (f"{cond1_user_input}\n"
167
173
  f"{cond2_user_input}\n"
168
174
  f"return _cond1(case) and _cond2(case)")
@@ -175,7 +181,7 @@ class CallableExpression(SubclassJSONSerializer):
175
181
  new_function_body = extract_function_source(file_path, [function_name])[function_name]
176
182
  if new_function_body is None:
177
183
  return
178
- self.user_input = self.encapsulating_function + '\n' + new_function_body
184
+ self.user_input = self.get_encapsulating_function() + '\n' + new_function_body
179
185
 
180
186
  def write_to_python_file(self, file_path: str, append: bool = False):
181
187
  """
@@ -208,7 +214,7 @@ class CallableExpression(SubclassJSONSerializer):
208
214
  Set the user input.
209
215
  """
210
216
  if value is not None:
211
- self._user_input = encapsulate_user_input(value, self.encapsulating_function)
217
+ self._user_input = encapsulate_user_input(value, self.get_encapsulating_function())
212
218
  self.scope = get_used_scope(self.user_input, self.scope)
213
219
  self.expression_tree = parse_string_to_expression(self.user_input)
214
220
  self.code = compile_expression_to_code(self.expression_tree)
@@ -253,7 +259,8 @@ class CallableExpression(SubclassJSONSerializer):
253
259
  "conclusion_type": [get_full_class_name(t) for t in self.conclusion_type]
254
260
  if self.conclusion_type is not None else None,
255
261
  "scope": {k: get_full_class_name(v) for k, v in self.scope.items()
256
- if hasattr(v, '__module__') and hasattr(v, '__name__')},
262
+ if hasattr(v, '__module__') and hasattr(v, '__name__')
263
+ and v.__module__ is not None and v.__name__ is not None},
257
264
  "conclusion": conclusion_to_json(self.conclusion),
258
265
  "mutually_exclusive": self.mutually_exclusive,
259
266
  }
@@ -286,8 +293,8 @@ def parse_string_to_expression(expression_str: str) -> AST:
286
293
  :param expression_str: The string which will be parsed.
287
294
  :return: The parsed expression.
288
295
  """
289
- if not expression_str.startswith(CallableExpression.encapsulating_function):
290
- expression_str = encapsulate_user_input(expression_str, CallableExpression.encapsulating_function)
296
+ if not expression_str.startswith(CallableExpression.get_encapsulating_function()):
297
+ expression_str = encapsulate_user_input(expression_str, CallableExpression.get_encapsulating_function())
291
298
  mode = 'exec' if expression_str.startswith('def') else 'eval'
292
299
  tree = ast.parse(expression_str, mode=mode)
293
300
  logging.debug(f"AST parsed successfully: {ast.dump(tree)}")
@@ -84,7 +84,7 @@ class Case(UserDict, SubclassJSONSerializer):
84
84
  def _to_json(self) -> Dict[str, Any]:
85
85
  serializable = {k: v for k, v in self.items() if not k.startswith("_")}
86
86
  serializable["_id"] = self._id
87
- serializable["_obj_type"] = get_full_class_name(self._obj_type)
87
+ serializable["_obj_type"] = get_full_class_name(self._obj_type) if self._obj_type is not None else None
88
88
  serializable["_name"] = self._name
89
89
  for k, v in serializable.items():
90
90
  if isinstance(v, set):
@@ -96,7 +96,7 @@ class Case(UserDict, SubclassJSONSerializer):
96
96
  @classmethod
97
97
  def _from_json(cls, data: Dict[str, Any]) -> Case:
98
98
  id_ = data.pop("_id")
99
- obj_type = get_type_from_string(data.pop("_obj_type"))
99
+ obj_type = get_type_from_string(data.pop("_obj_type")) if data["_obj_type"] is not None else None
100
100
  name = data.pop("_name")
101
101
  for k, v in data.items():
102
102
  data[k] = SubclassJSONSerializer.from_json(v)
@@ -308,7 +308,10 @@ def create_case_attribute_from_iterable_attribute(attr_value: Any, name: str, ob
308
308
  :return: A case attribute that represents the original iterable attribute.
309
309
  """
310
310
  values = list(attr_value.values()) if isinstance(attr_value, (dict, UserDict)) else attr_value
311
- _type = type(list(values)[0]) if len(values) > 0 else get_value_type_from_type_hint(name, obj)
311
+ try:
312
+ _type = type(list(values)[0]) if len(values) > 0 else get_value_type_from_type_hint(name, obj)
313
+ except ValueError:
314
+ _type = None
312
315
  attr_case = Case(_type, _id=id(attr_value), _name=name, original_object=attr_value)
313
316
  case_attr = CaseAttribute(values)
314
317
  for idx, val in enumerate(values):
@@ -317,7 +320,10 @@ def create_case_attribute_from_iterable_attribute(attr_value: Any, name: str, ob
317
320
  obj_name=name, parent_is_iterable=True)
318
321
  attr_case.update(sub_attr_case)
319
322
  for sub_attr, val in attr_case.items():
320
- setattr(case_attr, sub_attr, val)
323
+ try:
324
+ setattr(case_attr, sub_attr, val)
325
+ except AttributeError:
326
+ pass
321
327
  return case_attr
322
328
 
323
329
 
@@ -5,9 +5,11 @@ import typing
5
5
  from dataclasses import dataclass, field
6
6
 
7
7
  import typing_extensions
8
+ from omegaconf import MISSING
8
9
  from sqlalchemy.orm import DeclarativeBase as SQLTable
9
- from typing_extensions import Any, Optional, Dict, Type, Tuple, Union, List, get_origin, Set
10
+ from typing_extensions import Any, Optional, Dict, Type, Tuple, Union, List, get_origin, Set, Callable
10
11
 
12
+ from ..utils import get_method_name, get_function_import_path_and_representation
11
13
  from .callable_expression import CallableExpression
12
14
  from .case import create_case, Case
13
15
  from ..utils import copy_case, make_list, make_set, get_origin_and_args_from_type_hint, get_value_type_from_type_hint, \
@@ -37,6 +39,24 @@ class CaseQuery:
37
39
  """
38
40
  Whether the attribute can only take one value (i.e. True) or multiple values (i.e. False).
39
41
  """
42
+ case_factory: Optional[Callable[[], Any]] = None
43
+ """
44
+ The factory method that can be used to recreate the original case.
45
+ """
46
+ case_factory_idx: Optional[int] = None
47
+ """
48
+ This is used when the case factory is a list of cases, this index is used to select the case from the list.
49
+ """
50
+ case_conf: Optional[CaseConf] = None
51
+ """
52
+ The case configuration that is used to (re)create the original case, recommended to be used when you want to
53
+ the case to persist in the rule base, this would allow it to be used for merging with other similar conclusion RDRs.
54
+ """
55
+ scenario: Optional[Callable] = None
56
+ """
57
+ The executable scenario is the root callable that recreates the situation that the case is
58
+ created in, for example, when the case is created from a test function, this would be the test function itself.
59
+ """
40
60
  _target: Optional[CallableExpression] = None
41
61
  """
42
62
  The target expression of the attribute.
@@ -95,7 +115,7 @@ class CaseQuery:
95
115
  """
96
116
  if self._case is not None:
97
117
  return self._case
98
- elif not isinstance(self.original_case, (Case, SQLTable)):
118
+ elif not isinstance(self.original_case, Case):
99
119
  self._case = create_case(self.original_case, max_recursion_idx=3)
100
120
  else:
101
121
  self._case = self.original_case
@@ -225,4 +245,43 @@ class CaseQuery:
225
245
  self.mutually_exclusive, _target=self.target, default_value=self.default_value,
226
246
  scope=self.scope, _case=copy_case(self.case), _target_value=self.target_value,
227
247
  conditions=self.conditions, is_function=self.is_function,
228
- function_args_type_hints=self.function_args_type_hints)
248
+ function_args_type_hints=self.function_args_type_hints,
249
+ case_factory=self.case_factory, case_factory_idx=self.case_factory_idx,
250
+ case_conf=self.case_conf, scenario=self.scenario)
251
+
252
+
253
+ @dataclass
254
+ class CaseConf:
255
+ factory_method: Callable[[Any], Any] = MISSING
256
+
257
+ def create(self) -> Any:
258
+ return self.factory_method()
259
+
260
+
261
+ @dataclass
262
+ class CaseFactoryMetaData:
263
+ factory_method: Optional[Callable[[Optional[CaseConf]], Any]] = None
264
+ factory_idx: Optional[int] = None
265
+ case_conf: Optional[CaseConf] = None
266
+ scenario: Optional[Callable] = None
267
+
268
+ @classmethod
269
+ def from_case_query(cls, case_query: CaseQuery) -> CaseFactoryMetaData:
270
+ return cls(factory_method=case_query.case_factory, factory_idx=case_query.case_factory_idx,
271
+ case_conf=case_query.case_conf, scenario=case_query.scenario)
272
+
273
+ def __repr__(self):
274
+ factory_method_repr = None
275
+ scenario_repr = None
276
+ if self.factory_method is not None:
277
+ _, factory_method_repr = get_function_import_path_and_representation(self.factory_method)
278
+ if self.scenario is not None:
279
+ _, scenario_repr = get_function_import_path_and_representation(self.scenario)
280
+ return (f"CaseFactoryMetaData("
281
+ f"factory_method={factory_method_repr}, "
282
+ f"factory_idx={self.factory_idx}, "
283
+ f"case_conf={self.case_conf},"
284
+ f" scenario={scenario_repr})")
285
+
286
+ def __str__(self):
287
+ return self.__repr__()
@@ -138,7 +138,12 @@ class Expert(ABC):
138
138
  os.makedirs(dir_name, exist_ok=True)
139
139
  with open(dir_name + '/__init__.py', 'w') as f:
140
140
  f.write('# This is an empty init file to make the directory a package.\n')
141
- action = 'w' if not self.append else 'a'
141
+ # Current file data
142
+ current_file_data = None
143
+ if os.path.exists(path + '.py'):
144
+ with open(path + '.py', 'r') as f:
145
+ current_file_data = f.read()
146
+ action = 'a' if self.append and current_file_data is not None else 'w'
142
147
  with open(path + '.py', action) as f:
143
148
  for scope, func_source in self.all_expert_answers:
144
149
  if len(scope) > 0:
@@ -147,9 +152,11 @@ class Expert(ABC):
147
152
  imports = ''
148
153
  if func_source is not None:
149
154
  uid = uuid.uuid4().hex
150
- func_source = encapsulate_user_input(func_source, CallableExpression.encapsulating_function + f'_{uid}')
155
+ func_source = encapsulate_user_input(func_source, CallableExpression.get_encapsulating_function(f'_{uid}'))
151
156
  else:
152
157
  func_source = 'pass # No user input provided for this case.\n'
158
+ if current_file_data is not None and func_source[1:] in current_file_data:
159
+ continue
153
160
  f.write(imports + func_source + '\n' + '\n\n\n\'===New Answer===\'\n\n\n')
154
161
 
155
162
  def load_answers(self, path: Optional[str] = None):
@@ -194,6 +201,7 @@ class Expert(ABC):
194
201
  answer = answer.strip('\n').strip()
195
202
  if 'def ' not in answer and 'pass' in answer:
196
203
  self.all_expert_answers.append(({}, None))
204
+ continue
197
205
  scope = extract_imports(tree=ast.parse(answer))
198
206
  function_source = all_function_sources[i].replace(all_function_sources_names[i],
199
207
  CallableExpression.encapsulating_function_name)
@@ -240,6 +248,7 @@ class Human(Expert):
240
248
  except IndexError:
241
249
  self.use_loaded_answers = False
242
250
  if user_input is not None:
251
+ case_query.scope.update(loaded_scope)
243
252
  condition = CallableExpression(user_input, bool, scope=case_query.scope)
244
253
  else:
245
254
  user_input, condition = self.user_prompt.prompt_user_for_expression(case_query, PromptFor.Conditions)
@@ -265,6 +274,7 @@ class Human(Expert):
265
274
  try:
266
275
  loaded_scope, expert_input = self.all_expert_answers.pop(0)
267
276
  if expert_input is not None:
277
+ case_query.scope.update(loaded_scope)
268
278
  expression = CallableExpression(expert_input, case_query.attribute_type,
269
279
  scope=case_query.scope,
270
280
  mutually_exclusive=case_query.mutually_exclusive)
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from types import ModuleType
5
+
6
+ from ripple_down_rules.datastructures.dataclasses import CaseFactoryMetaData
7
+
8
+ from .datastructures.case import create_case
9
+ from .datastructures.dataclasses import CaseQuery
10
+ from typing_extensions import Type, Optional, Callable, Any, Dict, TYPE_CHECKING, Union
11
+
12
+ from .utils import get_func_rdr_model_name, copy_case, make_set, update_case
13
+ from .utils import calculate_precision_and_recall
14
+
15
+ if TYPE_CHECKING:
16
+ from .rdr import RippleDownRules
17
+
18
+
19
+ def general_rdr_classify(classifiers_dict: Dict[str, Union[ModuleType, RippleDownRules]],
20
+ case: Any, modify_original_case: bool = False,
21
+ case_query: Optional[CaseQuery] = None) -> Dict[str, Any]:
22
+ """
23
+ Classify a case by going through all classifiers and adding the categories that are classified,
24
+ and then restarting the classification until no more categories can be added.
25
+
26
+ :param classifiers_dict: A dictionary mapping conclusion types to the classifiers that produce them.
27
+ :param case: The case to classify.
28
+ :param modify_original_case: Whether to modify the original case or create a copy and modify it.
29
+ :param case_query: The case query to extract metadata from if needed.
30
+ :return: The categories that the case belongs to.
31
+ """
32
+ conclusions = {}
33
+ case = create_case(case)
34
+ case_cp = copy_case(case) if not modify_original_case else case
35
+ while True:
36
+ new_conclusions = {}
37
+ for attribute_name, rdr in classifiers_dict.items():
38
+ pred_atts = rdr.classify(case_cp, case_query=case_query)
39
+ if pred_atts is None:
40
+ continue
41
+ if rdr.mutually_exclusive:
42
+ if attribute_name not in conclusions or \
43
+ (attribute_name in conclusions and conclusions[attribute_name] != pred_atts):
44
+ conclusions[attribute_name] = pred_atts
45
+ new_conclusions[attribute_name] = pred_atts
46
+ else:
47
+ pred_atts = make_set(pred_atts)
48
+ if attribute_name in conclusions:
49
+ pred_atts = {p for p in pred_atts if p not in conclusions[attribute_name]}
50
+ if len(pred_atts) > 0:
51
+ new_conclusions[attribute_name] = pred_atts
52
+ if attribute_name not in conclusions:
53
+ conclusions[attribute_name] = set()
54
+ conclusions[attribute_name].update(pred_atts)
55
+ if attribute_name in new_conclusions:
56
+ temp_case_query = CaseQuery(case_cp, attribute_name, rdr.conclusion_type, rdr.mutually_exclusive)
57
+ update_case(temp_case_query, new_conclusions)
58
+ if len(new_conclusions) == 0:
59
+ break
60
+ return conclusions
61
+
62
+
63
+ def is_matching(classifier: Callable[[Any], Any], case_query: CaseQuery, pred_cat: Optional[Dict[str, Any]] = None) -> bool:
64
+ """
65
+ :param classifier: The RDR classifier to check the prediction of.
66
+ :param case_query: The case query to check.
67
+ :param pred_cat: The predicted category.
68
+ :return: Whether the classifier prediction is matching case_query target or not.
69
+ """
70
+ if case_query.target is None:
71
+ return False
72
+ if pred_cat is None:
73
+ pred_cat = classifier(case_query.case)
74
+ if not isinstance(pred_cat, dict):
75
+ pred_cat = {case_query.attribute_name: pred_cat}
76
+ target = {case_query.attribute_name: case_query.target_value}
77
+ precision, recall = calculate_precision_and_recall(pred_cat, target)
78
+ return all(recall) and all(precision)
79
+
80
+
81
+ def load_or_create_func_rdr_model(func, model_dir: str, rdr_type: Type[RippleDownRules],
82
+ **rdr_kwargs) -> RippleDownRules:
83
+ """
84
+ Load the RDR model of the function if it exists, otherwise create a new one.
85
+
86
+ :param func: The function to load the model for.
87
+ :param model_dir: The directory where the model is stored.
88
+ :param rdr_type: The type of the RDR model to load.
89
+ :param rdr_kwargs: Additional arguments to pass to the RDR constructor in the case of a new model.
90
+ """
91
+ model_name = get_func_rdr_model_name(func)
92
+ model_path = os.path.join(model_dir, model_name, "rdr_metadata", f"{model_name}.json")
93
+ if os.path.exists(model_path):
94
+ rdr = rdr_type.load(load_dir=model_dir, model_name=model_name)
95
+ else:
96
+ rdr = rdr_type(**rdr_kwargs)
97
+ return rdr