Coverage for src / chebpy / chebfun.py: 98%

347 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-20 05:55 +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 Examples: 

68 >>> f = Chebfun.initempty() 

69 >>> f.isempty 

70 True 

71 """ 

72 return cls([]) 

73 

74 @classmethod 

75 def initidentity(cls, domain=None): 

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

77 

78 Args: 

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

80 If None, uses the default domain from preferences. 

81 

82 Returns: 

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

84 

85 Examples: 

86 >>> import numpy as np 

87 >>> x = Chebfun.initidentity([-1, 1]) 

88 >>> float(x(0.5)) 

89 0.5 

90 >>> np.allclose(x([0, 0.5, 1]), [0, 0.5, 1]) 

91 True 

92 """ 

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

94 

95 @classmethod 

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

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

98 

99 Args: 

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

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

102 If None, uses the default domain from preferences. 

103 

104 Returns: 

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

106 

107 Examples: 

108 >>> import numpy as np 

109 >>> f = Chebfun.initconst(3.14, [-1, 1]) 

110 >>> float(f(0)) 

111 3.14 

112 >>> float(f(0.5)) 

113 3.14 

114 >>> np.allclose(f([0, 0.5, 1]), [3.14, 3.14, 3.14]) 

115 True 

116 """ 

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

118 

119 @classmethod 

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

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

122 

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

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

125 

126 Args: 

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

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

129 If None, uses the default domain from preferences. 

130 

131 Returns: 

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

133 

134 Examples: 

135 >>> import numpy as np 

136 >>> f = Chebfun.initfun_adaptive(lambda x: np.sin(x), [-np.pi, np.pi]) 

137 >>> bool(abs(f(0)) < 1e-10) 

138 True 

139 >>> bool(abs(f(np.pi/2) - 1) < 1e-10) 

140 True 

141 """ 

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

143 

144 @classmethod 

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

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

147 

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

149 rather than determining the number adaptively. 

150 

151 Args: 

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

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

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

155 the size of the domain. 

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

157 If None, uses the default domain from preferences. 

158 

159 Returns: 

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

161 

162 Raises: 

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

164 """ 

165 nn = np.array(n) 

166 if nn.size < 2: 

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

168 else: 

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

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

171 raise BadFunLengthArgument 

172 funs = [] 

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

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

175 return cls(funs) 

176 

177 @classmethod 

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

179 """Initialize a Chebfun from a function. 

180 

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

182 or initfun_fixedlen based on whether n is provided. 

183 

184 Args: 

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

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

187 If None, uses the default domain from preferences. 

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

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

190 

191 Returns: 

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

193 """ 

194 if n is None: 

195 return cls.initfun_adaptive(f, domain) 

196 else: 

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

198 

199 # -------------------- 

200 # operator overloads 

201 # -------------------- 

202 def __add__(self, f): 

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

204 

205 Args: 

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

207 

208 Returns: 

209 Chebfun: A new Chebfun representing the sum. 

210 """ 

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

212 

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

214 @float_argument 

215 def __call__(self, x): 

216 """Evaluate the Chebfun at points x. 

217 

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

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

220 

221 Args: 

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

223 

224 Returns: 

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

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

227 """ 

228 # initialise output 

229 dtype = complex if self.iscomplex else float 

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

231 

232 # evaluate a fun when x is an interior point 

233 for fun in self: 

234 idx = fun.interval.isinterior(x) 

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

236 

237 # evaluate the breakpoint data for x at a breakpoint 

238 breakpoints = self.breakpoints 

239 for break_point in breakpoints: 

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

241 

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

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

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

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

246 return out 

247 

248 def __iter__(self): 

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

250 

251 Returns: 

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

253 """ 

254 return self.funs.__iter__() 

255 

256 def __len__(self): 

257 """Return the total number of coefficients across all funs. 

258 

259 Returns: 

260 int: The sum of sizes of all constituent funs. 

261 """ 

262 return sum(f.size for f in self.funs) 

263 

264 def __eq__(self, other): 

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

266 

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

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

269 

270 Args: 

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

272 

273 Returns: 

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

275 """ 

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

277 return False 

278 

279 # Check if both are empty 

280 if self.isempty and other.isempty: 

281 return True 

282 

283 # Check if domains are equal 

284 if self.domain != other.domain: 

285 return False 

286 

287 # Check function values at test points 

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

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

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

291 

292 def __mul__(self, f): 

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

294 

295 Args: 

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

297 

298 Returns: 

299 Chebfun: A new Chebfun representing the product. 

300 """ 

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

302 

303 def __neg__(self): 

304 """Return the negative of this Chebfun. 

305 

306 Returns: 

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

308 """ 

309 return self.__class__(-self.funs) 

310 

311 def __pos__(self): 

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

313 

314 Returns: 

315 Chebfun: This Chebfun object (unchanged). 

316 """ 

317 return self 

318 

319 def __abs__(self): 

320 """Return the absolute value of this Chebfun. 

321 

322 Returns: 

323 Chebfun: A new Chebfun representing |f(x)|. 

324 """ 

325 abs_funs = [] 

326 for fun in self.funs: 

327 abs_funs.append(fun.absolute()) 

328 return self.__class__(abs_funs) 

329 

330 def __pow__(self, f): 

331 """Raise this Chebfun to a power. 

332 

333 Args: 

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

335 

336 Returns: 

337 Chebfun: A new Chebfun representing self^f. 

338 """ 

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

340 

341 def __rtruediv__(self, c): 

342 """Divide a scalar by this Chebfun. 

343 

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

345 

346 Args: 

347 c (scalar): The scalar numerator. 

348 

349 Returns: 

350 Chebfun: A new Chebfun representing c / self. 

351 

352 Note: 

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

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

355 """ 

356 

357 def constfun(cheb, const): 

358 return 0.0 * cheb + const 

359 

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

361 return self.__class__(newfuns) 

362 

363 @self_empty("Chebfun<empty>") 

364 def __repr__(self): 

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

366 

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

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

369 

370 Returns: 

371 str: A string representation of the Chebfun. 

372 """ 

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

374 numpcs = self.funs.size 

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

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

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

378 toprow = " interval length endpoint values\n" 

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

380 rowdta = "" 

381 for fun in self: 

382 endpts = fun.support 

383 xl, xr = endpts 

384 fl, fr = fun(endpts) 

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

386 rowdta += row 

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

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

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

390 

391 def __rsub__(self, f): 

392 """Subtract this Chebfun from another object. 

393 

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

395 i.e., f - self. 

396 

397 Args: 

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

399 

400 Returns: 

401 Chebfun: A new Chebfun representing f - self. 

402 """ 

403 return -(self - f) 

404 

405 @cast_arg_to_chebfun 

406 def __rpow__(self, f): 

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

408 

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

410 i.e., f ** self. 

411 

412 Args: 

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

414 

415 Returns: 

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

417 """ 

418 return f**self 

419 

420 def __truediv__(self, f): 

421 """Divide this Chebfun by another object. 

422 

423 Args: 

424 f (Chebfun or scalar): The divisor. 

425 

426 Returns: 

427 Chebfun: A new Chebfun representing self / f. 

428 """ 

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

430 

431 __rmul__ = __mul__ 

432 __div__ = __truediv__ 

433 __rdiv__ = __rtruediv__ 

434 __radd__ = __add__ 

435 

436 def __str__(self): 

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

438 

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

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

441 

442 Returns: 

443 str: A concise string representation of the Chebfun. 

444 """ 

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

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

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

448 return out 

449 

450 def __sub__(self, f): 

451 """Subtract another object from this Chebfun. 

452 

453 Args: 

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

455 

456 Returns: 

457 Chebfun: A new Chebfun representing self - f. 

458 """ 

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

460 

461 # ------------------ 

462 # internal helpers 

463 # ------------------ 

464 @self_empty() 

465 def _apply_binop(self, f, op): 

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

467 

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

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

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

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

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

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

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

475 

476 Args: 

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

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

479 

480 Returns: 

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

482 """ 

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

484 return f 

485 if np.isscalar(f): 

486 chbfn1 = self 

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

488 simplify = False 

489 else: 

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

491 chbfn1 = self._break(newdom) 

492 chbfn2 = f._break(newdom) 

493 simplify = True 

494 newfuns = [] 

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

496 newfun = op(fun1, fun2) 

497 if simplify: 

498 newfun = newfun.simplify() 

499 newfuns.append(newfun) 

500 return self.__class__(newfuns) 

501 

502 def _break(self, targetdomain): 

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

504 

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

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

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

508 

509 Args: 

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

511 

512 Returns: 

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

514 """ 

515 newfuns = [] 

516 subintervals = targetdomain.intervals 

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

518 for fun in self: 

519 while interval in fun.interval: 

520 newfun = fun.restrict(interval) 

521 newfuns.append(newfun) 

522 try: 

523 interval = next(subintervals) 

524 except StopIteration: 

525 break 

526 return self.__class__(newfuns) 

527 

528 # ------------ 

529 # properties 

530 # ------------ 

531 @property 

532 def breakpoints(self): 

533 """Get the breakpoints of this Chebfun. 

534 

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

536 

537 Returns: 

538 numpy.ndarray: Array of breakpoints. 

539 """ 

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

541 

542 @property 

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

544 def domain(self): 

545 """Get the domain of this Chebfun. 

546 

547 Returns: 

548 Domain: A Domain object corresponding to this Chebfun. 

549 """ 

550 return Domain.from_chebfun(self) 

551 

552 @domain.setter 

553 def domain(self, new_domain): 

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

555 

556 Args: 

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

558 """ 

559 self.restrict_(new_domain) 

560 

561 @property 

562 @self_empty(Domain([])) 

563 def support(self): 

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

565 

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

567 

568 Returns: 

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

570 """ 

571 return self.domain.support 

572 

573 @property 

574 @self_empty(0.0) 

575 def hscale(self): 

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

577 

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

579 

580 Returns: 

581 float: The horizontal scale. 

582 """ 

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

584 

585 @property 

586 @self_empty(False) 

587 def iscomplex(self): 

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

589 

590 Returns: 

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

592 False otherwise. 

593 """ 

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

595 

596 @property 

597 @self_empty(False) 

598 def isconst(self): 

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

600 

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

602 

603 Returns: 

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

605 

606 Note: 

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

608 """ 

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

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

611 

612 @property 

613 def isempty(self): 

614 """Check if this Chebfun is empty. 

615 

616 An empty Chebfun contains no functions. 

617 

618 Returns: 

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

620 """ 

621 return self.funs.size == 0 

622 

623 @property 

624 @self_empty(0.0) 

625 def vscale(self): 

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

627 

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

629 

630 Returns: 

631 float: The vertical scale. 

632 """ 

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

634 

635 @property 

636 @self_empty() 

637 def x(self): 

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

639 

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

641 defined on the same support as this Chebfun. 

642 

643 Returns: 

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

645 """ 

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

647 

648 # ----------- 

649 # utilities 

650 # ---------- 

651 

652 def imag(self): 

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

654 

655 Returns: 

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

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

658 """ 

659 if self.iscomplex: 

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

661 else: 

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

663 

664 def real(self): 

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

666 

667 Returns: 

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

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

670 """ 

671 if self.iscomplex: 

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

673 else: 

674 return self 

675 

676 def copy(self): 

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

678 

679 Returns: 

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

681 """ 

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

683 

684 @self_empty() 

685 def _restrict(self, subinterval): 

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

687 

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

689 without performing simplification. 

690 

691 Args: 

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

693 

694 Returns: 

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

696 """ 

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

698 return self._break(newdom) 

699 

700 def restrict(self, subinterval): 

701 """Restrict a Chebfun to a subinterval. 

702 

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

704 and simplifies the result. 

705 

706 Args: 

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

708 

709 Returns: 

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

711 """ 

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

713 

714 @self_empty() 

715 def restrict_(self, subinterval): 

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

717 

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

719 subinterval and simplifying the result. 

720 

721 Args: 

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

723 

724 Returns: 

725 Chebfun: The modified Chebfun (self). 

726 """ 

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

728 self.funs = restricted.funs 

729 self.breakdata = compute_breakdata(self.funs) 

730 return self 

731 

732 @cache 

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

734 def roots(self, merge=None): 

735 """Compute the roots of a Chebfun. 

736 

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

738 of each piece of the Chebfun and combining them. 

739 

740 Args: 

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

742 uses the value from preferences. Defaults to None. 

743 

744 Returns: 

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

746 

747 Examples: 

748 >>> import numpy as np 

749 >>> f = Chebfun.initfun_adaptive(lambda x: x**2 - 1, [-2, 2]) 

750 >>> roots = f.roots() 

751 >>> len(roots) 

752 2 

753 >>> np.allclose(sorted(roots), [-1, 1]) 

754 True 

755 """ 

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

757 allrts = [] 

758 prvrts = np.array([]) 

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

760 for fun in self: 

761 rts = fun.roots() 

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

763 # TODO: there could be multiple roots at breakpoints 

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

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

766 rts = rts[1:] 

767 allrts.append(rts) 

768 prvrts = rts 

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

770 

771 @self_empty() 

772 def simplify(self): 

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

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

775 

776 def translate(self, c): 

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

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

779 

780 # ---------- 

781 # calculus 

782 # ---------- 

783 def cumsum(self): 

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

785 

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

787 constant of integration chosen so that the indefinite integral evaluates 

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

789 are added to ensure continuity across the pieces. 

790 

791 Returns: 

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

793 

794 Examples: 

795 >>> import numpy as np 

796 >>> f = Chebfun.initconst(1.0, [-1, 1]) 

797 >>> F = f.cumsum() 

798 >>> bool(abs(F(-1)) < 1e-10) 

799 True 

800 >>> bool(abs(F(1) - 2.0) < 1e-10) 

801 True 

802 """ 

803 newfuns = [] 

804 prevfun = None 

805 for fun in self: 

806 integral = fun.cumsum() 

807 if prevfun: 

808 # enforce continuity by adding the function value 

809 # at the right endpoint of the previous fun 

810 _, fb = prevfun.endvalues 

811 integral = integral + fb 

812 newfuns.append(integral) 

813 prevfun = integral 

814 return self.__class__(newfuns) 

815 

816 def diff(self, n=1): 

817 """Compute the derivative of the Chebfun. 

818 

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

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

821 corresponding piece in the original Chebfun. 

822 

823 Args: 

824 n: Order of differentiation (default: 1). Must be non-negative integer. 

825 

826 Returns: 

827 Chebfun: A new Chebfun representing the nth derivative of this Chebfun. 

828 

829 Examples: 

830 >>> from chebpy import chebfun 

831 >>> f = chebfun(lambda x: x**3) 

832 >>> df1 = f.diff() # first derivative: 3*x**2 

833 >>> df2 = f.diff(2) # second derivative: 6*x 

834 >>> df3 = f.diff(3) # third derivative: 6 

835 >>> bool(abs(df1(0.5) - 0.75) < 1e-10) 

836 True 

837 >>> bool(abs(df2(0.5) - 3.0) < 1e-10) 

838 True 

839 >>> bool(abs(df3(0.5) - 6.0) < 1e-10) 

840 True 

841 """ 

842 if not isinstance(n, int): 

843 raise TypeError("Derivative order must be an integer") 

844 if n == 0: 

845 return self 

846 if n < 0: 

847 raise ValueError("Derivative order must be non-negative") 

848 

849 result = self 

850 for _ in range(n): 

851 dfuns = np.array([fun.diff() for fun in result]) 

852 result = self.__class__(dfuns) 

853 return result 

854 

855 def sum(self): 

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

857 

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

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

860 piece. 

861 

862 Returns: 

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

864 

865 Examples: 

866 >>> import numpy as np 

867 >>> f = Chebfun.initfun_adaptive(lambda x: x**2, [-1, 1]) 

868 >>> bool(abs(f.sum() - 2.0/3.0) < 1e-10) 

869 True 

870 >>> g = Chebfun.initconst(1.0, [-1, 1]) 

871 >>> bool(abs(g.sum() - 2.0) < 1e-10) 

872 True 

873 """ 

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

875 

876 def dot(self, f): 

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

878 

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

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

881 the result over the domain. 

882 

883 Args: 

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

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

886 

887 Returns: 

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

889 """ 

890 return (self * f).sum() 

891 

892 def norm(self, p=2): 

893 """Compute the Lp norm of the Chebfun over its domain. 

894 

895 This method calculates the Lp norm of the Chebfun. The L2 norm is the 

896 default and is computed as sqrt(integral(|f|^2)). For p=inf, returns 

897 the maximum absolute value by checking critical points (extrema). 

898 

899 Args: 

900 p (int or float): The norm type. Supported values are 1, 2, positive 

901 integers/floats, or np.inf. Defaults to 2 (L2 norm). 

902 

903 Returns: 

904 float: The Lp norm of the Chebfun. 

905 

906 Examples: 

907 >>> from chebpy import chebfun 

908 >>> import numpy as np 

909 >>> f = chebfun(lambda x: x**2, [-1, 1]) 

910 >>> np.allclose(f.norm(), 0.6324555320336759) # L2 norm 

911 True 

912 >>> np.allclose(f.norm(np.inf), 1.0) # Maximum absolute value 

913 True 

914 """ 

915 if p == 2: 

916 # L2 norm: sqrt(integral(|f|^2)) 

917 return np.sqrt(self.dot(self)) 

918 elif p == np.inf: 

919 # L-infinity norm: max|f(x)| 

920 df = self.diff() 

921 critical_pts = df.roots() 

922 # Add endpoints 

923 endpoints = np.array([self.domain[0], self.domain[-1]]) 

924 # Combine all test points 

925 test_pts = np.concatenate([critical_pts, endpoints]) 

926 # Evaluate and find max 

927 vals = np.abs(self(test_pts)) 

928 return np.max(vals) 

929 elif p == 1: 

930 # L1 norm: integral(|f|) 

931 return self.absolute().sum() 

932 elif p > 0: 

933 # General Lp norm: (integral(|f|^p))^(1/p) 

934 f_abs = self.absolute() 

935 f_pow_p = f_abs**p 

936 integral = f_pow_p.sum() 

937 return integral ** (1.0 / p) 

938 else: 

939 raise ValueError(f"norm(p={p}): p must be positive or np.inf") 

940 

941 # ---------- 

942 # utilities 

943 # ---------- 

944 @self_empty() 

945 def absolute(self): 

946 """Absolute value of a Chebfun.""" 

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

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

949 return self.__class__(funs) 

950 

951 abs = absolute 

952 

953 @self_empty() 

954 @cast_arg_to_chebfun 

955 def maximum(self, other): 

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

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

958 

959 @self_empty() 

960 @cast_arg_to_chebfun 

961 def minimum(self, other): 

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

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

964 

965 def _maximum_minimum(self, other, comparator): 

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

967 

968 This internal method implements the algorithm for computing the pointwise 

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

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

971 

972 Args: 

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

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

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

976 this is operator.lt (<). 

977 

978 Returns: 

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

980 """ 

981 # Handle empty Chebfuns 

982 if self.isempty or other.isempty: 

983 return self.__class__.initempty() 

984 

985 # Find the intersection of domains 

986 try: 

987 # Try to use union if supports match 

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

989 except SupportMismatch: 

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

991 a_min, a_max = self.support 

992 b_min, b_max = other.support 

993 

994 # Calculate intersection 

995 c_min = max(a_min, b_min) 

996 c_max = min(a_max, b_max) 

997 

998 # If there's no intersection, return empty 

999 if c_min >= c_max: 

1000 return self.__class__.initempty() 

1001 

1002 # Restrict both functions to the intersection 

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

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

1005 

1006 # Recursively call with the restricted functions 

1007 return self_restricted._maximum_minimum(other_restricted, comparator) 

1008 

1009 # Continue with the original algorithm 

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

1011 newdom = newdom.merge(roots) 

1012 switch = newdom.support.merge(roots) 

1013 

1014 # Handle the case where switch is empty 

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

1016 return self.__class__.initempty() 

1017 

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

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

1020 keys = 1 - keys 

1021 funs = np.array([]) 

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

1023 subdom = newdom.restrict(interval) 

1024 if use_self: 

1025 subfun = self.restrict(subdom) 

1026 else: 

1027 subfun = other.restrict(subdom) 

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

1029 return self.__class__(funs) 

1030 

1031 # ---------- 

1032 # plotting 

1033 # ---------- 

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

1035 """Plot the Chebfun over its domain. 

1036 

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

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

1039 

1040 Args: 

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

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

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

1044 

1045 Returns: 

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

1047 """ 

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

1049 

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

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

1052 

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

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

1055 decay of coefficients in the Chebyshev series. 

1056 

1057 Args: 

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

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

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

1061 

1062 Returns: 

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

1064 """ 

1065 ax = ax or plt.gca() 

1066 for fun in self: 

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

1068 return ax 

1069 

1070 

1071# --------- 

1072# ufuncs 

1073# --------- 

1074def add_ufunc(op): 

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

1076 

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

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

1079 

1080 Args: 

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

1082 

1083 Note: 

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

1085 and will take no arguments other than self. 

1086 """ 

1087 

1088 @self_empty() 

1089 def method(self): 

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

1091 

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

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

1094 

1095 Args: 

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

1097 

1098 Returns: 

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

1100 """ 

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

1102 

1103 name = op.__name__ 

1104 method.__name__ = name 

1105 method.__doc__ = method.__doc__ 

1106 setattr(Chebfun, name, method) 

1107 

1108 

1109ufuncs = ( 

1110 np.arccos, 

1111 np.arccosh, 

1112 np.arcsin, 

1113 np.arcsinh, 

1114 np.arctan, 

1115 np.arctanh, 

1116 np.cos, 

1117 np.cosh, 

1118 np.exp, 

1119 np.exp2, 

1120 np.expm1, 

1121 np.log, 

1122 np.log2, 

1123 np.log10, 

1124 np.log1p, 

1125 np.sinh, 

1126 np.sin, 

1127 np.tan, 

1128 np.tanh, 

1129 np.sqrt, 

1130) 

1131 

1132for op in ufuncs: 

1133 add_ufunc(op)