Coverage for chebpy/core/chebfun.py: 99%

313 statements  

« prev     ^ index     » next       coverage.py v7.10.2, created at 2025-08-07 10:30 +0000

1"""Implementation of the Chebfun class for piecewise function approximation. 

2 

3This module provides the Chebfun class, which is the main user-facing class in the 

4ChebPy package. It represents functions using piecewise polynomial approximations 

5on arbitrary intervals, allowing for operations such as integration, differentiation, 

6root-finding, and more. 

7 

8The Chebfun class is inspired by the MATLAB package of the same name and provides 

9similar functionality for working with functions rather than numbers. 

10""" 

11 

12import operator 

13 

14import matplotlib.pyplot as plt 

15import numpy as np 

16 

17from .bndfun import Bndfun 

18from .decorators import cache, cast_arg_to_chebfun, float_argument, self_empty 

19from .exceptions import BadFunLengthArgument, SupportMismatch 

20from .plotting import plotfun 

21from .settings import _preferences as prefs 

22from .utilities import Domain, check_funs, compute_breakdata, generate_funs 

23 

24 

25class Chebfun: 

26 """Main class for representing and manipulating functions in ChebPy. 

27 

28 The Chebfun class represents functions using piecewise polynomial approximations 

29 on arbitrary intervals. It provides a comprehensive set of operations for working 

30 with these function representations, including: 

31 

32 - Function evaluation at arbitrary points 

33 - Algebraic operations (addition, multiplication, etc.) 

34 - Calculus operations (differentiation, integration, etc.) 

35 - Rootfinding 

36 - Plotting 

37 

38 Chebfun objects can be created from callable functions, constant values, or 

39 directly from function pieces. The class supports both adaptive and fixed-length 

40 approximations, allowing for efficient representation of functions with varying 

41 complexity across different intervals. 

42 

43 Attributes: 

44 funs (numpy.ndarray): Array of function pieces that make up the Chebfun. 

45 breakdata (OrderedDict): Mapping of breakpoints to function values. 

46 transposed (bool): Flag indicating if the Chebfun is transposed. 

47 """ 

48 

49 def __init__(self, funs): 

50 """Initialize a Chebfun object. 

51 

52 Args: 

53 funs (list): List of function objects to be included in the Chebfun. 

54 These will be checked and sorted using check_funs. 

55 """ 

56 self.funs = check_funs(funs) 

57 self.breakdata = compute_breakdata(self.funs) 

58 self.transposed = False 

59 

60 @classmethod 

61 def initempty(cls): 

62 """Initialize an empty Chebfun. 

63 

64 Returns: 

65 Chebfun: An empty Chebfun object with no functions. 

66 """ 

67 return cls([]) 

68 

69 @classmethod 

70 def initidentity(cls, domain=None): 

71 """Initialize a Chebfun representing the identity function f(x) = x. 

72 

73 Args: 

74 domain (array-like, optional): Domain on which to define the identity function. 

75 If None, uses the default domain from preferences. 

76 

77 Returns: 

78 Chebfun: A Chebfun object representing the identity function on the specified domain. 

79 """ 

80 return cls(generate_funs(domain, Bndfun.initidentity)) 

81 

82 @classmethod 

83 def initconst(cls, c, domain=None): 

84 """Initialize a Chebfun representing a constant function f(x) = c. 

85 

86 Args: 

87 c (float or complex): The constant value. 

88 domain (array-like, optional): Domain on which to define the constant function. 

89 If None, uses the default domain from preferences. 

90 

91 Returns: 

92 Chebfun: A Chebfun object representing the constant function on the specified domain. 

93 """ 

94 return cls(generate_funs(domain, Bndfun.initconst, {"c": c})) 

95 

96 @classmethod 

97 def initfun_adaptive(cls, f, domain=None): 

98 """Initialize a Chebfun by adaptively sampling a function. 

99 

100 This method determines the appropriate number of points needed to represent 

101 the function to the specified tolerance using an adaptive algorithm. 

102 

103 Args: 

104 f (callable): The function to be approximated. 

105 domain (array-like, optional): Domain on which to define the function. 

106 If None, uses the default domain from preferences. 

107 

108 Returns: 

109 Chebfun: A Chebfun object representing the function on the specified domain. 

110 """ 

111 return cls(generate_funs(domain, Bndfun.initfun_adaptive, {"f": f})) 

112 

113 @classmethod 

114 def initfun_fixedlen(cls, f, n, domain=None): 

115 """Initialize a Chebfun with a fixed number of points. 

116 

117 This method uses a specified number of points to represent the function, 

118 rather than determining the number adaptively. 

119 

120 Args: 

121 f (callable): The function to be approximated. 

122 n (int or array-like): Number of points to use. If a single value, uses the same 

123 number for each interval. If an array, must have one fewer elements than 

124 the size of the domain. 

125 domain (array-like, optional): Domain on which to define the function. 

126 If None, uses the default domain from preferences. 

127 

128 Returns: 

129 Chebfun: A Chebfun object representing the function on the specified domain. 

130 

131 Raises: 

132 BadFunLengthArgument: If n is an array and its size doesn't match domain.size - 1. 

133 """ 

134 nn = np.array(n) 

135 if nn.size < 2: 

136 funs = generate_funs(domain, Bndfun.initfun_fixedlen, {"f": f, "n": n}) 

137 else: 

138 domain = Domain(domain if domain is not None else prefs.domain) 

139 if not nn.size == domain.size - 1: 

140 raise BadFunLengthArgument 

141 funs = [] 

142 for interval, length in zip(domain.intervals, nn): 

143 funs.append(Bndfun.initfun_fixedlen(f, interval, length)) 

144 return cls(funs) 

145 

146 @classmethod 

147 def initfun(cls, f, domain=None, n=None): 

148 """Initialize a Chebfun from a function. 

149 

150 This is a general-purpose constructor that delegates to either initfun_adaptive 

151 or initfun_fixedlen based on whether n is provided. 

152 

153 Args: 

154 f (callable): The function to be approximated. 

155 domain (array-like, optional): Domain on which to define the function. 

156 If None, uses the default domain from preferences. 

157 n (int or array-like, optional): Number of points to use. If None, determines 

158 the number adaptively. If provided, uses a fixed number of points. 

159 

160 Returns: 

161 Chebfun: A Chebfun object representing the function on the specified domain. 

162 """ 

163 if n is None: 

164 return cls.initfun_adaptive(f, domain) 

165 else: 

166 return cls.initfun_fixedlen(f, n, domain) 

167 

168 # -------------------- 

169 # operator overloads 

170 # -------------------- 

171 def __add__(self, f): 

172 """Add a Chebfun with another Chebfun or a scalar. 

173 

174 Args: 

175 f (Chebfun or scalar): The object to add to this Chebfun. 

176 

177 Returns: 

178 Chebfun: A new Chebfun representing the sum. 

179 """ 

180 return self._apply_binop(f, operator.add) 

181 

182 @self_empty(np.array([])) 

183 @float_argument 

184 def __call__(self, x): 

185 """Evaluate the Chebfun at points x. 

186 

187 This method evaluates the Chebfun at the specified points. It handles interior 

188 points, breakpoints, and points outside the domain appropriately. 

189 

190 Args: 

191 x (float or array-like): Points at which to evaluate the Chebfun. 

192 

193 Returns: 

194 float or numpy.ndarray: The value(s) of the Chebfun at the specified point(s). 

195 Returns a scalar if x is a scalar, otherwise an array of the same size as x. 

196 """ 

197 # initialise output 

198 dtype = complex if self.iscomplex else float 

199 out = np.full(x.size, np.nan, dtype=dtype) 

200 

201 # evaluate a fun when x is an interior point 

202 for fun in self: 

203 idx = fun.interval.isinterior(x) 

204 out[idx] = fun(x[idx]) 

205 

206 # evaluate the breakpoint data for x at a breakpoint 

207 breakpoints = self.breakpoints 

208 for break_point in breakpoints: 

209 out[x == break_point] = self.breakdata[break_point] 

210 

211 # first and last funs used to evaluate outside of the chebfun domain 

212 lpts, rpts = x < breakpoints[0], x > breakpoints[-1] 

213 out[lpts] = self.funs[0](x[lpts]) 

214 out[rpts] = self.funs[-1](x[rpts]) 

215 return out 

216 

217 def __iter__(self): 

218 """Return an iterator over the functions in this Chebfun. 

219 

220 Returns: 

221 iterator: An iterator over the functions (funs) in this Chebfun. 

222 """ 

223 return self.funs.__iter__() 

224 

225 def __eq__(self, other): 

226 """Test for equality between two Chebfun objects. 

227 

228 Two Chebfun objects are considered equal if they have the same domain 

229 and their function values are equal (within tolerance) at a set of test points. 

230 

231 Args: 

232 other (object): The object to compare with this Chebfun. 

233 

234 Returns: 

235 bool: True if the objects are equal, False otherwise. 

236 """ 

237 if not isinstance(other, self.__class__): 

238 return False 

239 

240 # Check if both are empty 

241 if self.isempty and other.isempty: 

242 return True 

243 

244 # Check if domains are equal 

245 if self.domain != other.domain: 

246 return False 

247 

248 # Check function values at test points 

249 xx = np.linspace(self.support[0], self.support[1], 100) 

250 tol = 1e2 * max(self.vscale, other.vscale) * prefs.eps 

251 return np.all(np.abs(self(xx) - other(xx)) <= tol) 

252 

253 def __mul__(self, f): 

254 """Multiply a Chebfun with another Chebfun or a scalar. 

255 

256 Args: 

257 f (Chebfun or scalar): The object to multiply with this Chebfun. 

258 

259 Returns: 

260 Chebfun: A new Chebfun representing the product. 

261 """ 

262 return self._apply_binop(f, operator.mul) 

263 

264 def __neg__(self): 

265 """Return the negative of this Chebfun. 

266 

267 Returns: 

268 Chebfun: A new Chebfun representing -f(x). 

269 """ 

270 return self.__class__(-self.funs) 

271 

272 def __pos__(self): 

273 """Return the positive of this Chebfun (which is the Chebfun itself). 

274 

275 Returns: 

276 Chebfun: This Chebfun object (unchanged). 

277 """ 

278 return self 

279 

280 def __pow__(self, f): 

281 """Raise this Chebfun to a power. 

282 

283 Args: 

284 f (Chebfun or scalar): The exponent to which this Chebfun is raised. 

285 

286 Returns: 

287 Chebfun: A new Chebfun representing self^f. 

288 """ 

289 return self._apply_binop(f, operator.pow) 

290 

291 def __rtruediv__(self, c): 

292 """Divide a scalar by this Chebfun. 

293 

294 This method is called when a scalar is divided by a Chebfun, i.e., c / self. 

295 

296 Args: 

297 c (scalar): The scalar numerator. 

298 

299 Returns: 

300 Chebfun: A new Chebfun representing c / self. 

301 

302 Note: 

303 This is executed when truediv(f, self) fails, which is to say whenever c 

304 is not a Chebfun. We proceed on the assumption f is a scalar. 

305 """ 

306 

307 def constfun(cheb, const): 

308 return 0.0 * cheb + const 

309 

310 newfuns = [fun.initfun_adaptive(lambda x: constfun(x, c) / fun(x), fun.interval) for fun in self] 

311 return self.__class__(newfuns) 

312 

313 @self_empty("Chebfun<empty>") 

314 def __repr__(self): 

315 """Return a string representation of the Chebfun. 

316 

317 This method returns a detailed string representation of the Chebfun, 

318 including information about its domain, intervals, and endpoint values. 

319 

320 Returns: 

321 str: A string representation of the Chebfun. 

322 """ 

323 rowcol = "row" if self.transposed else "column" 

324 numpcs = self.funs.size 

325 plural = "" if numpcs == 1 else "s" 

326 header = f"Chebfun {rowcol} ({numpcs} smooth piece{plural})\n" 

327 domain_info = f"domain: {self.support}\n" 

328 toprow = " interval length endpoint values\n" 

329 tmplat = "[{:8.2g},{:8.2g}] {:6} {:8.2g} {:8.2g}\n" 

330 rowdta = "" 

331 for fun in self: 

332 endpts = fun.support 

333 xl, xr = endpts 

334 fl, fr = fun(endpts) 

335 row = tmplat.format(xl, xr, fun.size, fl, fr) 

336 rowdta += row 

337 btmrow = f"vertical scale = {self.vscale:3.2g}" 

338 btmxtr = "" if numpcs == 1 else f" total length = {sum([f.size for f in self])}" 

339 return header + domain_info + toprow + rowdta + btmrow + btmxtr 

340 

341 def __rsub__(self, f): 

342 """Subtract this Chebfun from another object. 

343 

344 This method is called when another object is subtracted by this Chebfun, 

345 i.e., f - self. 

346 

347 Args: 

348 f (Chebfun or scalar): The object from which to subtract this Chebfun. 

349 

350 Returns: 

351 Chebfun: A new Chebfun representing f - self. 

352 """ 

353 return -(self - f) 

354 

355 @cast_arg_to_chebfun 

356 def __rpow__(self, f): 

357 """Raise another object to the power of this Chebfun. 

358 

359 This method is called when another object is raised to the power of this Chebfun, 

360 i.e., f ** self. 

361 

362 Args: 

363 f (Chebfun or scalar): The base to be raised to the power of this Chebfun. 

364 

365 Returns: 

366 Chebfun: A new Chebfun representing f ** self. 

367 """ 

368 return f**self 

369 

370 def __truediv__(self, f): 

371 """Divide this Chebfun by another object. 

372 

373 Args: 

374 f (Chebfun or scalar): The divisor. 

375 

376 Returns: 

377 Chebfun: A new Chebfun representing self / f. 

378 """ 

379 return self._apply_binop(f, operator.truediv) 

380 

381 __rmul__ = __mul__ 

382 __div__ = __truediv__ 

383 __rdiv__ = __rtruediv__ 

384 __radd__ = __add__ 

385 

386 def __str__(self): 

387 """Return a concise string representation of the Chebfun. 

388 

389 This method returns a brief string representation of the Chebfun, 

390 showing its orientation, number of pieces, total size, and domain. 

391 

392 Returns: 

393 str: A concise string representation of the Chebfun. 

394 """ 

395 rowcol = "row" if self.transposed else "col" 

396 domain_str = f"domain {self.support}" if not self.isempty else "empty" 

397 out = f"<Chebfun-{rowcol},{self.funs.size},{sum([f.size for f in self])}, {domain_str}>\n" 

398 return out 

399 

400 def __sub__(self, f): 

401 """Subtract another object from this Chebfun. 

402 

403 Args: 

404 f (Chebfun or scalar): The object to subtract from this Chebfun. 

405 

406 Returns: 

407 Chebfun: A new Chebfun representing self - f. 

408 """ 

409 return self._apply_binop(f, operator.sub) 

410 

411 # ------------------ 

412 # internal helpers 

413 # ------------------ 

414 @self_empty() 

415 def _apply_binop(self, f, op): 

416 """Apply a binary operation between this Chebfun and another object. 

417 

418 This is a funnel method used in the implementation of Chebfun binary 

419 operators. The high-level idea is to first break each chebfun into a 

420 series of pieces corresponding to the union of the domains of each 

421 before applying the supplied binary operator and simplifying. In the 

422 case of the second argument being a scalar we don't need to do the 

423 simplify step, since at the Tech-level these operations are defined 

424 such that there is no change in the number of coefficients. 

425 

426 Args: 

427 f (Chebfun or scalar): The second operand of the binary operation. 

428 op (callable): The binary operation to apply (e.g., operator.add). 

429 

430 Returns: 

431 Chebfun: A new Chebfun resulting from applying the binary operation. 

432 """ 

433 if hasattr(f, "isempty") and f.isempty: 

434 return f 

435 if np.isscalar(f): 

436 chbfn1 = self 

437 chbfn2 = f * np.ones(self.funs.size) 

438 simplify = False 

439 else: 

440 newdom = self.domain.union(f.domain) 

441 chbfn1 = self._break(newdom) 

442 chbfn2 = f._break(newdom) 

443 simplify = True 

444 newfuns = [] 

445 for fun1, fun2 in zip(chbfn1, chbfn2): 

446 newfun = op(fun1, fun2) 

447 if simplify: 

448 newfun = newfun.simplify() 

449 newfuns.append(newfun) 

450 return self.__class__(newfuns) 

451 

452 def _break(self, targetdomain): 

453 """Resample this Chebfun to a new domain. 

454 

455 This method resamples the Chebfun to the supplied Domain object. It is 

456 intended as a private method since one will typically need to have 

457 called either Domain.union(f) or Domain.merge(f) prior to calling this method. 

458 

459 Args: 

460 targetdomain (Domain): The domain to which this Chebfun should be resampled. 

461 

462 Returns: 

463 Chebfun: A new Chebfun resampled to the target domain. 

464 """ 

465 newfuns = [] 

466 subintervals = targetdomain.intervals 

467 interval = next(subintervals) # next(..) for Python2/3 compatibility 

468 for fun in self: 

469 while interval in fun.interval: 

470 newfun = fun.restrict(interval) 

471 newfuns.append(newfun) 

472 try: 

473 interval = next(subintervals) 

474 except StopIteration: 

475 break 

476 return self.__class__(newfuns) 

477 

478 # ------------ 

479 # properties 

480 # ------------ 

481 @property 

482 def breakpoints(self): 

483 """Get the breakpoints of this Chebfun. 

484 

485 Breakpoints are the points where the Chebfun transitions from one piece to another. 

486 

487 Returns: 

488 numpy.ndarray: Array of breakpoints. 

489 """ 

490 return np.array([x for x in self.breakdata.keys()]) 

491 

492 @property 

493 @self_empty(np.array([])) 

494 def domain(self): 

495 """Get the domain of this Chebfun. 

496 

497 Returns: 

498 Domain: A Domain object corresponding to this Chebfun. 

499 """ 

500 return Domain.from_chebfun(self) 

501 

502 @domain.setter 

503 def domain(self, new_domain): 

504 """Set the domain of the Chebfun by restricting to the new domain. 

505 

506 Args: 

507 new_domain (array-like): The new domain to which this Chebfun should be restricted. 

508 """ 

509 self.restrict_(new_domain) 

510 

511 @property 

512 @self_empty(Domain([])) 

513 def support(self): 

514 """Get the support interval of this Chebfun. 

515 

516 The support is the interval between the first and last breakpoints. 

517 

518 Returns: 

519 numpy.ndarray: Array containing the first and last breakpoints. 

520 """ 

521 return self.domain.support 

522 

523 @property 

524 @self_empty(0.0) 

525 def hscale(self): 

526 """Get the horizontal scale of this Chebfun. 

527 

528 The horizontal scale is the maximum absolute value of the support interval. 

529 

530 Returns: 

531 float: The horizontal scale. 

532 """ 

533 return float(np.abs(self.support).max()) 

534 

535 @property 

536 @self_empty(False) 

537 def iscomplex(self): 

538 """Check if this Chebfun has complex values. 

539 

540 Returns: 

541 bool: True if any of the functions in this Chebfun have complex values, 

542 False otherwise. 

543 """ 

544 return any(fun.iscomplex for fun in self) 

545 

546 @property 

547 @self_empty(False) 

548 def isconst(self): 

549 """Check if this Chebfun represents a constant function. 

550 

551 A Chebfun is constant if all of its pieces are constant with the same value. 

552 

553 Returns: 

554 bool: True if this Chebfun represents a constant function, False otherwise. 

555 

556 Note: 

557 TODO: find an abstract way of referencing funs[0].coeffs[0] 

558 """ 

559 c = self.funs[0].coeffs[0] 

560 return all(fun.isconst and fun.coeffs[0] == c for fun in self) 

561 

562 @property 

563 def isempty(self): 

564 """Check if this Chebfun is empty. 

565 

566 An empty Chebfun contains no functions. 

567 

568 Returns: 

569 bool: True if this Chebfun is empty, False otherwise. 

570 """ 

571 return self.funs.size == 0 

572 

573 @property 

574 @self_empty(0.0) 

575 def vscale(self): 

576 """Get the vertical scale of this Chebfun. 

577 

578 The vertical scale is the maximum of the vertical scales of all pieces. 

579 

580 Returns: 

581 float: The vertical scale. 

582 """ 

583 return np.max([fun.vscale for fun in self]) 

584 

585 @property 

586 @self_empty() 

587 def x(self): 

588 """Get the identity function on the support of this Chebfun. 

589 

590 This property returns a new Chebfun representing the identity function f(x) = x 

591 defined on the same support as this Chebfun. 

592 

593 Returns: 

594 Chebfun: A Chebfun representing the identity function on the support of this Chebfun. 

595 """ 

596 return self.__class__.initidentity(self.support) 

597 

598 # ----------- 

599 # utilities 

600 # ---------- 

601 

602 def imag(self): 

603 """Get the imaginary part of this Chebfun. 

604 

605 Returns: 

606 Chebfun: A new Chebfun representing the imaginary part of this Chebfun. 

607 If this Chebfun is real-valued, returns a zero Chebfun. 

608 """ 

609 if self.iscomplex: 

610 return self.__class__([fun.imag() for fun in self]) 

611 else: 

612 return self.initconst(0, domain=self.domain) 

613 

614 def real(self): 

615 """Get the real part of this Chebfun. 

616 

617 Returns: 

618 Chebfun: A new Chebfun representing the real part of this Chebfun. 

619 If this Chebfun is already real-valued, returns this Chebfun. 

620 """ 

621 if self.iscomplex: 

622 return self.__class__([fun.real() for fun in self]) 

623 else: 

624 return self 

625 

626 def copy(self): 

627 """Create a deep copy of this Chebfun. 

628 

629 Returns: 

630 Chebfun: A new Chebfun that is a deep copy of this Chebfun. 

631 """ 

632 return self.__class__([fun.copy() for fun in self]) 

633 

634 @self_empty() 

635 def _restrict(self, subinterval): 

636 """Restrict a Chebfun to a subinterval, without simplifying. 

637 

638 This is an internal method that restricts the Chebfun to a subinterval 

639 without performing simplification. 

640 

641 Args: 

642 subinterval (array-like): The subinterval to which this Chebfun should be restricted. 

643 

644 Returns: 

645 Chebfun: A new Chebfun restricted to the specified subinterval, without simplification. 

646 """ 

647 newdom = self.domain.restrict(Domain(subinterval)) 

648 return self._break(newdom) 

649 

650 def restrict(self, subinterval): 

651 """Restrict a Chebfun to a subinterval. 

652 

653 This method creates a new Chebfun that is restricted to the specified subinterval 

654 and simplifies the result. 

655 

656 Args: 

657 subinterval (array-like): The subinterval to which this Chebfun should be restricted. 

658 

659 Returns: 

660 Chebfun: A new Chebfun restricted to the specified subinterval. 

661 """ 

662 return self._restrict(subinterval).simplify() 

663 

664 @self_empty() 

665 def restrict_(self, subinterval): 

666 """Restrict a Chebfun to a subinterval, modifying the object in place. 

667 

668 This method modifies the current Chebfun by restricting it to the specified 

669 subinterval and simplifying the result. 

670 

671 Args: 

672 subinterval (array-like): The subinterval to which this Chebfun should be restricted. 

673 

674 Returns: 

675 Chebfun: The modified Chebfun (self). 

676 """ 

677 restricted = self._restrict(subinterval).simplify() 

678 self.funs = restricted.funs 

679 self.breakdata = compute_breakdata(self.funs) 

680 return self 

681 

682 @cache 

683 @self_empty(np.array([])) 

684 def roots(self, merge=None): 

685 """Compute the roots of a Chebfun. 

686 

687 This method finds the values x for which f(x) = 0, by computing the roots 

688 of each piece of the Chebfun and combining them. 

689 

690 Args: 

691 merge (bool, optional): Whether to merge roots at breakpoints. If None, 

692 uses the value from preferences. Defaults to None. 

693 

694 Returns: 

695 numpy.ndarray: Array of roots sorted in ascending order. 

696 """ 

697 merge = merge if merge is not None else prefs.mergeroots 

698 allrts = [] 

699 prvrts = np.array([]) 

700 htol = 1e2 * self.hscale * prefs.eps 

701 for fun in self: 

702 rts = fun.roots() 

703 # ignore first root if equal to the last root of previous fun 

704 # TODO: there could be multiple roots at breakpoints 

705 if prvrts.size > 0 and rts.size > 0: 

706 if merge and abs(prvrts[-1] - rts[0]) <= htol: 

707 rts = rts[1:] 

708 allrts.append(rts) 

709 prvrts = rts 

710 return np.concatenate([x for x in allrts]) 

711 

712 @self_empty() 

713 def simplify(self): 

714 """Simplify each fun in the chebfun.""" 

715 return self.__class__([fun.simplify() for fun in self]) 

716 

717 def translate(self, c): 

718 """Translate a chebfun by c, i.e., return f(x-c).""" 

719 return self.__class__([x.translate(c) for x in self]) 

720 

721 # ---------- 

722 # calculus 

723 # ---------- 

724 def cumsum(self): 

725 """Compute the indefinite integral (antiderivative) of the Chebfun. 

726 

727 This method computes the indefinite integral of the Chebfun, with the 

728 constant of integration chosen so that the indefinite integral evaluates 

729 to 0 at the left endpoint of the domain. For piecewise functions, constants 

730 are added to ensure continuity across the pieces. 

731 

732 Returns: 

733 Chebfun: A new Chebfun representing the indefinite integral of this Chebfun. 

734 """ 

735 newfuns = [] 

736 prevfun = None 

737 for fun in self: 

738 integral = fun.cumsum() 

739 if prevfun: 

740 # enforce continuity by adding the function value 

741 # at the right endpoint of the previous fun 

742 _, fb = prevfun.endvalues 

743 integral = integral + fb 

744 newfuns.append(integral) 

745 prevfun = integral 

746 return self.__class__(newfuns) 

747 

748 def diff(self): 

749 """Compute the derivative of the Chebfun. 

750 

751 This method calculates the derivative of the Chebfun with respect to x. 

752 It creates a new Chebfun where each piece is the derivative of the 

753 corresponding piece in the original Chebfun. 

754 

755 Returns: 

756 Chebfun: A new Chebfun representing the derivative of this Chebfun. 

757 """ 

758 dfuns = np.array([fun.diff() for fun in self]) 

759 return self.__class__(dfuns) 

760 

761 def sum(self): 

762 """Compute the definite integral of the Chebfun over its domain. 

763 

764 This method calculates the definite integral of the Chebfun over its 

765 entire domain of definition by summing the definite integrals of each 

766 piece. 

767 

768 Returns: 

769 float or complex: The definite integral of the Chebfun over its domain. 

770 """ 

771 return np.sum([fun.sum() for fun in self]) 

772 

773 def dot(self, f): 

774 """Compute the dot product of this Chebfun with another function. 

775 

776 This method calculates the inner product (dot product) of this Chebfun 

777 with another function f by multiplying them pointwise and then integrating 

778 the result over the domain. 

779 

780 Args: 

781 f (Chebfun or scalar): The function or scalar to compute the dot product with. 

782 If not a Chebfun, it will be converted to one. 

783 

784 Returns: 

785 float or complex: The dot product of this Chebfun with f. 

786 """ 

787 return (self * f).sum() 

788 

789 # ---------- 

790 # utilities 

791 # ---------- 

792 @self_empty() 

793 def absolute(self): 

794 """Absolute value of a Chebfun.""" 

795 newdom = self.domain.merge(self.roots()) 

796 funs = [x.absolute() for x in self._break(newdom)] 

797 return self.__class__(funs) 

798 

799 abs = absolute 

800 

801 @self_empty() 

802 @cast_arg_to_chebfun 

803 def maximum(self, other): 

804 """Pointwise maximum of self and another chebfun.""" 

805 return self._maximum_minimum(other, operator.ge) 

806 

807 @self_empty() 

808 @cast_arg_to_chebfun 

809 def minimum(self, other): 

810 """Pointwise mimimum of self and another chebfun.""" 

811 return self._maximum_minimum(other, operator.lt) 

812 

813 def _maximum_minimum(self, other, comparator): 

814 """Method for computing the pointwise maximum/minimum of two Chebfuns. 

815 

816 This internal method implements the algorithm for computing the pointwise 

817 maximum or minimum of two Chebfun objects, based on the provided comparator. 

818 It is used by the maximum() and minimum() methods. 

819 

820 Args: 

821 other (Chebfun): Another Chebfun to compare with this one. 

822 comparator (callable): A function that compares two values and returns 

823 a boolean. For maximum, this is operator.ge (>=), and for minimum, 

824 this is operator.lt (<). 

825 

826 Returns: 

827 Chebfun: A new Chebfun representing the pointwise maximum or minimum. 

828 """ 

829 # Handle empty Chebfuns 

830 if self.isempty or other.isempty: 

831 return self.__class__.initempty() 

832 

833 # Find the intersection of domains 

834 try: 

835 # Try to use union if supports match 

836 newdom = self.domain.union(other.domain) 

837 except SupportMismatch: 

838 # If supports don't match, find the intersection 

839 a_min, a_max = self.support 

840 b_min, b_max = other.support 

841 

842 # Calculate intersection 

843 c_min = max(a_min, b_min) 

844 c_max = min(a_max, b_max) 

845 

846 # If there's no intersection, return empty 

847 if c_min >= c_max: 

848 return self.__class__.initempty() 

849 

850 # Restrict both functions to the intersection 

851 self_restricted = self.restrict([c_min, c_max]) 

852 other_restricted = other.restrict([c_min, c_max]) 

853 

854 # Recursively call with the restricted functions 

855 return self_restricted._maximum_minimum(other_restricted, comparator) 

856 

857 # Continue with the original algorithm 

858 roots = (self - other).roots() 

859 newdom = newdom.merge(roots) 

860 switch = newdom.support.merge(roots) 

861 

862 # Handle the case where switch is empty 

863 if switch.size == 0: # pragma: no cover 

864 return self.__class__.initempty() 

865 

866 keys = 0.5 * ((-1) ** np.arange(switch.size - 1) + 1) 

867 if switch.size > 0 and comparator(other(switch[0]), self(switch[0])): 

868 keys = 1 - keys 

869 funs = np.array([]) 

870 for interval, use_self in zip(switch.intervals, keys): 

871 subdom = newdom.restrict(interval) 

872 if use_self: 

873 subfun = self.restrict(subdom) 

874 else: 

875 subfun = other.restrict(subdom) 

876 funs = np.append(funs, subfun.funs) 

877 return self.__class__(funs) 

878 

879 # ---------- 

880 # plotting 

881 # ---------- 

882 def plot(self, ax=None, **kwds): 

883 """Plot the Chebfun over its domain. 

884 

885 This method plots the Chebfun over its domain using matplotlib. 

886 For complex-valued Chebfuns, it plots the real part against the imaginary part. 

887 

888 Args: 

889 ax (matplotlib.axes.Axes, optional): The axes on which to plot. If None, 

890 a new axes will be created. Defaults to None. 

891 **kwds: Additional keyword arguments to pass to matplotlib's plot function. 

892 

893 Returns: 

894 matplotlib.axes.Axes: The axes on which the plot was created. 

895 """ 

896 return plotfun(self, self.support, ax=ax, **kwds) 

897 

898 def plotcoeffs(self, ax=None, **kwds): 

899 """Plot the coefficients of the Chebfun on a semilogy scale. 

900 

901 This method plots the absolute values of the coefficients for each piece 

902 of the Chebfun on a semilogy scale, which is useful for visualizing the 

903 decay of coefficients in the Chebyshev series. 

904 

905 Args: 

906 ax (matplotlib.axes.Axes, optional): The axes on which to plot. If None, 

907 a new axes will be created. Defaults to None. 

908 **kwds: Additional keyword arguments to pass to matplotlib's semilogy function. 

909 

910 Returns: 

911 matplotlib.axes.Axes: The axes on which the plot was created. 

912 """ 

913 ax = ax or plt.gca() 

914 for fun in self: 

915 fun.plotcoeffs(ax=ax, **kwds) 

916 return ax 

917 

918 

919# --------- 

920# ufuncs 

921# --------- 

922def add_ufunc(op): 

923 """Add a NumPy universal function method to the Chebfun class. 

924 

925 This function creates a method that applies a NumPy universal function (ufunc) 

926 to each piece of a Chebfun and returns a new Chebfun representing the result. 

927 

928 Args: 

929 op (callable): The NumPy universal function to apply. 

930 

931 Note: 

932 The created method will have the same name as the NumPy function 

933 and will take no arguments other than self. 

934 """ 

935 

936 @self_empty() 

937 def method(self): 

938 """Apply a NumPy universal function to this Chebfun. 

939 

940 This method applies a NumPy universal function (ufunc) to each piece 

941 of this Chebfun and returns a new Chebfun representing the result. 

942 

943 Args: 

944 self (Chebfun): The Chebfun object to which the function is applied. 

945 

946 Returns: 

947 Chebfun: A new Chebfun representing op(f(x)). 

948 """ 

949 return self.__class__([op(fun) for fun in self]) 

950 

951 name = op.__name__ 

952 method.__name__ = name 

953 method.__doc__ = method.__doc__ 

954 setattr(Chebfun, name, method) 

955 

956 

957ufuncs = ( 

958 np.arccos, 

959 np.arccosh, 

960 np.arcsin, 

961 np.arcsinh, 

962 np.arctan, 

963 np.arctanh, 

964 np.cos, 

965 np.cosh, 

966 np.exp, 

967 np.exp2, 

968 np.expm1, 

969 np.log, 

970 np.log2, 

971 np.log10, 

972 np.log1p, 

973 np.sinh, 

974 np.sin, 

975 np.tan, 

976 np.tanh, 

977 np.sqrt, 

978) 

979 

980for op in ufuncs: 

981 add_ufunc(op)