Coverage for /usr/lib/python3/dist-packages/sympy/concrete/expr_with_limits.py: 17%

292 statements  

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

1from sympy.core.add import Add 

2from sympy.core.containers import Tuple 

3from sympy.core.expr import Expr 

4from sympy.core.function import AppliedUndef, UndefinedFunction 

5from sympy.core.mul import Mul 

6from sympy.core.relational import Equality, Relational 

7from sympy.core.singleton import S 

8from sympy.core.symbol import Symbol, Dummy 

9from sympy.core.sympify import sympify 

10from sympy.functions.elementary.piecewise import (piecewise_fold, 

11 Piecewise) 

12from sympy.logic.boolalg import BooleanFunction 

13from sympy.matrices.matrices import MatrixBase 

14from sympy.sets.sets import Interval, Set 

15from sympy.sets.fancysets import Range 

16from sympy.tensor.indexed import Idx 

17from sympy.utilities import flatten 

18from sympy.utilities.iterables import sift, is_sequence 

19from sympy.utilities.exceptions import sympy_deprecation_warning 

20 

21 

22def _common_new(cls, function, *symbols, discrete, **assumptions): 

23 """Return either a special return value or the tuple, 

24 (function, limits, orientation). This code is common to 

25 both ExprWithLimits and AddWithLimits.""" 

26 function = sympify(function) 

27 

28 if isinstance(function, Equality): 

29 # This transforms e.g. Integral(Eq(x, y)) to Eq(Integral(x), Integral(y)) 

30 # but that is only valid for definite integrals. 

31 limits, orientation = _process_limits(*symbols, discrete=discrete) 

32 if not (limits and all(len(limit) == 3 for limit in limits)): 

33 sympy_deprecation_warning( 

34 """ 

35 Creating a indefinite integral with an Eq() argument is 

36 deprecated. 

37 

38 This is because indefinite integrals do not preserve equality 

39 due to the arbitrary constants. If you want an equality of 

40 indefinite integrals, use Eq(Integral(a, x), Integral(b, x)) 

41 explicitly. 

42 """, 

43 deprecated_since_version="1.6", 

44 active_deprecations_target="deprecated-indefinite-integral-eq", 

45 stacklevel=5, 

46 ) 

47 

48 lhs = function.lhs 

49 rhs = function.rhs 

50 return Equality(cls(lhs, *symbols, **assumptions), \ 

51 cls(rhs, *symbols, **assumptions)) 

52 

53 if function is S.NaN: 

54 return S.NaN 

55 

56 if symbols: 

57 limits, orientation = _process_limits(*symbols, discrete=discrete) 

58 for i, li in enumerate(limits): 

59 if len(li) == 4: 

60 function = function.subs(li[0], li[-1]) 

61 limits[i] = Tuple(*li[:-1]) 

62 else: 

63 # symbol not provided -- we can still try to compute a general form 

64 free = function.free_symbols 

65 if len(free) != 1: 

66 raise ValueError( 

67 "specify dummy variables for %s" % function) 

68 limits, orientation = [Tuple(s) for s in free], 1 

69 

70 # denest any nested calls 

71 while cls == type(function): 

72 limits = list(function.limits) + limits 

73 function = function.function 

74 

75 # Any embedded piecewise functions need to be brought out to the 

76 # top level. We only fold Piecewise that contain the integration 

77 # variable. 

78 reps = {} 

79 symbols_of_integration = {i[0] for i in limits} 

80 for p in function.atoms(Piecewise): 

81 if not p.has(*symbols_of_integration): 

82 reps[p] = Dummy() 

83 # mask off those that don't 

84 function = function.xreplace(reps) 

85 # do the fold 

86 function = piecewise_fold(function) 

87 # remove the masking 

88 function = function.xreplace({v: k for k, v in reps.items()}) 

89 

90 return function, limits, orientation 

91 

92 

93def _process_limits(*symbols, discrete=None): 

94 """Process the list of symbols and convert them to canonical limits, 

95 storing them as Tuple(symbol, lower, upper). The orientation of 

96 the function is also returned when the upper limit is missing 

97 so (x, 1, None) becomes (x, None, 1) and the orientation is changed. 

98 In the case that a limit is specified as (symbol, Range), a list of 

99 length 4 may be returned if a change of variables is needed; the 

100 expression that should replace the symbol in the expression is 

101 the fourth element in the list. 

102 """ 

103 limits = [] 

104 orientation = 1 

105 if discrete is None: 

106 err_msg = 'discrete must be True or False' 

107 elif discrete: 

108 err_msg = 'use Range, not Interval or Relational' 

109 else: 

110 err_msg = 'use Interval or Relational, not Range' 

111 for V in symbols: 

112 if isinstance(V, (Relational, BooleanFunction)): 

113 if discrete: 

114 raise TypeError(err_msg) 

115 variable = V.atoms(Symbol).pop() 

116 V = (variable, V.as_set()) 

117 elif isinstance(V, Symbol) or getattr(V, '_diff_wrt', False): 

118 if isinstance(V, Idx): 

119 if V.lower is None or V.upper is None: 

120 limits.append(Tuple(V)) 

121 else: 

122 limits.append(Tuple(V, V.lower, V.upper)) 

123 else: 

124 limits.append(Tuple(V)) 

125 continue 

126 if is_sequence(V) and not isinstance(V, Set): 

127 if len(V) == 2 and isinstance(V[1], Set): 

128 V = list(V) 

129 if isinstance(V[1], Interval): # includes Reals 

130 if discrete: 

131 raise TypeError(err_msg) 

132 V[1:] = V[1].inf, V[1].sup 

133 elif isinstance(V[1], Range): 

134 if not discrete: 

135 raise TypeError(err_msg) 

136 lo = V[1].inf 

137 hi = V[1].sup 

138 dx = abs(V[1].step) # direction doesn't matter 

139 if dx == 1: 

140 V[1:] = [lo, hi] 

141 else: 

142 if lo is not S.NegativeInfinity: 

143 V = [V[0]] + [0, (hi - lo)//dx, dx*V[0] + lo] 

144 else: 

145 V = [V[0]] + [0, S.Infinity, -dx*V[0] + hi] 

146 else: 

147 # more complicated sets would require splitting, e.g. 

148 # Union(Interval(1, 3), interval(6,10)) 

149 raise NotImplementedError( 

150 'expecting Range' if discrete else 

151 'Relational or single Interval' ) 

152 V = sympify(flatten(V)) # list of sympified elements/None 

153 if isinstance(V[0], (Symbol, Idx)) or getattr(V[0], '_diff_wrt', False): 

154 newsymbol = V[0] 

155 if len(V) == 3: 

156 # general case 

157 if V[2] is None and V[1] is not None: 

158 orientation *= -1 

159 V = [newsymbol] + [i for i in V[1:] if i is not None] 

160 

161 lenV = len(V) 

162 if not isinstance(newsymbol, Idx) or lenV == 3: 

163 if lenV == 4: 

164 limits.append(Tuple(*V)) 

165 continue 

166 if lenV == 3: 

167 if isinstance(newsymbol, Idx): 

168 # Idx represents an integer which may have 

169 # specified values it can take on; if it is 

170 # given such a value, an error is raised here 

171 # if the summation would try to give it a larger 

172 # or smaller value than permitted. None and Symbolic 

173 # values will not raise an error. 

174 lo, hi = newsymbol.lower, newsymbol.upper 

175 try: 

176 if lo is not None and not bool(V[1] >= lo): 

177 raise ValueError("Summation will set Idx value too low.") 

178 except TypeError: 

179 pass 

180 try: 

181 if hi is not None and not bool(V[2] <= hi): 

182 raise ValueError("Summation will set Idx value too high.") 

183 except TypeError: 

184 pass 

185 limits.append(Tuple(*V)) 

186 continue 

187 if lenV == 1 or (lenV == 2 and V[1] is None): 

188 limits.append(Tuple(newsymbol)) 

189 continue 

190 elif lenV == 2: 

191 limits.append(Tuple(newsymbol, V[1])) 

192 continue 

193 

194 raise ValueError('Invalid limits given: %s' % str(symbols)) 

195 

196 return limits, orientation 

197 

198 

199class ExprWithLimits(Expr): 

200 __slots__ = ('is_commutative',) 

201 

202 def __new__(cls, function, *symbols, **assumptions): 

203 from sympy.concrete.products import Product 

204 pre = _common_new(cls, function, *symbols, 

205 discrete=issubclass(cls, Product), **assumptions) 

206 if isinstance(pre, tuple): 

207 function, limits, _ = pre 

208 else: 

209 return pre 

210 

211 # limits must have upper and lower bounds; the indefinite form 

212 # is not supported. This restriction does not apply to AddWithLimits 

213 if any(len(l) != 3 or None in l for l in limits): 

214 raise ValueError('ExprWithLimits requires values for lower and upper bounds.') 

215 

216 obj = Expr.__new__(cls, **assumptions) 

217 arglist = [function] 

218 arglist.extend(limits) 

219 obj._args = tuple(arglist) 

220 obj.is_commutative = function.is_commutative # limits already checked 

221 

222 return obj 

223 

224 @property 

225 def function(self): 

226 """Return the function applied across limits. 

227 

228 Examples 

229 ======== 

230 

231 >>> from sympy import Integral 

232 >>> from sympy.abc import x 

233 >>> Integral(x**2, (x,)).function 

234 x**2 

235 

236 See Also 

237 ======== 

238 

239 limits, variables, free_symbols 

240 """ 

241 return self._args[0] 

242 

243 @property 

244 def kind(self): 

245 return self.function.kind 

246 

247 @property 

248 def limits(self): 

249 """Return the limits of expression. 

250 

251 Examples 

252 ======== 

253 

254 >>> from sympy import Integral 

255 >>> from sympy.abc import x, i 

256 >>> Integral(x**i, (i, 1, 3)).limits 

257 ((i, 1, 3),) 

258 

259 See Also 

260 ======== 

261 

262 function, variables, free_symbols 

263 """ 

264 return self._args[1:] 

265 

266 @property 

267 def variables(self): 

268 """Return a list of the limit variables. 

269 

270 >>> from sympy import Sum 

271 >>> from sympy.abc import x, i 

272 >>> Sum(x**i, (i, 1, 3)).variables 

273 [i] 

274 

275 See Also 

276 ======== 

277 

278 function, limits, free_symbols 

279 as_dummy : Rename dummy variables 

280 sympy.integrals.integrals.Integral.transform : Perform mapping on the dummy variable 

281 """ 

282 return [l[0] for l in self.limits] 

283 

284 @property 

285 def bound_symbols(self): 

286 """Return only variables that are dummy variables. 

287 

288 Examples 

289 ======== 

290 

291 >>> from sympy import Integral 

292 >>> from sympy.abc import x, i, j, k 

293 >>> Integral(x**i, (i, 1, 3), (j, 2), k).bound_symbols 

294 [i, j] 

295 

296 See Also 

297 ======== 

298 

299 function, limits, free_symbols 

300 as_dummy : Rename dummy variables 

301 sympy.integrals.integrals.Integral.transform : Perform mapping on the dummy variable 

302 """ 

303 return [l[0] for l in self.limits if len(l) != 1] 

304 

305 @property 

306 def free_symbols(self): 

307 """ 

308 This method returns the symbols in the object, excluding those 

309 that take on a specific value (i.e. the dummy symbols). 

310 

311 Examples 

312 ======== 

313 

314 >>> from sympy import Sum 

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

316 >>> Sum(x, (x, y, 1)).free_symbols 

317 {y} 

318 """ 

319 # don't test for any special values -- nominal free symbols 

320 # should be returned, e.g. don't return set() if the 

321 # function is zero -- treat it like an unevaluated expression. 

322 function, limits = self.function, self.limits 

323 # mask off non-symbol integration variables that have 

324 # more than themself as a free symbol 

325 reps = {i[0]: i[0] if i[0].free_symbols == {i[0]} else Dummy() 

326 for i in self.limits} 

327 function = function.xreplace(reps) 

328 isyms = function.free_symbols 

329 for xab in limits: 

330 v = reps[xab[0]] 

331 if len(xab) == 1: 

332 isyms.add(v) 

333 continue 

334 # take out the target symbol 

335 if v in isyms: 

336 isyms.remove(v) 

337 # add in the new symbols 

338 for i in xab[1:]: 

339 isyms.update(i.free_symbols) 

340 reps = {v: k for k, v in reps.items()} 

341 return {reps.get(_, _) for _ in isyms} 

342 

343 @property 

344 def is_number(self): 

345 """Return True if the Sum has no free symbols, else False.""" 

346 return not self.free_symbols 

347 

348 def _eval_interval(self, x, a, b): 

349 limits = [(i if i[0] != x else (x, a, b)) for i in self.limits] 

350 integrand = self.function 

351 return self.func(integrand, *limits) 

352 

353 def _eval_subs(self, old, new): 

354 """ 

355 Perform substitutions over non-dummy variables 

356 of an expression with limits. Also, can be used 

357 to specify point-evaluation of an abstract antiderivative. 

358 

359 Examples 

360 ======== 

361 

362 >>> from sympy import Sum, oo 

363 >>> from sympy.abc import s, n 

364 >>> Sum(1/n**s, (n, 1, oo)).subs(s, 2) 

365 Sum(n**(-2), (n, 1, oo)) 

366 

367 >>> from sympy import Integral 

368 >>> from sympy.abc import x, a 

369 >>> Integral(a*x**2, x).subs(x, 4) 

370 Integral(a*x**2, (x, 4)) 

371 

372 See Also 

373 ======== 

374 

375 variables : Lists the integration variables 

376 transform : Perform mapping on the dummy variable for integrals 

377 change_index : Perform mapping on the sum and product dummy variables 

378 

379 """ 

380 func, limits = self.function, list(self.limits) 

381 

382 # If one of the expressions we are replacing is used as a func index 

383 # one of two things happens. 

384 # - the old variable first appears as a free variable 

385 # so we perform all free substitutions before it becomes 

386 # a func index. 

387 # - the old variable first appears as a func index, in 

388 # which case we ignore. See change_index. 

389 

390 # Reorder limits to match standard mathematical practice for scoping 

391 limits.reverse() 

392 

393 if not isinstance(old, Symbol) or \ 

394 old.free_symbols.intersection(self.free_symbols): 

395 sub_into_func = True 

396 for i, xab in enumerate(limits): 

397 if 1 == len(xab) and old == xab[0]: 

398 if new._diff_wrt: 

399 xab = (new,) 

400 else: 

401 xab = (old, old) 

402 limits[i] = Tuple(xab[0], *[l._subs(old, new) for l in xab[1:]]) 

403 if len(xab[0].free_symbols.intersection(old.free_symbols)) != 0: 

404 sub_into_func = False 

405 break 

406 if isinstance(old, (AppliedUndef, UndefinedFunction)): 

407 sy2 = set(self.variables).intersection(set(new.atoms(Symbol))) 

408 sy1 = set(self.variables).intersection(set(old.args)) 

409 if not sy2.issubset(sy1): 

410 raise ValueError( 

411 "substitution cannot create dummy dependencies") 

412 sub_into_func = True 

413 if sub_into_func: 

414 func = func.subs(old, new) 

415 else: 

416 # old is a Symbol and a dummy variable of some limit 

417 for i, xab in enumerate(limits): 

418 if len(xab) == 3: 

419 limits[i] = Tuple(xab[0], *[l._subs(old, new) for l in xab[1:]]) 

420 if old == xab[0]: 

421 break 

422 # simplify redundant limits (x, x) to (x, ) 

423 for i, xab in enumerate(limits): 

424 if len(xab) == 2 and (xab[0] - xab[1]).is_zero: 

425 limits[i] = Tuple(xab[0], ) 

426 

427 # Reorder limits back to representation-form 

428 limits.reverse() 

429 

430 return self.func(func, *limits) 

431 

432 @property 

433 def has_finite_limits(self): 

434 """ 

435 Returns True if the limits are known to be finite, either by the 

436 explicit bounds, assumptions on the bounds, or assumptions on the 

437 variables. False if known to be infinite, based on the bounds. 

438 None if not enough information is available to determine. 

439 

440 Examples 

441 ======== 

442 

443 >>> from sympy import Sum, Integral, Product, oo, Symbol 

444 >>> x = Symbol('x') 

445 >>> Sum(x, (x, 1, 8)).has_finite_limits 

446 True 

447 

448 >>> Integral(x, (x, 1, oo)).has_finite_limits 

449 False 

450 

451 >>> M = Symbol('M') 

452 >>> Sum(x, (x, 1, M)).has_finite_limits 

453 

454 >>> N = Symbol('N', integer=True) 

455 >>> Product(x, (x, 1, N)).has_finite_limits 

456 True 

457 

458 See Also 

459 ======== 

460 

461 has_reversed_limits 

462 

463 """ 

464 

465 ret_None = False 

466 for lim in self.limits: 

467 if len(lim) == 3: 

468 if any(l.is_infinite for l in lim[1:]): 

469 # Any of the bounds are +/-oo 

470 return False 

471 elif any(l.is_infinite is None for l in lim[1:]): 

472 # Maybe there are assumptions on the variable? 

473 if lim[0].is_infinite is None: 

474 ret_None = True 

475 else: 

476 if lim[0].is_infinite is None: 

477 ret_None = True 

478 

479 if ret_None: 

480 return None 

481 return True 

482 

483 @property 

484 def has_reversed_limits(self): 

485 """ 

486 Returns True if the limits are known to be in reversed order, either 

487 by the explicit bounds, assumptions on the bounds, or assumptions on the 

488 variables. False if known to be in normal order, based on the bounds. 

489 None if not enough information is available to determine. 

490 

491 Examples 

492 ======== 

493 

494 >>> from sympy import Sum, Integral, Product, oo, Symbol 

495 >>> x = Symbol('x') 

496 >>> Sum(x, (x, 8, 1)).has_reversed_limits 

497 True 

498 

499 >>> Sum(x, (x, 1, oo)).has_reversed_limits 

500 False 

501 

502 >>> M = Symbol('M') 

503 >>> Integral(x, (x, 1, M)).has_reversed_limits 

504 

505 >>> N = Symbol('N', integer=True, positive=True) 

506 >>> Sum(x, (x, 1, N)).has_reversed_limits 

507 False 

508 

509 >>> Product(x, (x, 2, N)).has_reversed_limits 

510 

511 >>> Product(x, (x, 2, N)).subs(N, N + 2).has_reversed_limits 

512 False 

513 

514 See Also 

515 ======== 

516 

517 sympy.concrete.expr_with_intlimits.ExprWithIntLimits.has_empty_sequence 

518 

519 """ 

520 ret_None = False 

521 for lim in self.limits: 

522 if len(lim) == 3: 

523 var, a, b = lim 

524 dif = b - a 

525 if dif.is_extended_negative: 

526 return True 

527 elif dif.is_extended_nonnegative: 

528 continue 

529 else: 

530 ret_None = True 

531 else: 

532 return None 

533 if ret_None: 

534 return None 

535 return False 

536 

537 

538class AddWithLimits(ExprWithLimits): 

539 r"""Represents unevaluated oriented additions. 

540 Parent class for Integral and Sum. 

541 """ 

542 

543 __slots__ = () 

544 

545 def __new__(cls, function, *symbols, **assumptions): 

546 from sympy.concrete.summations import Sum 

547 pre = _common_new(cls, function, *symbols, 

548 discrete=issubclass(cls, Sum), **assumptions) 

549 if isinstance(pre, tuple): 

550 function, limits, orientation = pre 

551 else: 

552 return pre 

553 

554 obj = Expr.__new__(cls, **assumptions) 

555 arglist = [orientation*function] # orientation not used in ExprWithLimits 

556 arglist.extend(limits) 

557 obj._args = tuple(arglist) 

558 obj.is_commutative = function.is_commutative # limits already checked 

559 

560 return obj 

561 

562 def _eval_adjoint(self): 

563 if all(x.is_real for x in flatten(self.limits)): 

564 return self.func(self.function.adjoint(), *self.limits) 

565 return None 

566 

567 def _eval_conjugate(self): 

568 if all(x.is_real for x in flatten(self.limits)): 

569 return self.func(self.function.conjugate(), *self.limits) 

570 return None 

571 

572 def _eval_transpose(self): 

573 if all(x.is_real for x in flatten(self.limits)): 

574 return self.func(self.function.transpose(), *self.limits) 

575 return None 

576 

577 def _eval_factor(self, **hints): 

578 if 1 == len(self.limits): 

579 summand = self.function.factor(**hints) 

580 if summand.is_Mul: 

581 out = sift(summand.args, lambda w: w.is_commutative \ 

582 and not set(self.variables) & w.free_symbols) 

583 return Mul(*out[True])*self.func(Mul(*out[False]), \ 

584 *self.limits) 

585 else: 

586 summand = self.func(self.function, *self.limits[0:-1]).factor() 

587 if not summand.has(self.variables[-1]): 

588 return self.func(1, [self.limits[-1]]).doit()*summand 

589 elif isinstance(summand, Mul): 

590 return self.func(summand, self.limits[-1]).factor() 

591 return self 

592 

593 def _eval_expand_basic(self, **hints): 

594 summand = self.function.expand(**hints) 

595 force = hints.get('force', False) 

596 if (summand.is_Add and (force or summand.is_commutative and 

597 self.has_finite_limits is not False)): 

598 return Add(*[self.func(i, *self.limits) for i in summand.args]) 

599 elif isinstance(summand, MatrixBase): 

600 return summand.applyfunc(lambda x: self.func(x, *self.limits)) 

601 elif summand != self.function: 

602 return self.func(summand, *self.limits) 

603 return self