Coverage for /usr/lib/python3/dist-packages/sympy/assumptions/assume.py: 34%

177 statements  

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

1"""A module which implements predicates and assumption context.""" 

2 

3from contextlib import contextmanager 

4import inspect 

5from sympy.core.symbol import Str 

6from sympy.core.sympify import _sympify 

7from sympy.logic.boolalg import Boolean, false, true 

8from sympy.multipledispatch.dispatcher import Dispatcher, str_signature 

9from sympy.utilities.exceptions import sympy_deprecation_warning 

10from sympy.utilities.iterables import is_sequence 

11from sympy.utilities.source import get_class 

12 

13 

14class AssumptionsContext(set): 

15 """ 

16 Set containing default assumptions which are applied to the ``ask()`` 

17 function. 

18 

19 Explanation 

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

21 

22 This is used to represent global assumptions, but you can also use this 

23 class to create your own local assumptions contexts. It is basically a thin 

24 wrapper to Python's set, so see its documentation for advanced usage. 

25 

26 Examples 

27 ======== 

28 

29 The default assumption context is ``global_assumptions``, which is initially empty: 

30 

31 >>> from sympy import ask, Q 

32 >>> from sympy.assumptions import global_assumptions 

33 >>> global_assumptions 

34 AssumptionsContext() 

35 

36 You can add default assumptions: 

37 

38 >>> from sympy.abc import x 

39 >>> global_assumptions.add(Q.real(x)) 

40 >>> global_assumptions 

41 AssumptionsContext({Q.real(x)}) 

42 >>> ask(Q.real(x)) 

43 True 

44 

45 And remove them: 

46 

47 >>> global_assumptions.remove(Q.real(x)) 

48 >>> print(ask(Q.real(x))) 

49 None 

50 

51 The ``clear()`` method removes every assumption: 

52 

53 >>> global_assumptions.add(Q.positive(x)) 

54 >>> global_assumptions 

55 AssumptionsContext({Q.positive(x)}) 

56 >>> global_assumptions.clear() 

57 >>> global_assumptions 

58 AssumptionsContext() 

59 

60 See Also 

61 ======== 

62 

63 assuming 

64 

65 """ 

66 

67 def add(self, *assumptions): 

68 """Add assumptions.""" 

69 for a in assumptions: 

70 super().add(a) 

71 

72 def _sympystr(self, printer): 

73 if not self: 

74 return "%s()" % self.__class__.__name__ 

75 return "{}({})".format(self.__class__.__name__, printer._print_set(self)) 

76 

77global_assumptions = AssumptionsContext() 

78 

79 

80class AppliedPredicate(Boolean): 

81 """ 

82 The class of expressions resulting from applying ``Predicate`` to 

83 the arguments. ``AppliedPredicate`` merely wraps its argument and 

84 remain unevaluated. To evaluate it, use the ``ask()`` function. 

85 

86 Examples 

87 ======== 

88 

89 >>> from sympy import Q, ask 

90 >>> Q.integer(1) 

91 Q.integer(1) 

92 

93 The ``function`` attribute returns the predicate, and the ``arguments`` 

94 attribute returns the tuple of arguments. 

95 

96 >>> type(Q.integer(1)) 

97 <class 'sympy.assumptions.assume.AppliedPredicate'> 

98 >>> Q.integer(1).function 

99 Q.integer 

100 >>> Q.integer(1).arguments 

101 (1,) 

102 

103 Applied predicates can be evaluated to a boolean value with ``ask``: 

104 

105 >>> ask(Q.integer(1)) 

106 True 

107 

108 """ 

109 __slots__ = () 

110 

111 def __new__(cls, predicate, *args): 

112 if not isinstance(predicate, Predicate): 

113 raise TypeError("%s is not a Predicate." % predicate) 

114 args = map(_sympify, args) 

115 return super().__new__(cls, predicate, *args) 

116 

117 @property 

118 def arg(self): 

119 """ 

120 Return the expression used by this assumption. 

121 

122 Examples 

123 ======== 

124 

125 >>> from sympy import Q, Symbol 

126 >>> x = Symbol('x') 

127 >>> a = Q.integer(x + 1) 

128 >>> a.arg 

129 x + 1 

130 

131 """ 

132 # Will be deprecated 

133 args = self._args 

134 if len(args) == 2: 

135 # backwards compatibility 

136 return args[1] 

137 raise TypeError("'arg' property is allowed only for unary predicates.") 

138 

139 @property 

140 def function(self): 

141 """ 

142 Return the predicate. 

143 """ 

144 # Will be changed to self.args[0] after args overriding is removed 

145 return self._args[0] 

146 

147 @property 

148 def arguments(self): 

149 """ 

150 Return the arguments which are applied to the predicate. 

151 """ 

152 # Will be changed to self.args[1:] after args overriding is removed 

153 return self._args[1:] 

154 

155 def _eval_ask(self, assumptions): 

156 return self.function.eval(self.arguments, assumptions) 

157 

158 @property 

159 def binary_symbols(self): 

160 from .ask import Q 

161 if self.function == Q.is_true: 

162 i = self.arguments[0] 

163 if i.is_Boolean or i.is_Symbol: 

164 return i.binary_symbols 

165 if self.function in (Q.eq, Q.ne): 

166 if true in self.arguments or false in self.arguments: 

167 if self.arguments[0].is_Symbol: 

168 return {self.arguments[0]} 

169 elif self.arguments[1].is_Symbol: 

170 return {self.arguments[1]} 

171 return set() 

172 

173 

174class PredicateMeta(type): 

175 def __new__(cls, clsname, bases, dct): 

176 # If handler is not defined, assign empty dispatcher. 

177 if "handler" not in dct: 

178 name = f"Ask{clsname.capitalize()}Handler" 

179 handler = Dispatcher(name, doc="Handler for key %s" % name) 

180 dct["handler"] = handler 

181 

182 dct["_orig_doc"] = dct.get("__doc__", "") 

183 

184 return super().__new__(cls, clsname, bases, dct) 

185 

186 @property 

187 def __doc__(cls): 

188 handler = cls.handler 

189 doc = cls._orig_doc 

190 if cls is not Predicate and handler is not None: 

191 doc += "Handler\n" 

192 doc += " =======\n\n" 

193 

194 # Append the handler's doc without breaking sphinx documentation. 

195 docs = [" Multiply dispatched method: %s" % handler.name] 

196 if handler.doc: 

197 for line in handler.doc.splitlines(): 

198 if not line: 

199 continue 

200 docs.append(" %s" % line) 

201 other = [] 

202 for sig in handler.ordering[::-1]: 

203 func = handler.funcs[sig] 

204 if func.__doc__: 

205 s = ' Inputs: <%s>' % str_signature(sig) 

206 lines = [] 

207 for line in func.__doc__.splitlines(): 

208 lines.append(" %s" % line) 

209 s += "\n".join(lines) 

210 docs.append(s) 

211 else: 

212 other.append(str_signature(sig)) 

213 if other: 

214 othersig = " Other signatures:" 

215 for line in other: 

216 othersig += "\n * %s" % line 

217 docs.append(othersig) 

218 

219 doc += '\n\n'.join(docs) 

220 

221 return doc 

222 

223 

224class Predicate(Boolean, metaclass=PredicateMeta): 

225 """ 

226 Base class for mathematical predicates. It also serves as a 

227 constructor for undefined predicate objects. 

228 

229 Explanation 

230 =========== 

231 

232 Predicate is a function that returns a boolean value [1]. 

233 

234 Predicate function is object, and it is instance of predicate class. 

235 When a predicate is applied to arguments, ``AppliedPredicate`` 

236 instance is returned. This merely wraps the argument and remain 

237 unevaluated. To obtain the truth value of applied predicate, use the 

238 function ``ask``. 

239 

240 Evaluation of predicate is done by multiple dispatching. You can 

241 register new handler to the predicate to support new types. 

242 

243 Every predicate in SymPy can be accessed via the property of ``Q``. 

244 For example, ``Q.even`` returns the predicate which checks if the 

245 argument is even number. 

246 

247 To define a predicate which can be evaluated, you must subclass this 

248 class, make an instance of it, and register it to ``Q``. After then, 

249 dispatch the handler by argument types. 

250 

251 If you directly construct predicate using this class, you will get 

252 ``UndefinedPredicate`` which cannot be dispatched. This is useful 

253 when you are building boolean expressions which do not need to be 

254 evaluated. 

255 

256 Examples 

257 ======== 

258 

259 Applying and evaluating to boolean value: 

260 

261 >>> from sympy import Q, ask 

262 >>> ask(Q.prime(7)) 

263 True 

264 

265 You can define a new predicate by subclassing and dispatching. Here, 

266 we define a predicate for sexy primes [2] as an example. 

267 

268 >>> from sympy import Predicate, Integer 

269 >>> class SexyPrimePredicate(Predicate): 

270 ... name = "sexyprime" 

271 >>> Q.sexyprime = SexyPrimePredicate() 

272 >>> @Q.sexyprime.register(Integer, Integer) 

273 ... def _(int1, int2, assumptions): 

274 ... args = sorted([int1, int2]) 

275 ... if not all(ask(Q.prime(a), assumptions) for a in args): 

276 ... return False 

277 ... return args[1] - args[0] == 6 

278 >>> ask(Q.sexyprime(5, 11)) 

279 True 

280 

281 Direct constructing returns ``UndefinedPredicate``, which can be 

282 applied but cannot be dispatched. 

283 

284 >>> from sympy import Predicate, Integer 

285 >>> Q.P = Predicate("P") 

286 >>> type(Q.P) 

287 <class 'sympy.assumptions.assume.UndefinedPredicate'> 

288 >>> Q.P(1) 

289 Q.P(1) 

290 >>> Q.P.register(Integer)(lambda expr, assump: True) 

291 Traceback (most recent call last): 

292 ... 

293 TypeError: <class 'sympy.assumptions.assume.UndefinedPredicate'> cannot be dispatched. 

294 

295 References 

296 ========== 

297 

298 .. [1] https://en.wikipedia.org/wiki/Predicate_%28mathematical_logic%29 

299 .. [2] https://en.wikipedia.org/wiki/Sexy_prime 

300 

301 """ 

302 

303 is_Atom = True 

304 

305 def __new__(cls, *args, **kwargs): 

306 if cls is Predicate: 

307 return UndefinedPredicate(*args, **kwargs) 

308 obj = super().__new__(cls, *args) 

309 return obj 

310 

311 @property 

312 def name(self): 

313 # May be overridden 

314 return type(self).__name__ 

315 

316 @classmethod 

317 def register(cls, *types, **kwargs): 

318 """ 

319 Register the signature to the handler. 

320 """ 

321 if cls.handler is None: 

322 raise TypeError("%s cannot be dispatched." % type(cls)) 

323 return cls.handler.register(*types, **kwargs) 

324 

325 @classmethod 

326 def register_many(cls, *types, **kwargs): 

327 """ 

328 Register multiple signatures to same handler. 

329 """ 

330 def _(func): 

331 for t in types: 

332 if not is_sequence(t): 

333 t = (t,) # for convenience, allow passing `type` to mean `(type,)` 

334 cls.register(*t, **kwargs)(func) 

335 return _ 

336 

337 def __call__(self, *args): 

338 return AppliedPredicate(self, *args) 

339 

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

341 """ 

342 Evaluate ``self(*args)`` under the given assumptions. 

343 

344 This uses only direct resolution methods, not logical inference. 

345 """ 

346 result = None 

347 try: 

348 result = self.handler(*args, assumptions=assumptions) 

349 except NotImplementedError: 

350 pass 

351 return result 

352 

353 def _eval_refine(self, assumptions): 

354 # When Predicate is no longer Boolean, delete this method 

355 return self 

356 

357 

358class UndefinedPredicate(Predicate): 

359 """ 

360 Predicate without handler. 

361 

362 Explanation 

363 =========== 

364 

365 This predicate is generated by using ``Predicate`` directly for 

366 construction. It does not have a handler, and evaluating this with 

367 arguments is done by SAT solver. 

368 

369 Examples 

370 ======== 

371 

372 >>> from sympy import Predicate, Q 

373 >>> Q.P = Predicate('P') 

374 >>> Q.P.func 

375 <class 'sympy.assumptions.assume.UndefinedPredicate'> 

376 >>> Q.P.name 

377 Str('P') 

378 

379 """ 

380 

381 handler = None 

382 

383 def __new__(cls, name, handlers=None): 

384 # "handlers" parameter supports old design 

385 if not isinstance(name, Str): 

386 name = Str(name) 

387 obj = super(Boolean, cls).__new__(cls, name) 

388 obj.handlers = handlers or [] 

389 return obj 

390 

391 @property 

392 def name(self): 

393 return self.args[0] 

394 

395 def _hashable_content(self): 

396 return (self.name,) 

397 

398 def __getnewargs__(self): 

399 return (self.name,) 

400 

401 def __call__(self, expr): 

402 return AppliedPredicate(self, expr) 

403 

404 def add_handler(self, handler): 

405 sympy_deprecation_warning( 

406 """ 

407 The AskHandler system is deprecated. Predicate.add_handler() 

408 should be replaced with the multipledispatch handler of Predicate. 

409 """, 

410 deprecated_since_version="1.8", 

411 active_deprecations_target='deprecated-askhandler', 

412 ) 

413 self.handlers.append(handler) 

414 

415 def remove_handler(self, handler): 

416 sympy_deprecation_warning( 

417 """ 

418 The AskHandler system is deprecated. Predicate.remove_handler() 

419 should be replaced with the multipledispatch handler of Predicate. 

420 """, 

421 deprecated_since_version="1.8", 

422 active_deprecations_target='deprecated-askhandler', 

423 ) 

424 self.handlers.remove(handler) 

425 

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

427 # Support for deprecated design 

428 # When old design is removed, this will always return None 

429 sympy_deprecation_warning( 

430 """ 

431 The AskHandler system is deprecated. Evaluating UndefinedPredicate 

432 objects should be replaced with the multipledispatch handler of 

433 Predicate. 

434 """, 

435 deprecated_since_version="1.8", 

436 active_deprecations_target='deprecated-askhandler', 

437 stacklevel=5, 

438 ) 

439 expr, = args 

440 res, _res = None, None 

441 mro = inspect.getmro(type(expr)) 

442 for handler in self.handlers: 

443 cls = get_class(handler) 

444 for subclass in mro: 

445 eval_ = getattr(cls, subclass.__name__, None) 

446 if eval_ is None: 

447 continue 

448 res = eval_(expr, assumptions) 

449 # Do not stop if value returned is None 

450 # Try to check for higher classes 

451 if res is None: 

452 continue 

453 if _res is None: 

454 _res = res 

455 else: 

456 # only check consistency if both resolutors have concluded 

457 if _res != res: 

458 raise ValueError('incompatible resolutors') 

459 break 

460 return res 

461 

462 

463@contextmanager 

464def assuming(*assumptions): 

465 """ 

466 Context manager for assumptions. 

467 

468 Examples 

469 ======== 

470 

471 >>> from sympy import assuming, Q, ask 

472 >>> from sympy.abc import x, y 

473 >>> print(ask(Q.integer(x + y))) 

474 None 

475 >>> with assuming(Q.integer(x), Q.integer(y)): 

476 ... print(ask(Q.integer(x + y))) 

477 True 

478 """ 

479 old_global_assumptions = global_assumptions.copy() 

480 global_assumptions.update(assumptions) 

481 try: 

482 yield 

483 finally: 

484 global_assumptions.clear() 

485 global_assumptions.update(old_global_assumptions)