Coverage for /usr/lib/python3/dist-packages/sympy/assumptions/relation/binrel.py: 32%

95 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

1""" 

2General binary relations. 

3""" 

4from typing import Optional 

5 

6from sympy.core.singleton import S 

7from sympy.assumptions import AppliedPredicate, ask, Predicate, Q # type: ignore 

8from sympy.core.kind import BooleanKind 

9from sympy.core.relational import Eq, Ne, Gt, Lt, Ge, Le 

10from sympy.logic.boolalg import conjuncts, Not 

11 

12__all__ = ["BinaryRelation", "AppliedBinaryRelation"] 

13 

14 

15class BinaryRelation(Predicate): 

16 """ 

17 Base class for all binary relational predicates. 

18 

19 Explanation 

20 =========== 

21 

22 Binary relation takes two arguments and returns ``AppliedBinaryRelation`` 

23 instance. To evaluate it to boolean value, use :obj:`~.ask()` or 

24 :obj:`~.refine()` function. 

25 

26 You can add support for new types by registering the handler to dispatcher. 

27 See :obj:`~.Predicate()` for more information about predicate dispatching. 

28 

29 Examples 

30 ======== 

31 

32 Applying and evaluating to boolean value: 

33 

34 >>> from sympy import Q, ask, sin, cos 

35 >>> from sympy.abc import x 

36 >>> Q.eq(sin(x)**2+cos(x)**2, 1) 

37 Q.eq(sin(x)**2 + cos(x)**2, 1) 

38 >>> ask(_) 

39 True 

40 

41 You can define a new binary relation by subclassing and dispatching. 

42 Here, we define a relation $R$ such that $x R y$ returns true if 

43 $x = y + 1$. 

44 

45 >>> from sympy import ask, Number, Q 

46 >>> from sympy.assumptions import BinaryRelation 

47 >>> class MyRel(BinaryRelation): 

48 ... name = "R" 

49 ... is_reflexive = False 

50 >>> Q.R = MyRel() 

51 >>> @Q.R.register(Number, Number) 

52 ... def _(n1, n2, assumptions): 

53 ... return ask(Q.zero(n1 - n2 - 1), assumptions) 

54 >>> Q.R(2, 1) 

55 Q.R(2, 1) 

56 

57 Now, we can use ``ask()`` to evaluate it to boolean value. 

58 

59 >>> ask(Q.R(2, 1)) 

60 True 

61 >>> ask(Q.R(1, 2)) 

62 False 

63 

64 ``Q.R`` returns ``False`` with minimum cost if two arguments have same 

65 structure because it is antireflexive relation [1] by 

66 ``is_reflexive = False``. 

67 

68 >>> ask(Q.R(x, x)) 

69 False 

70 

71 References 

72 ========== 

73 

74 .. [1] https://en.wikipedia.org/wiki/Reflexive_relation 

75 """ 

76 

77 is_reflexive: Optional[bool] = None 

78 is_symmetric: Optional[bool] = None 

79 

80 def __call__(self, *args): 

81 if not len(args) == 2: 

82 raise ValueError("Binary relation takes two arguments, but got %s." % len(args)) 

83 return AppliedBinaryRelation(self, *args) 

84 

85 @property 

86 def reversed(self): 

87 if self.is_symmetric: 

88 return self 

89 return None 

90 

91 @property 

92 def negated(self): 

93 return None 

94 

95 def _compare_reflexive(self, lhs, rhs): 

96 # quick exit for structurally same arguments 

97 # do not check != here because it cannot catch the 

98 # equivalent arguments with different structures. 

99 

100 # reflexivity does not hold to NaN 

101 if lhs is S.NaN or rhs is S.NaN: 

102 return None 

103 

104 reflexive = self.is_reflexive 

105 if reflexive is None: 

106 pass 

107 elif reflexive and (lhs == rhs): 

108 return True 

109 elif not reflexive and (lhs == rhs): 

110 return False 

111 return None 

112 

113 def eval(self, args, assumptions=True): 

114 # quick exit for structurally same arguments 

115 ret = self._compare_reflexive(*args) 

116 if ret is not None: 

117 return ret 

118 

119 # don't perform simplify on args here. (done by AppliedBinaryRelation._eval_ask) 

120 # evaluate by multipledispatch 

121 lhs, rhs = args 

122 ret = self.handler(lhs, rhs, assumptions=assumptions) 

123 if ret is not None: 

124 return ret 

125 

126 # check reversed order if the relation is reflexive 

127 if self.is_reflexive: 

128 types = (type(lhs), type(rhs)) 

129 if self.handler.dispatch(*types) is not self.handler.dispatch(*reversed(types)): 

130 ret = self.handler(rhs, lhs, assumptions=assumptions) 

131 

132 return ret 

133 

134 

135class AppliedBinaryRelation(AppliedPredicate): 

136 """ 

137 The class of expressions resulting from applying ``BinaryRelation`` 

138 to the arguments. 

139 

140 """ 

141 

142 @property 

143 def lhs(self): 

144 """The left-hand side of the relation.""" 

145 return self.arguments[0] 

146 

147 @property 

148 def rhs(self): 

149 """The right-hand side of the relation.""" 

150 return self.arguments[1] 

151 

152 @property 

153 def reversed(self): 

154 """ 

155 Try to return the relationship with sides reversed. 

156 """ 

157 revfunc = self.function.reversed 

158 if revfunc is None: 

159 return self 

160 return revfunc(self.rhs, self.lhs) 

161 

162 @property 

163 def reversedsign(self): 

164 """ 

165 Try to return the relationship with signs reversed. 

166 """ 

167 revfunc = self.function.reversed 

168 if revfunc is None: 

169 return self 

170 if not any(side.kind is BooleanKind for side in self.arguments): 

171 return revfunc(-self.lhs, -self.rhs) 

172 return self 

173 

174 @property 

175 def negated(self): 

176 neg_rel = self.function.negated 

177 if neg_rel is None: 

178 return Not(self, evaluate=False) 

179 return neg_rel(*self.arguments) 

180 

181 def _eval_ask(self, assumptions): 

182 conj_assumps = set() 

183 binrelpreds = {Eq: Q.eq, Ne: Q.ne, Gt: Q.gt, Lt: Q.lt, Ge: Q.ge, Le: Q.le} 

184 for a in conjuncts(assumptions): 

185 if a.func in binrelpreds: 

186 conj_assumps.add(binrelpreds[type(a)](*a.args)) 

187 else: 

188 conj_assumps.add(a) 

189 

190 # After CNF in assumptions module is modified to take polyadic 

191 # predicate, this will be removed 

192 if any(rel in conj_assumps for rel in (self, self.reversed)): 

193 return True 

194 neg_rels = (self.negated, self.reversed.negated, Not(self, evaluate=False), 

195 Not(self.reversed, evaluate=False)) 

196 if any(rel in conj_assumps for rel in neg_rels): 

197 return False 

198 

199 # evaluation using multipledispatching 

200 ret = self.function.eval(self.arguments, assumptions) 

201 if ret is not None: 

202 return ret 

203 

204 # simplify the args and try again 

205 args = tuple(a.simplify() for a in self.arguments) 

206 return self.function.eval(args, assumptions) 

207 

208 def __bool__(self): 

209 ret = ask(self) 

210 if ret is None: 

211 raise TypeError("Cannot determine truth value of %s" % self) 

212 return ret