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
« 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
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
12__all__ = ["BinaryRelation", "AppliedBinaryRelation"]
15class BinaryRelation(Predicate):
16 """
17 Base class for all binary relational predicates.
19 Explanation
20 ===========
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.
26 You can add support for new types by registering the handler to dispatcher.
27 See :obj:`~.Predicate()` for more information about predicate dispatching.
29 Examples
30 ========
32 Applying and evaluating to boolean value:
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
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$.
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)
57 Now, we can use ``ask()`` to evaluate it to boolean value.
59 >>> ask(Q.R(2, 1))
60 True
61 >>> ask(Q.R(1, 2))
62 False
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``.
68 >>> ask(Q.R(x, x))
69 False
71 References
72 ==========
74 .. [1] https://en.wikipedia.org/wiki/Reflexive_relation
75 """
77 is_reflexive: Optional[bool] = None
78 is_symmetric: Optional[bool] = None
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)
85 @property
86 def reversed(self):
87 if self.is_symmetric:
88 return self
89 return None
91 @property
92 def negated(self):
93 return None
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.
100 # reflexivity does not hold to NaN
101 if lhs is S.NaN or rhs is S.NaN:
102 return None
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
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
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
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)
132 return ret
135class AppliedBinaryRelation(AppliedPredicate):
136 """
137 The class of expressions resulting from applying ``BinaryRelation``
138 to the arguments.
140 """
142 @property
143 def lhs(self):
144 """The left-hand side of the relation."""
145 return self.arguments[0]
147 @property
148 def rhs(self):
149 """The right-hand side of the relation."""
150 return self.arguments[1]
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)
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
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)
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)
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
199 # evaluation using multipledispatching
200 ret = self.function.eval(self.arguments, assumptions)
201 if ret is not None:
202 return ret
204 # simplify the args and try again
205 args = tuple(a.simplify() for a in self.arguments)
206 return self.function.eval(args, assumptions)
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