tricc-oo 1.6.25__py3-none-any.whl → 1.6.27__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.
tests/build.py CHANGED
@@ -158,7 +158,7 @@ if __name__ == "__main__":
158
158
  if debug_level is not None:
159
159
  setup_logger("default", debug_file_path, LEVELS[debug_level])
160
160
  elif "pydevd" in sys.modules:
161
- setup_logger("default", debug_file_path, logging.DEBUG)
161
+ setup_logger("default", debug_file_path, logging.INFO)
162
162
  else:
163
163
  setup_logger("default", debug_file_path, logging.INFO)
164
164
  file_content = []
@@ -0,0 +1,127 @@
1
+ import unittest
2
+ from tricc_oo.models.base import (
3
+ TriccOperator, TriccOperation, TriccStatic, TriccReference,
4
+ clean_and_list, not_clean
5
+ )
6
+
7
+
8
+ class TestCleanAndList(unittest.TestCase):
9
+
10
+ def test_empty_list(self):
11
+ """Test with empty list"""
12
+ result = clean_and_list([])
13
+ self.assertEqual(result, [])
14
+
15
+ def test_single_true(self):
16
+ """Test single TRUE is removed"""
17
+ result = clean_and_list([TriccStatic(True)])
18
+ self.assertEqual(result, [])
19
+
20
+ def test_single_false(self):
21
+ """Test single FALSE returns [FALSE]"""
22
+ result = clean_and_list([TriccStatic(False)])
23
+ self.assertEqual(result, [TriccStatic(False)])
24
+
25
+ def test_mixed_true_false(self):
26
+ """Test TRUE and FALSE returns [FALSE]"""
27
+ result = clean_and_list([TriccStatic(True), TriccStatic(False)])
28
+ self.assertEqual(result, [TriccStatic(False)])
29
+
30
+ def test_and_flattening(self):
31
+ """Test AND operations are flattened"""
32
+ a = TriccReference("a")
33
+ b = TriccReference("b")
34
+ c = TriccReference("c")
35
+ and_op = TriccOperation(TriccOperator.AND, [a, b])
36
+ result = clean_and_list([and_op, c])
37
+ expected = sorted([a, b, c], key=str)
38
+ self.assertEqual(result, expected)
39
+
40
+ def test_nested_and_flattening(self):
41
+ """Test nested AND operations are flattened"""
42
+ a = TriccReference("a")
43
+ b = TriccReference("b")
44
+ c = TriccReference("c")
45
+ d = TriccReference("d")
46
+ inner_and = TriccOperation(TriccOperator.AND, [a, b])
47
+ outer_and = TriccOperation(TriccOperator.AND, [inner_and, c])
48
+ result = clean_and_list([outer_and, d])
49
+ expected = sorted([a, b, c, d], key=str)
50
+ self.assertEqual(result, expected)
51
+
52
+ def test_contradiction_detection_exists(self):
53
+ """Test EXISTS(a) and NOTEXISTS(a) returns [FALSE]"""
54
+ a = TriccReference("a")
55
+ exists_a = TriccOperation(TriccOperator.EXISTS, [a])
56
+ not_exists_a = TriccOperation(TriccOperator.NOTEXISTS, [a])
57
+ result = clean_and_list([exists_a, not_exists_a])
58
+ self.assertEqual(result, [TriccStatic(False)])
59
+
60
+ def test_contradiction_detection_boolean_ops(self):
61
+ """Test boolean operation and its negation returns [FALSE]"""
62
+ a = TriccReference("a")
63
+ is_true_a = TriccOperation(TriccOperator.ISTRUE, [a])
64
+ is_false_a = TriccOperation(TriccOperator.ISFALSE, [a])
65
+ result = clean_and_list([is_true_a, is_false_a])
66
+ self.assertEqual(result, [TriccStatic(False)])
67
+
68
+ def test_remove_trues(self):
69
+ """Test TRUE values are removed"""
70
+ a = TriccReference("a")
71
+ b = TriccReference("b")
72
+ result = clean_and_list([a, TriccStatic(True), b])
73
+ expected = sorted([a, b], key=str)
74
+ self.assertEqual(result, expected)
75
+
76
+ def test_not_and_optimization_simple(self):
77
+ """Test NOT(AND(a, b)) with a present removes a from AND"""
78
+ a = TriccReference("a")
79
+ b = TriccReference("b")
80
+ not_and = TriccOperation(TriccOperator.NOT, [
81
+ TriccOperation(TriccOperator.AND, [a, b])
82
+ ])
83
+ result = clean_and_list([a, not_and])
84
+ # Should simplify NOT(AND(b)) which is equivalent to OR(NOT(b))
85
+ # But let's see what the current logic does
86
+ expected = sorted([a, not_and], key=str)
87
+ self.assertEqual(result, expected)
88
+
89
+ def test_basic_uniqueness(self):
90
+ """Test duplicate elements are removed"""
91
+ a = TriccReference("a")
92
+ result = clean_and_list([a, a, a])
93
+ self.assertEqual(result, [a])
94
+
95
+ def test_mixed_types(self):
96
+ """Test with mixed reference and static types"""
97
+ ref = TriccReference("test")
98
+ static = TriccStatic("value")
99
+ result = clean_and_list([ref, static, TriccStatic(True)])
100
+ expected = sorted([ref, static], key=str)
101
+ self.assertEqual(result, expected)
102
+
103
+ def test_false_with_others(self):
104
+ """Test FALSE with other elements returns [FALSE]"""
105
+ a = TriccReference("a")
106
+ b = TriccReference("b")
107
+ result = clean_and_list([a, TriccStatic(False), b])
108
+ self.assertEqual(result, [TriccStatic(False)])
109
+
110
+ def test_only_trues(self):
111
+ """Test list of only TRUEs returns empty"""
112
+ result = clean_and_list([TriccStatic(True), TriccStatic(True)])
113
+ self.assertEqual(result, [])
114
+
115
+ def test_complex_and_flattening(self):
116
+ """Test complex AND flattening with TRUEs and duplicates"""
117
+ a = TriccReference("a")
118
+ b = TriccReference("b")
119
+ c = TriccReference("c")
120
+ and_op = TriccOperation(TriccOperator.AND, [a, TriccStatic(True), b])
121
+ result = clean_and_list([and_op, c, TriccStatic(True)])
122
+ expected = sorted([a, b, c], key=str)
123
+ self.assertEqual(result, expected)
124
+
125
+
126
+ if __name__ == "__main__":
127
+ unittest.main()
tricc_oo/models/base.py CHANGED
@@ -603,7 +603,7 @@ def clean_and_list(argv):
603
603
  internal = list(set(argv))
604
604
  for a in internal:
605
605
  for b in internal[internal.index(a) + 1:]:
606
- if not_clean(b) == a:
606
+ if not_clean(a) == b or not_clean(b) == a:
607
607
  return [TriccStatic(False)]
608
608
  return sorted(list(set(argv)), key=str)
609
609
 
@@ -777,4 +777,384 @@ def nand_join(left, right):
777
777
  return and_join([left, not_clean(right)])
778
778
 
779
779
 
780
+ def simplify_with_sympy(operation):
781
+ """
782
+ Simplify a TriccOperation using SymPy boolean and algebraic operations.
783
+
784
+ This enhanced version maps Tricc operators to actual SymPy operations/symbols
785
+ instead of treating them as placeholders, enabling sophisticated mixed-domain
786
+ simplifications.
787
+
788
+ Args:
789
+ operation: TriccOperation to simplify
790
+
791
+ Returns:
792
+ Simplified TriccOperation or original if no simplification possible
793
+ """
794
+ # Step 1: Check if operation contains operations that can be simplified
795
+ if not isinstance(operation, TriccOperation):
796
+ return operation
797
+
798
+ try:
799
+ import sympy as sp
800
+ from sympy.logic.boolalg import simplify_logic
801
+ from sympy import Add, Mul, Pow, Eq, Ne, And, Or, Not, Gt, Ge, Lt, Le
802
+ from sympy.functions import Abs, sign
803
+ except ImportError:
804
+ logger.warning("SymPy not available, skipping algebraic simplification")
805
+ return operation
806
+
807
+ # Step 2: Create comprehensive Tricc-to-SymPy operator mapping
808
+ tricc_to_sympy_map = {
809
+ # Boolean operations - treat as symbols to preserve structure
810
+ TriccOperator.AND: And,
811
+ TriccOperator.OR: Or,
812
+ TriccOperator.NOT: Not,
813
+ TriccOperator.ISTRUE: None, # Will be treated as symbol
814
+ TriccOperator.ISFALSE: None, # Will be treated as symbol
815
+ TriccOperator.EXISTS: None, # Will be treated as symbol
816
+ TriccOperator.NOTEXISTS: None, # Will be treated as symbol
817
+ TriccOperator.ISNOTTRUE: None, # Will be treated as symbol
818
+ TriccOperator.ISNOTFALSE: None, # Will be treated as symbol
819
+
820
+ # Arithmetic operations
821
+ TriccOperator.PLUS: Add,
822
+ TriccOperator.MINUS: lambda *args: Add(args[0], Mul(-1, args[1])) if len(args) == 2 else Mul(-1, args[0]),
823
+ TriccOperator.MULTIPLIED: Mul,
824
+ TriccOperator.DIVIDED: lambda x, y: Mul(x, Pow(y, -1)),
825
+ TriccOperator.MODULO: lambda x, y: x % y, # Modulo
826
+
827
+ # Comparison operations
828
+ TriccOperator.EQUAL: Eq,
829
+ TriccOperator.NOTEQUAL: Ne,
830
+ TriccOperator.MORE: Gt,
831
+ TriccOperator.LESS: Lt,
832
+ TriccOperator.MORE_OR_EQUAL: Ge,
833
+ TriccOperator.LESS_OR_EQUAL: Le,
834
+
835
+ # Numeric operations
836
+ TriccOperator.ROUND: lambda x: sp.round(x),
837
+ TriccOperator.CAST_NUMBER: lambda x: x, # Identity for numbers
838
+ TriccOperator.CAST_INTEGER: lambda x: sp.floor(x),
839
+
840
+ # Special operations (placeholders for now)
841
+ TriccOperator.COALESCE: None, # Will use placeholder
842
+ TriccOperator.CONCATENATE: None, # Will use placeholder
843
+ TriccOperator.CASE: None, # Will use placeholder
844
+ TriccOperator.IFS: None, # Will use placeholder
845
+ }
846
+
847
+ # Step 3: Create reference mapping with structural keys
848
+ ref_counter = 0
849
+ ref_map = {} # SymPy symbol -> actual reference
850
+ reverse_map = {} # structural key -> SymPy symbol
851
+
852
+ def get_structural_key(ref):
853
+ """Generate a canonical key based on structural equality"""
854
+ if isinstance(ref, TriccOperation):
855
+ ref_keys = [get_structural_key(r) for r in ref.reference]
856
+ return f"op_{ref.operator}_{'_'.join(ref_keys)}"
857
+ elif isinstance(ref, TriccReference):
858
+ return f"ref_{ref.value}"
859
+ elif isinstance(ref, TriccStatic):
860
+ return f"static_{ref.value}"
861
+ elif hasattr(ref, 'id'):
862
+ return f"{ref.__class__.__name__}_{ref.id}"
863
+ else:
864
+ return str(ref)
865
+
866
+ def get_sympy_symbol(ref):
867
+ """Get or create SymPy symbol for a reference"""
868
+ nonlocal ref_counter
869
+ ref_key = get_structural_key(ref)
870
+ if ref_key in reverse_map:
871
+ return reverse_map[ref_key]
872
+
873
+ # Create unique symbol name
874
+ if hasattr(ref, 'reference') and isinstance(ref.reference, (list, OrderedSet)):
875
+ # For operations, create path-based name
876
+ path_parts = []
877
+ current = ref
878
+ depth = 0
879
+ while hasattr(current, 'reference') and depth < 3:
880
+ if isinstance(current.reference, (list, OrderedSet)) and len(current.reference) > 0:
881
+ path_parts.insert(0, str(len(current.reference)))
882
+ current = current.reference[0]
883
+ depth += 1
884
+ else:
885
+ break
886
+ if path_parts:
887
+ symbol_name = f"r_{'_'.join(path_parts)}"
888
+ else:
889
+ symbol_name = f"r_{ref_counter}"
890
+ else:
891
+ symbol_name = f"r_{ref_counter}"
892
+
893
+ ref_counter += 1
894
+ symbol = sp.Symbol(symbol_name)
895
+ ref_map[symbol] = ref
896
+ reverse_map[ref_key] = symbol
897
+ return symbol
898
+
899
+ def tricc_to_sympy_expr(op):
900
+ """Convert TriccOperation to SymPy expression"""
901
+ if isinstance(op, TriccOperation):
902
+ # Special handling for boolean operations
903
+ if op.operator == TriccOperator.ISTRUE:
904
+ # istrue(x) becomes a symbol for the entire operation
905
+ return get_sympy_symbol(op)
906
+ elif op.operator == TriccOperator.ISNOTTRUE:
907
+ # isnottrue(x) becomes not(istrue(x))
908
+ if len(op.reference) == 1:
909
+ istrue_op = TriccOperation(TriccOperator.ISTRUE, op.reference)
910
+ istrue_symbol = get_sympy_symbol(istrue_op)
911
+ return Not(istrue_symbol)
912
+ else:
913
+ return get_sympy_symbol(op)
914
+ elif op.operator == TriccOperator.NOT and len(op.reference) == 1:
915
+ # Handle NOT(boolean_operation) by applying SymPy to inner and wrapping
916
+ inner = op.reference[0]
917
+ if isinstance(inner, TriccOperation) and inner.get_datatype() == "boolean":
918
+ # Apply SymPy to the inner boolean expression
919
+ inner_sympy = tricc_to_sympy_expr(inner)
920
+ if inner_sympy != get_sympy_symbol(inner):
921
+ # Inner was simplified, wrap with NOT
922
+ return Not(inner_sympy)
923
+ else:
924
+ # Inner wasn't simplified, return as symbol
925
+ return get_sympy_symbol(op)
926
+ else:
927
+ # Not a boolean operation, handle normally
928
+ sympy_op = tricc_to_sympy_map.get(op.operator)
929
+ if sympy_op is None:
930
+ return get_sympy_symbol(op)
931
+
932
+ sympy_refs = [tricc_to_sympy_expr(ref) for ref in op.reference]
933
+ if callable(sympy_op):
934
+ try:
935
+ if len(sympy_refs) == 1:
936
+ return sympy_op(sympy_refs[0])
937
+ elif len(sympy_refs) == 2:
938
+ return sympy_op(sympy_refs[0], sympy_refs[1])
939
+ else:
940
+ result = sympy_refs[0]
941
+ for ref in sympy_refs[1:]:
942
+ result = sympy_op(result, ref)
943
+ return result
944
+ except Exception as e:
945
+ logger.debug(f"Failed to apply {sympy_op} to {sympy_refs}: {e}")
946
+ return get_sympy_symbol(op)
947
+ else:
948
+ try:
949
+ return sympy_op(*sympy_refs)
950
+ except Exception as e:
951
+ logger.debug(f"Failed to create {sympy_op} with {sympy_refs}: {e}")
952
+ return get_sympy_symbol(op)
953
+ else:
954
+ # Normal operation handling
955
+ sympy_op = tricc_to_sympy_map.get(op.operator)
956
+
957
+ if sympy_op is None:
958
+ # Use placeholder for unmapped operations
959
+ return get_sympy_symbol(op)
960
+
961
+ # Convert references to SymPy expressions
962
+ sympy_refs = [tricc_to_sympy_expr(ref) for ref in op.reference]
963
+
964
+ # Apply the SymPy operation
965
+ if callable(sympy_op):
966
+ try:
967
+ if len(sympy_refs) == 1:
968
+ return sympy_op(sympy_refs[0])
969
+ elif len(sympy_refs) == 2:
970
+ return sympy_op(sympy_refs[0], sympy_refs[1])
971
+ else:
972
+ # For operations with multiple args, apply sequentially
973
+ result = sympy_refs[0]
974
+ for ref in sympy_refs[1:]:
975
+ result = sympy_op(result, ref)
976
+ return result
977
+ except Exception as e:
978
+ logger.debug(f"Failed to apply {sympy_op} to {sympy_refs}: {e}")
979
+ return get_sympy_symbol(op)
980
+ else:
981
+ # Direct SymPy operation class
982
+ try:
983
+ return sympy_op(*sympy_refs)
984
+ except Exception as e:
985
+ logger.debug(f"Failed to create {sympy_op} with {sympy_refs}: {e}")
986
+ return get_sympy_symbol(op)
987
+
988
+ elif isinstance(op, TriccStatic):
989
+ if isinstance(op.value, bool):
990
+ return sp.true if op.value else sp.false
991
+ elif isinstance(op.value, (int, float)):
992
+ return sp.S(op.value)
993
+ else:
994
+ return get_sympy_symbol(op)
995
+ else:
996
+ # References and other objects
997
+ return get_sympy_symbol(op)
998
+
999
+ def sympy_expr_to_tricc(expr):
1000
+ """Convert SymPy expression back to TriccOperation using expression tree"""
1001
+ if isinstance(expr, sp.Symbol):
1002
+ # Direct symbol lookup
1003
+ if expr in ref_map:
1004
+ return ref_map[expr]
1005
+ else:
1006
+ # This shouldn't happen, but handle gracefully
1007
+ return TriccReference(str(expr))
1008
+
1009
+ elif expr is sp.true:
1010
+ return TriccStatic(True)
1011
+ elif expr is sp.false:
1012
+ return TriccStatic(False)
1013
+
1014
+ elif isinstance(expr, (sp.Integer, sp.Float)):
1015
+ return TriccStatic(float(expr))
1016
+
1017
+
1018
+
1019
+ elif isinstance(expr, And):
1020
+ args = [sympy_expr_to_tricc(arg) for arg in expr.args]
1021
+ return and_join(args)
1022
+
1023
+ elif isinstance(expr, Or):
1024
+ args = [sympy_expr_to_tricc(arg) for arg in expr.args]
1025
+ return or_join(args)
1026
+
1027
+ elif isinstance(expr, Not):
1028
+ inner = sympy_expr_to_tricc(expr.args[0])
1029
+ return TriccOperation(TriccOperator.NOT, [inner])
1030
+
1031
+ elif isinstance(expr, Add):
1032
+ if len(expr.args) == 2:
1033
+ left, right = [sympy_expr_to_tricc(arg) for arg in expr.args]
1034
+ # Check if right is negative (indicating subtraction)
1035
+ if isinstance(right, TriccOperation) and right.operator == TriccOperator.MINUS and len(right.reference) == 1:
1036
+ return TriccOperation(TriccOperator.MINUS, [left, right.reference[0]])
1037
+ else:
1038
+ return TriccOperation(TriccOperator.PLUS, [left, right])
1039
+ else:
1040
+ # Multiple addition
1041
+ args = [sympy_expr_to_tricc(arg) for arg in expr.args]
1042
+ return TriccOperation(TriccOperator.PLUS, args)
1043
+
1044
+ elif isinstance(expr, Mul):
1045
+ if len(expr.args) == 2:
1046
+ left, right = [sympy_expr_to_tricc(arg) for arg in expr.args]
1047
+ # Check for division (multiplication by reciprocal)
1048
+ if isinstance(right, TriccOperation) and right.operator == TriccOperator.DIVIDED:
1049
+ if len(right.reference) == 1:
1050
+ return TriccOperation(TriccOperator.DIVIDED, [left, right.reference[0]])
1051
+ else:
1052
+ return TriccOperation(TriccOperator.MULTIPLIED, [left, right])
1053
+ else:
1054
+ # Multiple multiplication
1055
+ args = [sympy_expr_to_tricc(arg) for arg in expr.args]
1056
+ return TriccOperation(TriccOperator.MULTIPLIED, args)
1057
+
1058
+ elif isinstance(expr, Pow):
1059
+ # Handle division (x^(-1) = 1/x)
1060
+ if len(expr.args) == 2 and expr.args[1] == -1:
1061
+ numerator = sympy_expr_to_tricc(expr.args[0])
1062
+ denominator = TriccStatic(1)
1063
+ return TriccOperation(TriccOperator.DIVIDED, [numerator, denominator])
1064
+ else:
1065
+ # Other powers - fallback to placeholder
1066
+ return TriccReference(str(expr))
1067
+
1068
+ elif isinstance(expr, (Eq, Ne, Lt, Le, Gt, Ge)):
1069
+ left, right = [sympy_expr_to_tricc(arg) for arg in expr.args]
1070
+ op_map = {
1071
+ Eq: TriccOperator.EQUAL,
1072
+ Ne: TriccOperator.NOTEQUAL,
1073
+ Lt: TriccOperator.LESS,
1074
+ Le: TriccOperator.LESS_OR_EQUAL,
1075
+ Gt: TriccOperator.MORE,
1076
+ Ge: TriccOperator.MORE_OR_EQUAL,
1077
+ }
1078
+ operator = op_map.get(type(expr), TriccOperator.EQUAL)
1079
+ return TriccOperation(operator, [left, right])
1080
+
1081
+ else:
1082
+ # Unknown SymPy expression type - fallback to string representation
1083
+ logger.debug(f"Unknown SymPy expression type: {type(expr)}, falling back to placeholder")
1084
+ return TriccReference(str(expr))
1085
+
1086
+ # Step 4: Apply custom boolean simplifications first
1087
+ if operation.get_datatype() == "boolean":
1088
+ # Apply our custom boolean simplifications
1089
+ custom_simplified = apply_boolean_simplifications(operation)
1090
+ if custom_simplified != operation:
1091
+ logger.debug(f"Custom boolean simplification: {operation} -> {custom_simplified}")
1092
+ return custom_simplified
1093
+
1094
+ # Step 5: Convert to SymPy and simplify
1095
+ try:
1096
+ sympy_expr = tricc_to_sympy_expr(operation)
1097
+ logger.debug(f"Original SymPy expression: {sympy_expr}")
1098
+
1099
+ # Apply appropriate simplification based on expression type
1100
+ if operation.get_datatype() == "boolean":
1101
+ # Use boolean algebra simplification
1102
+ simplified_expr = simplify_logic(sympy_expr)
1103
+ else:
1104
+ # Use general algebraic simplification
1105
+ simplified_expr = sp.simplify(sympy_expr)
1106
+
1107
+ logger.debug(f"Simplified SymPy expression: {simplified_expr}")
1108
+
1109
+ # Step 6: Convert back to TriccOperation if different
1110
+ if simplified_expr != sympy_expr:
1111
+ reconstructed = sympy_expr_to_tricc(simplified_expr)
1112
+ logger.debug(f"SymPy simplified {operation} to {reconstructed}")
1113
+ return reconstructed
1114
+
1115
+ except Exception as e:
1116
+ logger.warning(f"SymPy simplification failed for {operation}: {e}")
1117
+
1118
+ return operation
1119
+
1120
+
1121
+ def apply_boolean_simplifications(operation):
1122
+ """Apply custom boolean simplifications that SymPy might miss"""
1123
+ if not isinstance(operation, TriccOperation):
1124
+ return operation
1125
+
1126
+ if operation.operator == TriccOperator.OR:
1127
+ # Apply clean_or_list which includes our pattern recognition
1128
+ cleaned_list = clean_or_list(set(operation.reference))
1129
+ if len(cleaned_list) == 1:
1130
+ return cleaned_list[0]
1131
+ elif len(cleaned_list) != len(operation.reference):
1132
+ return TriccOperation(TriccOperator.OR, cleaned_list)
1133
+
1134
+ elif operation.operator == TriccOperator.AND:
1135
+ # Apply clean_and_list
1136
+ cleaned_list = clean_and_list(operation.reference)
1137
+ if len(cleaned_list) == 1:
1138
+ return cleaned_list[0]
1139
+ elif len(cleaned_list) != len(operation.reference):
1140
+ return TriccOperation(TriccOperator.AND, cleaned_list)
1141
+
1142
+ # Recursively apply to sub-operations
1143
+ simplified_refs = []
1144
+ changed = False
1145
+ for ref in operation.reference:
1146
+ if isinstance(ref, TriccOperation):
1147
+ simplified_ref = apply_boolean_simplifications(ref)
1148
+ if simplified_ref != ref:
1149
+ changed = True
1150
+ simplified_refs.append(simplified_ref)
1151
+ else:
1152
+ simplified_refs.append(ref)
1153
+
1154
+ if changed:
1155
+ return TriccOperation(operation.operator, simplified_refs)
1156
+
1157
+ return operation
1158
+
1159
+
780
1160
  TriccGroup.update_forward_refs()
@@ -285,6 +285,7 @@ class DrawioStrategy(BaseInputStrategy):
285
285
  else:
286
286
  # all good, only one target node found
287
287
  linked_target_node = link_in_list[0]
288
+
288
289
  # steal the edges
289
290
  replace_node(node, linked_target_node, page)
290
291
 
@@ -13,7 +13,8 @@ from pyxform import create_survey_from_xls
13
13
  from tricc_oo.converters.utils import clean_name
14
14
  from tricc_oo.models.base import (
15
15
  TriccOperator,
16
- TriccOperation, TriccStatic, TriccReference
16
+ TriccOperation, TriccStatic, TriccReference,
17
+ simplify_with_sympy
17
18
  )
18
19
  from tricc_oo.models.ordered_set import OrderedSet
19
20
  from tricc_oo.models.calculate import (
@@ -379,6 +380,9 @@ class XLSFormStrategy(BaseOutPutStrategy):
379
380
  return processed_nodes
380
381
 
381
382
  def get_tricc_operation_expression(self, operation):
383
+ # Apply SymPy boolean simplification for boolean operations
384
+ #operation = simplify_with_sympy(operation)
385
+
382
386
  ref_expressions = []
383
387
  original_references = []
384
388
  if not hasattr(operation, "reference"):
@@ -126,7 +126,7 @@ def get_last_version(name, processed_nodes, _list=None):
126
126
  def get_node_expressions(node, processed_nodes, process=None):
127
127
  get_overall_exp = issubclass(
128
128
  node.__class__,
129
- (TriccNodeDisplayCalculateBase, TriccNodeProposedDiagnosis, TriccNodeDiagnosis)
129
+ (TriccNodeDisplayCalculateBase, TriccNodeProposedDiagnosis, TriccNodeDiagnosis, TriccNodeActivity)
130
130
  ) and not isinstance(node, (TriccNodeDisplayBridge))
131
131
  expression = None
132
132
  # in case of recursive call processed_nodes will be None
@@ -176,7 +176,7 @@ def get_version_inheritance(node, all_prev_versions, processed_nodes):
176
176
  expression = node.expression or node.expression_reference or getattr(node, "relevance", None)
177
177
  # Merge with ALL previous versions, not just the last one
178
178
  if all_prev_versions:
179
- expression = merge_all_expressions(expression, all_prev_versions)
179
+ expression = merge_expressions(expression, *all_prev_versions)
180
180
  if node.expression:
181
181
  node.expression = expression
182
182
  elif node.expression_reference:
@@ -216,39 +216,15 @@ def get_version_inheritance(node, all_prev_versions, processed_nodes):
216
216
  node.expression = TriccOperation(TriccOperator.COALESCE, coalesce_operands)
217
217
 
218
218
 
219
- def merge_expression(expression, last_version):
219
+ def merge_expressions(expression, last_version, *argv):
220
220
  datatype = expression.get_datatype()
221
221
  if datatype == "boolean":
222
- expression = or_join([TriccOperation(TriccOperator.ISTRUE, [last_version]), expression])
222
+ expression = or_join([TriccOperation(TriccOperator.ISTRUE, [last_version, *argv]), expression])
223
223
 
224
224
  elif datatype == "number":
225
- expression = TriccOperation(TriccOperator.PLUS, [last_version, expression])
225
+ expression = TriccOperation(TriccOperator.PLUS, [last_version, *argv, expression])
226
226
  else:
227
- expression = TriccOperation(TriccOperator.COALESCE, [last_version, expression])
228
- return expression
229
-
230
-
231
- def merge_all_expressions(expression, all_versions):
232
- """
233
- Merge an expression with ALL previous versions, not just the last one.
234
- This ensures inheritance works even when intermediate versions weren't evaluated
235
- due to activity relevance conditions.
236
- """
237
- if not all_versions:
238
- return expression
239
-
240
- datatype = expression.get_datatype() if expression else "unknown"
241
-
242
- if datatype == "boolean":
243
- expression = or_join([expression, *all_versions])
244
-
245
- else:
246
- # COALESCE through all previous versions, then the current expression
247
- coalesce_operands = list(all_versions)
248
- if expression:
249
- coalesce_operands.append(expression)
250
- expression = TriccOperation(TriccOperator.COALESCE, coalesce_operands)
251
-
227
+ expression = TriccOperation(TriccOperator.COALESCE, [expression, last_version, *argv])
252
228
  return expression
253
229
 
254
230
 
@@ -315,14 +291,23 @@ def load_calculate(
315
291
  add_used_calculate(node, r, calculates, used_calculates, processed_nodes)
316
292
 
317
293
  if last_version and hasattr(node, "relevance"):
294
+ last_expressions_other_activity = [
295
+ and_join([TriccOperation(TriccOperator.ISTRUE,[l]),l.activity.root]) for l in all_prev_versions if (
296
+ node.is_sequence_defined and
297
+ node.activity.base_instance != l.activity.base_instance
298
+ )
299
+ ]
300
+ last_expression_same_activity = [
301
+ TriccOperation(TriccOperator.ISTRUE,l) for l in all_prev_versions if (
302
+ node.is_sequence_defined and
303
+ node.activity == l.activity
304
+ )
305
+ ]
306
+ last_version_relevance = [*last_expressions_other_activity, *last_expression_same_activity]
318
307
  if isinstance(node, TriccNodeInputModel):
319
308
  version_relevance = TriccOperation(TriccOperator.ISNULL, [last_version])
320
- elif last_version.relevance:
321
- version_relevance = not_clean(last_version.relevance)
322
- elif last_version.activity.relevance:
323
- version_relevance = not_clean(
324
- last_version.activity.relevance,
325
- )
309
+ elif last_version_relevance:
310
+ version_relevance = not_clean(or_join(last_version_relevance))
326
311
  else:
327
312
  version_relevance = None
328
313
 
@@ -582,7 +567,7 @@ def generate_calculates(node, calculates, used_calculates, processed_nodes, proc
582
567
  node.activity.calculates.append(calc_node)
583
568
  last_version = set_last_version_false(calc_node, processed_nodes)
584
569
  if last_version:
585
- calc_node.expression = merge_expression(calc_node.expression, last_version)
570
+ calc_node.expression = merge_expressions(calc_node.expression, last_version)
586
571
  processed_nodes.add(calc_node)
587
572
  logger.debug(
588
573
  "generate_save_calculate:{}:{} as {}".format(
@@ -615,7 +600,7 @@ def generate_calculates(node, calculates, used_calculates, processed_nodes, proc
615
600
  node.activity.calculates.append(calc_node)
616
601
  last_version = set_last_version_false(calc_node, processed_nodes)
617
602
  if last_version:
618
- calc_node.expression = merge_expression(calc_node.expression, last_version)
603
+ calc_node.expression = merge_expressions(calc_node.expression, last_version)
619
604
  processed_nodes.add(calc_node)
620
605
  list_calc.append(calc_node)
621
606
  node.activity.nodes[calc_node.id] = calc_node
@@ -1988,14 +1973,25 @@ def get_node_expression(in_node, processed_nodes, get_overall_exp=False, is_prev
1988
1973
  (not is_prev or not ONE_QUESTION_AT_A_TIME)
1989
1974
  and hasattr(node, "relevance")
1990
1975
  and isinstance(node.relevance, (TriccOperation, TriccStatic))
1976
+ and getattr(node, 'is_sequence_defined', False)
1977
+ and not get_overall_exp
1991
1978
  ):
1992
1979
  expression = node.relevance
1993
- elif ONE_QUESTION_AT_A_TIME and is_prev and not get_overall_exp and hasattr(node, "required") and node.required:
1980
+ elif (
1981
+ ONE_QUESTION_AT_A_TIME
1982
+ and is_prev and not get_overall_exp
1983
+ and hasattr(node, "required")
1984
+ and node.required
1985
+ and getattr(node, 'is_sequence_defined', False)
1986
+ ):
1994
1987
  expression = get_required_node_expression(node)
1995
-
1996
1988
  if expression is None:
1997
1989
  expression = get_prev_node_expression(
1998
- node, processed_nodes=processed_nodes, get_overall_exp=get_overall_exp, process=process
1990
+ node,
1991
+ activity=node.activity,
1992
+ processed_nodes=processed_nodes,
1993
+ get_overall_exp=get_overall_exp,
1994
+ process=process
1999
1995
  )
2000
1996
  # in_node not in processed_nodes is need for calculates that can but run after the end of the activity
2001
1997
  # if isinstance(node, TriccNodeActivitiy) and not prev:
@@ -2218,8 +2214,9 @@ def create_determine_diagnosis_activity(diags):
2218
2214
  return activity
2219
2215
 
2220
2216
 
2221
- def get_prev_node_expression(node, processed_nodes, get_overall_exp=False, excluded_name=None, process=None):
2217
+ def get_prev_node_expression(node, activity, processed_nodes, get_overall_exp=False, excluded_name=None, process=None):
2222
2218
  expression = None
2219
+ sub = None
2223
2220
  if node is None:
2224
2221
  pass
2225
2222
  # when getting the prev node, we calculate the
@@ -2228,12 +2225,19 @@ def get_prev_node_expression(node, processed_nodes, get_overall_exp=False, exclu
2228
2225
  expression_inputs = clean_or_list(expression_inputs)
2229
2226
  else:
2230
2227
  expression_inputs = []
2231
- prev_activities = {}
2228
+ prev_activities = {node.activity.id: []}
2229
+ # sorting prev_nodes per activity
2230
+
2232
2231
  for prev_node in node.prev_nodes:
2233
2232
  if prev_node.activity.id not in prev_activities:
2234
2233
  prev_activities[prev_node.activity.id] = []
2234
+ if isinstance(prev_node, TriccNodeActivity):
2235
+ for a_prev_node in prev_node.prev_nodes:
2236
+ # if we share the calling contect of the activity
2237
+ if a_prev_node.activity == node.activity:
2238
+ prev_activities[node.activity.id].append(a_prev_node)
2235
2239
  prev_activities[prev_node.activity.id].append(prev_node)
2236
-
2240
+ # get the or_list expression of all the node per activity
2237
2241
  for act_id in prev_activities:
2238
2242
  act_expression_inputs = []
2239
2243
  none_sequence_defined_prev_node = False
@@ -2250,29 +2254,36 @@ def get_prev_node_expression(node, processed_nodes, get_overall_exp=False, exclu
2250
2254
  )
2251
2255
  ):
2252
2256
  # the rhombus should calculate only reference
2257
+ # expression from one prev node
2253
2258
  sub = get_node_expression(
2254
2259
  prev_node,
2255
2260
  processed_nodes=processed_nodes,
2256
2261
  get_overall_exp=get_overall_exp,
2257
2262
  is_prev=True,
2258
- process=get_overall_exp,
2259
- )
2260
- if isinstance(node, TriccNodeActivity) or get_overall_exp:
2263
+ process=process,
2264
+ ) or TriccStatic(True)
2265
+ # if it is an activity or overall then we add the sub to act expression
2266
+ # else we update directly the node releavance subs
2267
+ if node.activity.id != act_id or get_overall_exp:
2261
2268
  add_sub_expression(act_expression_inputs, sub)
2262
2269
  else:
2263
2270
  add_sub_expression(expression_inputs, sub)
2264
-
2271
+ # if cur prev node part of prev act elvaluated have some relevance we make an AND with prev act relevance
2265
2272
  if act_expression_inputs:
2266
2273
  act_sub = or_join(act_expression_inputs)
2267
2274
  # if there is condition fallback on the calling activity condition
2268
- act_relevance = get_node_expression(
2269
- prev_node.activity,
2270
- processed_nodes=processed_nodes,
2271
- get_overall_exp=get_overall_exp,
2272
- is_prev=True,
2273
- negate=False,
2274
- process=process,
2275
- )
2275
+ if prev_node.activity.relevance and prev_node.activity.relevance != TriccStatic(True):
2276
+ act_relevance = TriccOperation(TriccOperator.ISTRUE, [prev_node.activity.root])
2277
+ else:
2278
+ act_relevance = TriccStatic(True)
2279
+ # get_node_expression(
2280
+ # prev_node.activity,
2281
+ # processed_nodes=processed_nodes,
2282
+ # get_overall_exp=get_overall_exp,
2283
+ # is_prev=True,
2284
+ # negate=False,
2285
+ # process=process,
2286
+ # )
2276
2287
  if act_sub == TriccStatic(True):
2277
2288
  act_sub = act_relevance
2278
2289
  elif act_relevance != TriccStatic(True) and none_sequence_defined_prev_node:
@@ -2557,6 +2568,7 @@ def get_calculation_terms(node, processed_nodes, get_overall_exp=False, negate=F
2557
2568
  # because calculate are not the the activity group
2558
2569
  elif isinstance(node, (TriccNodeActivityStart)) and get_overall_exp:
2559
2570
  expression = get_prev_node_expression(
2571
+ node.activity,
2560
2572
  node.activity,
2561
2573
  processed_nodes=processed_nodes,
2562
2574
  get_overall_exp=get_overall_exp,
@@ -2605,7 +2617,7 @@ def get_calculation_terms(node, processed_nodes, get_overall_exp=False, negate=F
2605
2617
  expression = node.expression_reference
2606
2618
  elif expression is None:
2607
2619
  expression = get_prev_node_expression(
2608
- node, processed_nodes=processed_nodes, get_overall_exp=get_overall_exp, process=process
2620
+ node, node.activity, processed_nodes=processed_nodes, get_overall_exp=get_overall_exp, process=process
2609
2621
  )
2610
2622
 
2611
2623
  # manage the generic negation
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tricc-oo
3
- Version: 1.6.25
3
+ Version: 1.6.27
4
4
  Summary: Python library that converts CDSS L2 in L3
5
5
  Project-URL: Homepage, https://github.com/SwissTPH/tricc
6
6
  Project-URL: Issues, https://github.com/SwissTPH/tricc/issues
@@ -25,6 +25,7 @@ Requires-Dist: antlr4-tools==0.2.1
25
25
  Requires-Dist: beautifulsoup4
26
26
  Requires-Dist: ocldev
27
27
  Requires-Dist: pyxform
28
+ Requires-Dist: Sympy
28
29
  Dynamic: license-file
29
30
 
30
31
  # TRICC-OO
@@ -1,4 +1,5 @@
1
- tests/build.py,sha256=Qbxvjkj_Wk2nQ-WjaMGiE1FIe3SRmJMRIgeoMoxqlfQ,6748
1
+ tests/build.py,sha256=d_8QDUem7e77u_1BRE96xn9VXbG8N4L9pgpgG9fVWRw,6747
2
+ tests/test_clean_and_list.py,sha256=S8CcYHbnttToZy1COkN4PWtIiHuOFCD3dY9gFiVuxSI,4892
2
3
  tests/test_cql.py,sha256=IRfjrmcxMn60XlAQLUur-7OUELs1NioqxRXWcOf8QcQ,5678
3
4
  tests/to_ocl.py,sha256=4e-i65K3UM6wHgdVcrZcM9AyL1bahIsXJiZTXhhHgQk,2048
4
5
  tricc_oo/__init__.py,sha256=oWCE1ubmC_6iqaWOMgTei4eXVQgV202Ia-tXS1NnW_4,139
@@ -15,7 +16,7 @@ tricc_oo/converters/cql/cqlListener.py,sha256=fA7-8DcS2Q69ckwjdg57-OfFHBxjTZFdoS
15
16
  tricc_oo/converters/cql/cqlParser.py,sha256=x3KdrwX9nwENSEJ5Ex7_l5NMnu3kWBO0uLdYu4moTq0,414745
16
17
  tricc_oo/converters/cql/cqlVisitor.py,sha256=iHuup2S7OGSVWLEcI4H3oecRqgXztC1sKnew_1P2iGY,33880
17
18
  tricc_oo/models/__init__.py,sha256=CgS52LLqdDIaXHvZy08hhu_VaYw80OEdfL_llM9ICBA,108
18
- tricc_oo/models/base.py,sha256=f5EQtTSJH9aPanqCc0qD6579TgAirzYaZ5BtQ2Iu0xw,26136
19
+ tricc_oo/models/base.py,sha256=N2gPlAwCQxIiGJQjfu_49c9DkO3EjgJ1LirFgPNkSfQ,42422
19
20
  tricc_oo/models/calculate.py,sha256=uNP0IDUqPQcJq9Co05H8eX5wbR_DikSxuOHxfVE5Dxg,8018
20
21
  tricc_oo/models/lang.py,sha256=ZMRwdoPWe01wEDhOM0uRk-6rt3BkoAAZM8mZ61--s3A,2265
21
22
  tricc_oo/models/ocl.py,sha256=MybSeB6fgCOUVJ4aektff0vrrTZsyfwZ2Gt_pPBu_FY,8728
@@ -29,23 +30,23 @@ tricc_oo/serializers/xls_form.py,sha256=SZPvZK6y4l_2v3HuGWITHmKd5rTClhHWvbUU3Ren
29
30
  tricc_oo/strategies/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
31
  tricc_oo/strategies/input/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
32
  tricc_oo/strategies/input/base_input_strategy.py,sha256=BEODXS74na1QRRcJVQ4cxiD8F7uRqaLyhE3QzKpGVvk,3891
32
- tricc_oo/strategies/input/drawio.py,sha256=uXAUPhXOeg0Uk_BNqlCqFBW4cWNox4VfH559bj1fhC0,12767
33
+ tricc_oo/strategies/input/drawio.py,sha256=VCKAr_r-jJ_xJlyCMCH2-qSE2KIvc7fmR2y4X9QMv3k,12768
33
34
  tricc_oo/strategies/output/base_output_strategy.py,sha256=i9L5CVUqkEAMNyBsdHJ4xA7Nptr3myHr_fHHveDX1cU,8928
34
35
  tricc_oo/strategies/output/dhis2_form.py,sha256=jW9NW72_61ch1bHm8ShIH4xsJH-HMlZGPTT5txJxMUk,38278
35
36
  tricc_oo/strategies/output/fhir_form.py,sha256=hbL921pe1Doun4IQrJuZ_Sq2fCh98G3grYie5olC4uc,15740
36
37
  tricc_oo/strategies/output/html_form.py,sha256=qSleEZOMV_-Z04y-i-ucyd5rgAYWAyjPwMrw0IHtCRM,8604
37
38
  tricc_oo/strategies/output/openmrs_form.py,sha256=ne6TwAyhafR-WDs27QTKKFl85VD5sij_VEJtK6ZjOIE,28996
38
39
  tricc_oo/strategies/output/spice.py,sha256=QMeoismVC3PdbvwTK0PtUjWX9jl9780fbQIXn76fMXw,10761
39
- tricc_oo/strategies/output/xls_form.py,sha256=yz1G7Mdf4ZpdXaKU6YV8rjNQ288pwUzTh13aI5Up6AI,33884
40
+ tricc_oo/strategies/output/xls_form.py,sha256=n_C9J1_FGcwtnXePBbPUd1OTuCpdbNEzt_o6sBGqsCg,34038
40
41
  tricc_oo/strategies/output/xlsform_cdss.py,sha256=X00Lt5MzV8TX14dR4dFI1MqllI5S1e13bKbeysWM9uA,17435
41
42
  tricc_oo/strategies/output/xlsform_cht.py,sha256=66sTRqVL9ZUL8NpX2f0xCRQ-siUKcKphosR9y7wsbuM,28326
42
43
  tricc_oo/strategies/output/xlsform_cht_hf.py,sha256=xm6SKirV3nMZvM2w54_zJcXAeAgAkq-EEqGEjnOWv6c,988
43
44
  tricc_oo/visitors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
44
- tricc_oo/visitors/tricc.py,sha256=VL1Ds4a7gCA0V7Mo2qPipT2uV4dcXDK2_swLdxZavDA,114272
45
+ tricc_oo/visitors/tricc.py,sha256=vZebJR4gF-VJxNv1qZeTU08VrzAMjMsTX58artHtZKI,115411
45
46
  tricc_oo/visitors/utils.py,sha256=j83aAq5s5atXi3OC0jc_uJd54a8XrHHmizeeEbWZQJg,421
46
47
  tricc_oo/visitors/xform_pd.py,sha256=ryAnI3V9x3eTmJ2LNsUZfvl0_yfCqo6oBgeSu-WPqaE,9613
47
- tricc_oo-1.6.25.dist-info/licenses/LICENSE,sha256=Pz2eACSxkhsGfW9_iN60pgy-enjnbGTj8df8O3ebnQQ,16726
48
- tricc_oo-1.6.25.dist-info/METADATA,sha256=If9bVy3gEm3zT0OMh6UPuUJxsFndAk_MtcglJXUH4LQ,8600
49
- tricc_oo-1.6.25.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
50
- tricc_oo-1.6.25.dist-info/top_level.txt,sha256=NvbfMNAiy9m4b1unBsqpeOQWh4IgA1Xa33BtKA4abxk,15
51
- tricc_oo-1.6.25.dist-info/RECORD,,
48
+ tricc_oo-1.6.27.dist-info/licenses/LICENSE,sha256=Pz2eACSxkhsGfW9_iN60pgy-enjnbGTj8df8O3ebnQQ,16726
49
+ tricc_oo-1.6.27.dist-info/METADATA,sha256=JXCMTGTqMLUAzxIHCZ5JoXThXxyJCOzYJLC7qQAewlM,8621
50
+ tricc_oo-1.6.27.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
51
+ tricc_oo-1.6.27.dist-info/top_level.txt,sha256=NvbfMNAiy9m4b1unBsqpeOQWh4IgA1Xa33BtKA4abxk,15
52
+ tricc_oo-1.6.27.dist-info/RECORD,,