1"""

2 Abstract base class for the various polynomial Classes.

3

4 The ABCPolyBase class provides the methods needed to implement the common API

5 for the various polynomial classes. It operates as a mixin, but uses the

6 abc module from the stdlib, hence it is only available for Python >= 2.6.

7

8"""

9 import os

10 import abc

11 import numbers

12

13 import numpy as np

14 from . import polyutils as pu

15

16__all__ = ['ABCPolyBase']

17

18 class ABCPolyBase(abc.ABC):

19 """An abstract base class for immutable series classes.

20

21 ABCPolyBase provides the standard Python numerical methods

22 '+', '-', '*', '//', '%', 'divmod', '**', and '()' along with the

23 methods listed below.

24

25 .. versionadded:: 1.9.0

26

27 Parameters

28 ----------

29 coef : array_like

30 Series coefficients in order of increasing degree, i.e.,

31 ``(1, 2, 3)`` gives ``1*P_0(x) + 2*P_1(x) + 3*P_2(x)``, where

32 ``P_i`` is the basis polynomials of degree ``i``.

33 domain : (2,) array_like, optional

34 Domain to use. The interval ``[domain[0], domain[1]]`` is mapped

35 to the interval ``[window[0], window[1]]`` by shifting and scaling.

36 The default value is the derived class domain.

37 window : (2,) array_like, optional

38 Window, see domain for its use. The default value is the

39 derived class window.

40 symbol : str, optional

41 Symbol used to represent the independent variable in string

42 representations of the polynomial expression, e.g. for printing.

43 The symbol must be a valid Python identifier. Default value is 'x'.

44

45 .. versionadded:: 1.24

46

47 Attributes

48 ----------

49 coef : (N,) ndarray

50 Series coefficients in order of increasing degree.

51 domain : (2,) ndarray

52 Domain that is mapped to window.

53 window : (2,) ndarray

54 Window that domain is mapped to.

55 symbol : str

56 Symbol representing the independent variable.

57

58 Class Attributes

59 ----------------

60 maxpower : int

61 Maximum power allowed, i.e., the largest number ``n`` such that

62 ``p(x)**n`` is allowed. This is to limit runaway polynomial size.

63 domain : (2,) ndarray

64 Default domain of the class.

65 window : (2,) ndarray

66 Default window of the class.

67

68 """

69

70 # Not hashable

71 __hash__ = None

72

73 # Opt out of numpy ufuncs and Python ops with ndarray subclasses.

74 __array_ufunc__ = None

75

76 # Limit runaway size. T_n^m has degree n*m

77 maxpower = 100

78

79 # Unicode character mappings for improved __str__

80 _superscript_mapping = str.maketrans({

81 "0": "⁰",

82 "1": "¹",

83 "2": "²",

84 "3": "³",

85 "4": "⁴",

86 "5": "⁵",

87 "6": "⁶",

88 "7": "⁷",

89 "8": "⁸",

90 "9": "⁹"

91 })

92 _subscript_mapping = str.maketrans({

93 "0": "₀",

94 "1": "₁",

95 "2": "₂",

96 "3": "₃",

97 "4": "₄",

98 "5": "₅",

99 "6": "₆",

100 "7": "₇",

101 "8": "₈",

102 "9": "₉"

103 })

104 # Some fonts don't support full unicode character ranges necessary for

105 # the full set of superscripts and subscripts, including common/default

106 # fonts in Windows shells/terminals. Therefore, default to ascii-only

107 # printing on windows.

108 _use_unicode = not os.name == 'nt'

109

110 @property

111 def symbol(self):

112 return self._symbol

113

114 @property

115 @abc.abstractmethod

116 def domain(self):

117 pass

118

119 @property

120 @abc.abstractmethod

121 def window(self):

122 pass

123

124 @property

125 @abc.abstractmethod

126 def basis_name(self):

127 pass

128

129 @staticmethod

130 @abc.abstractmethod

131 def _add(c1, c2):

132 pass

133

134 @staticmethod

135 @abc.abstractmethod

136 def _sub(c1, c2):

137 pass

138

139 @staticmethod

140 @abc.abstractmethod

141 def _mul(c1, c2):

142 pass

143

144 @staticmethod

145 @abc.abstractmethod

146 def _div(c1, c2):

147 pass

148

149 @staticmethod

150 @abc.abstractmethod

151 def _pow(c, pow, maxpower=None):

152 pass

153

154 @staticmethod

155 @abc.abstractmethod

156 def _val(x, c):

157 pass

158

159 @staticmethod

160 @abc.abstractmethod

161 def _int(c, m, k, lbnd, scl):

162 pass

163

164 @staticmethod

165 @abc.abstractmethod

166 def _der(c, m, scl):

167 pass

168

169 @staticmethod

170 @abc.abstractmethod

171 def _fit(x, y, deg, rcond, full):

172 pass

173

174 @staticmethod

175 @abc.abstractmethod

176 def _line(off, scl):

177 pass

178

179 @staticmethod

180 @abc.abstractmethod

181 def _roots(c):

182 pass

183

184 @staticmethod

185 @abc.abstractmethod

186 def _fromroots(r):

187 pass

188

189 def has_samecoef(self, other):

190 """Check if coefficients match.

191

192 .. versionadded:: 1.6.0

193

194 Parameters

195 ----------

196 other : class instance

197 The other class must have the ``coef`` attribute.

198

199 Returns

200 -------

201 bool : boolean

202 True if the coefficients are the same, False otherwise.

203

204 """

205 if len(self.coef) != len(other.coef):

206 return False

207 elif not np.all(self.coef == other.coef):

208 return False

209 else:

210 return True

211

212 def has_samedomain(self, other):

213 """Check if domains match.

214

215 .. versionadded:: 1.6.0

216

217 Parameters

218 ----------

219 other : class instance

220 The other class must have the ``domain`` attribute.

221

222 Returns

223 -------

224 bool : boolean

225 True if the domains are the same, False otherwise.

226

227 """

228 return np.all(self.domain == other.domain)

229

230 def has_samewindow(self, other):

231 """Check if windows match.

232

233 .. versionadded:: 1.6.0

234

235 Parameters

236 ----------

237 other : class instance

238 The other class must have the ``window`` attribute.

239

240 Returns

241 -------

242 bool : boolean

243 True if the windows are the same, False otherwise.

244

245 """

246 return np.all(self.window == other.window)

247

248 def has_sametype(self, other):

249 """Check if types match.

250

251 .. versionadded:: 1.7.0

252

253 Parameters

254 ----------

255 other : object

256 Class instance.

257

258 Returns

259 -------

260 bool : boolean

261 True if other is same class as self

262

263 """

264 return isinstance(other, self.__class__)

265

266 def _get_coefficients(self, other):

267 """Interpret other as polynomial coefficients.

268

269 The `other` argument is checked to see if it is of the same

270 class as self with identical domain and window. If so,

271 return its coefficients, otherwise return `other`.

272

273 .. versionadded:: 1.9.0

274

275 Parameters

276 ----------

277 other : anything

278 Object to be checked.

279

280 Returns

281 -------

282 coef

283 The coefficients of`other` if it is a compatible instance,

284 of ABCPolyBase, otherwise `other`.

285

286 Raises

287 ------

288 TypeError

289 When `other` is an incompatible instance of ABCPolyBase.

290

291 """

292 if isinstance(other, ABCPolyBase):

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

294 raise TypeError("Polynomial types differ")

295 elif not np.all(self.domain == other.domain):

296 raise TypeError("Domains differ")

297 elif not np.all(self.window == other.window):

298 raise TypeError("Windows differ")

299 elif self.symbol != other.symbol:

300 raise ValueError("Polynomial symbols differ")

301 return other.coef

302 return other

303

304 def __init__(self, coef, domain=None, window=None, symbol='x'):

305 [coef] = pu.as_series([coef], trim=False)

306 self.coef = coef

307

308 if domain is not None:

309 [domain] = pu.as_series([domain], trim=False)

310 if len(domain) != 2:

311 raise ValueError("Domain has wrong number of elements.")

312 self.domain = domain

313

314 if window is not None:

315 [window] = pu.as_series([window], trim=False)

316 if len(window) != 2:

317 raise ValueError("Window has wrong number of elements.")

318 self.window = window

319

320 # Validation for symbol

321 try:

322 if not symbol.isidentifier():

323 raise ValueError(

324 "Symbol string must be a valid Python identifier"

325 )

326 # If a user passes in something other than a string, the above

327 # results in an AttributeError. Catch this and raise a more

328 # informative exception

329 except AttributeError:

330 raise TypeError("Symbol must be a non-empty string")

331

332 self._symbol = symbol

333

334 def __repr__(self):

335 coef = repr(self.coef)[6:-1]

336 domain = repr(self.domain)[6:-1]

337 window = repr(self.window)[6:-1]

338 name = self.__class__.__name__

339 return (f"{name}({coef}, domain={domain}, window={window}, "

340 f"symbol='{self.symbol}')")

341

342 def __format__(self, fmt_str):

343 if fmt_str == '':

344 return self.__str__()

345 if fmt_str not in ('ascii', 'unicode'):

346 raise ValueError(

347 f"Unsupported format string '{fmt_str}' passed to "

348 f"{self.__class__}.__format__. Valid options are "

349 f"'ascii' and 'unicode'"

350 )

351 if fmt_str == 'ascii':

352 return self._generate_string(self._str_term_ascii)

353 return self._generate_string(self._str_term_unicode)

354

355 def __str__(self):

356 if self._use_unicode:

357 return self._generate_string(self._str_term_unicode)

358 return self._generate_string(self._str_term_ascii)

359

360 def _generate_string(self, term_method):

361 """

362 Generate the full string representation of the polynomial, using

363 ``term_method`` to generate each polynomial term.

364 """

365 # Get configuration for line breaks

366 linewidth = np.get_printoptions().get('linewidth', 75)

367 if linewidth < 1:

368 linewidth = 1

369 out = pu.format_float(self.coef[0])

370 for i, coef in enumerate(self.coef[1:]):

371 out += " "

372 power = str(i + 1)

373 # Polynomial coefficient

374 # The coefficient array can be an object array with elements that

375 # will raise a TypeError with >= 0 (e.g. strings or Python

376 # complex). In this case, represent the coefficient as-is.

377 try:

378 if coef >= 0:

379 next_term = f"+ " + pu.format_float(coef, parens=True)

380 else:

381 next_term = f"- " + pu.format_float(-coef, parens=True)

382 except TypeError:

383 next_term = f"+ {coef}"

384 # Polynomial term

385 next_term += term_method(power, self.symbol)

386 # Length of the current line with next term added

387 line_len = len(out.split('\n')[-1]) + len(next_term)

388 # If not the last term in the polynomial, it will be two

389 # characters longer due to the +/- with the next term

390 if i < len(self.coef[1:]) - 1:

391 line_len += 2

392 # Handle linebreaking

393 if line_len >= linewidth:

394 next_term = next_term.replace(" ", "\n", 1)

395 out += next_term

396 return out

397

398 @classmethod

399 def _str_term_unicode(cls, i, arg_str):

400 """

401 String representation of single polynomial term using unicode

402 characters for superscripts and subscripts.

403 """

404 if cls.basis_name is None:

405 raise NotImplementedError(

406 "Subclasses must define either a basis_name, or override "

407 "_str_term_unicode(cls, i, arg_str)"

408 )

409 return (f"·{cls.basis_name}{i.translate(cls._subscript_mapping)}"

410 f"({arg_str})")

411

412 @classmethod

413 def _str_term_ascii(cls, i, arg_str):

414 """

415 String representation of a single polynomial term using ** and _ to

416 represent superscripts and subscripts, respectively.

417 """

418 if cls.basis_name is None:

419 raise NotImplementedError(

420 "Subclasses must define either a basis_name, or override "

421 "_str_term_ascii(cls, i, arg_str)"

422 )

423 return f" {cls.basis_name}_{i}({arg_str})"

424

425 @classmethod

426 def _repr_latex_term(cls, i, arg_str, needs_parens):

427 if cls.basis_name is None:

428 raise NotImplementedError(

429 "Subclasses must define either a basis name, or override "

430 "_repr_latex_term(i, arg_str, needs_parens)")

431 # since we always add parens, we don't care if the expression needs them

432 return f"{{{cls.basis_name}}}_{{{i}}}({arg_str})"

433

434 @staticmethod

435 def _repr_latex_scalar(x, parens=False):

436 # TODO: we're stuck with disabling math formatting until we handle

437 # exponents in this function

438 return r'\text{{{}}}'.format(pu.format_float(x, parens=parens))

439

440 def _repr_latex_(self):

441 # get the scaled argument string to the basis functions

442 off, scale = self.mapparms()

443 if off == 0 and scale == 1:

444 term = self.symbol

445 needs_parens = False

446 elif scale == 1:

447 term = f"{self._repr_latex_scalar(off)} + {self.symbol}"

448 needs_parens = True

449 elif off == 0:

450 term = f"{self._repr_latex_scalar(scale)}{self.symbol}"

451 needs_parens = True

452 else:

453 term = (

454 f"{self._repr_latex_scalar(off)} + "

455 f"{self._repr_latex_scalar(scale)}{self.symbol}"

456 )

457 needs_parens = True

458

459 mute = r"\color{{LightGray}}{{{}}}".format

460

461 parts = []

462 for i, c in enumerate(self.coef):

463 # prevent duplication of + and - signs

464 if i == 0:

465 coef_str = f"{self._repr_latex_scalar(c)}"

466 elif not isinstance(c, numbers.Real):

467 coef_str = f" + ({self._repr_latex_scalar(c)})"

468 elif not np.signbit(c):

469 coef_str = f" + {self._repr_latex_scalar(c, parens=True)}"

470 else:

471 coef_str = f" - {self._repr_latex_scalar(-c, parens=True)}"

472

473 # produce the string for the term

474 term_str = self._repr_latex_term(i, term, needs_parens)

475 if term_str == '1':

476 part = coef_str

477 else:

478 part = rf"{coef_str}\,{term_str}"

479

480 if c == 0:

481 part = mute(part)

482

483 parts.append(part)

484

485 if parts:

486 body = ''.join(parts)

487 else:

488 # in case somehow there are no coefficients at all

489 body = '0'

490

491 return rf"${self.symbol} \mapsto {body}$"

492

493

494

495 # Pickle and copy

496

497 def __getstate__(self):

498 ret = self.__dict__.copy()

499 ret['coef'] = self.coef.copy()

500 ret['domain'] = self.domain.copy()

501 ret['window'] = self.window.copy()

502 ret['symbol'] = self.symbol

503 return ret

504

505 def __setstate__(self, dict):

506 self.__dict__ = dict

507

508 # Call

509

510 def __call__(self, arg):

511 off, scl = pu.mapparms(self.domain, self.window)

512 arg = off + scl*arg

513 return self._val(arg, self.coef)

514

515 def __iter__(self):

516 return iter(self.coef)

517

518 def __len__(self):

519 return len(self.coef)

520

521 # Numeric properties.

522

523 def __neg__(self):

524 return self.__class__(

525 -self.coef, self.domain, self.window, self.symbol

526 )

527

528 def __pos__(self):

529 return self

530

531 def __add__(self, other):

532 othercoef = self._get_coefficients(other)

533 try:

534 coef = self._add(self.coef, othercoef)

535 except Exception:

536 return NotImplemented

537 return self.__class__(coef, self.domain, self.window, self.symbol)

538

539 def __sub__(self, other):

540 othercoef = self._get_coefficients(other)

541 try:

542 coef = self._sub(self.coef, othercoef)

543 except Exception:

544 return NotImplemented

545 return self.__class__(coef, self.domain, self.window, self.symbol)

546

547 def __mul__(self, other):

548 othercoef = self._get_coefficients(other)

549 try:

550 coef = self._mul(self.coef, othercoef)

551 except Exception:

552 return NotImplemented

553 return self.__class__(coef, self.domain, self.window, self.symbol)

554

555 def __truediv__(self, other):

556 # there is no true divide if the rhs is not a Number, although it

557 # could return the first n elements of an infinite series.

558 # It is hard to see where n would come from, though.

559 if not isinstance(other, numbers.Number) or isinstance(other, bool):

560 raise TypeError(

561 f"unsupported types for true division: "

562 f"'{type(self)}', '{type(other)}'"

563 )

564 return self.__floordiv__(other)

565

566 def __floordiv__(self, other):

567 res = self.__divmod__(other)

568 if res is NotImplemented:

569 return res

570 return res[0]

571

572 def __mod__(self, other):

573 res = self.__divmod__(other)

574 if res is NotImplemented:

575 return res

576 return res[1]

577

578 def __divmod__(self, other):

579 othercoef = self._get_coefficients(other)

580 try:

581 quo, rem = self._div(self.coef, othercoef)

582 except ZeroDivisionError:

583 raise

584 except Exception:

585 return NotImplemented

586 quo = self.__class__(quo, self.domain, self.window, self.symbol)

587 rem = self.__class__(rem, self.domain, self.window, self.symbol)

588 return quo, rem

589

590 def __pow__(self, other):

591 coef = self._pow(self.coef, other, maxpower=self.maxpower)

592 res = self.__class__(coef, self.domain, self.window, self.symbol)

593 return res

594

595 def __radd__(self, other):

596 try:

597 coef = self._add(other, self.coef)

598 except Exception:

599 return NotImplemented

600 return self.__class__(coef, self.domain, self.window, self.symbol)

601

602 def __rsub__(self, other):

603 try:

604 coef = self._sub(other, self.coef)

605 except Exception:

606 return NotImplemented

607 return self.__class__(coef, self.domain, self.window, self.symbol)

608

609 def __rmul__(self, other):

610 try:

611 coef = self._mul(other, self.coef)

612 except Exception:

613 return NotImplemented

614 return self.__class__(coef, self.domain, self.window, self.symbol)

615

616 def __rdiv__(self, other):

617 # set to __floordiv__ /.

618 return self.__rfloordiv__(other)

619

620 def __rtruediv__(self, other):

621 # An instance of ABCPolyBase is not considered a

622 # Number.

623 return NotImplemented

624

625 def __rfloordiv__(self, other):

626 res = self.__rdivmod__(other)

627 if res is NotImplemented:

628 return res

629 return res[0]

630

631 def __rmod__(self, other):

632 res = self.__rdivmod__(other)

633 if res is NotImplemented:

634 return res

635 return res[1]

636

637 def __rdivmod__(self, other):

638 try:

639 quo, rem = self._div(other, self.coef)

640 except ZeroDivisionError:

641 raise

642 except Exception:

643 return NotImplemented

644 quo = self.__class__(quo, self.domain, self.window, self.symbol)

645 rem = self.__class__(rem, self.domain, self.window, self.symbol)

646 return quo, rem

647

648 def __eq__(self, other):

649 res = (isinstance(other, self.__class__) and

650 np.all(self.domain == other.domain) and

651 np.all(self.window == other.window) and

652 (self.coef.shape == other.coef.shape) and

653 np.all(self.coef == other.coef) and

654 (self.symbol == other.symbol))

655 return res

656

657 def __ne__(self, other):

658 return not self.__eq__(other)

659

660 #

661 # Extra methods.

662 #

663

664 def copy(self):

665 """Return a copy.

666

667 Returns

668 -------

669 new_series : series

670 Copy of self.

671

672 """

673 return self.__class__(self.coef, self.domain, self.window, self.symbol)

674

675 def degree(self):

676 """The degree of the series.

677

678 .. versionadded:: 1.5.0

679

680 Returns

681 -------

682 degree : int

683 Degree of the series, one less than the number of coefficients.

684

685 """

686 return len(self) - 1

687

688 def cutdeg(self, deg):

689 """Truncate series to the given degree.

690

691 Reduce the degree of the series to `deg` by discarding the

692 high order terms. If `deg` is greater than the current degree a

693 copy of the current series is returned. This can be useful in least

694 squares where the coefficients of the high degree terms may be very

695 small.

696

697 .. versionadded:: 1.5.0

698

699 Parameters

700 ----------

701 deg : non-negative int

702 The series is reduced to degree `deg` by discarding the high

703 order terms. The value of `deg` must be a non-negative integer.

704

705 Returns

706 -------

707 new_series : series

708 New instance of series with reduced degree.

709

710 """

711 return self.truncate(deg + 1)

712

713 def trim(self, tol=0):

714 """Remove trailing coefficients

715

716 Remove trailing coefficients until a coefficient is reached whose

717 absolute value greater than `tol` or the beginning of the series is

718 reached. If all the coefficients would be removed the series is set

719 to ``[0]``. A new series instance is returned with the new

720 coefficients. The current instance remains unchanged.

721

722 Parameters

723 ----------

724 tol : non-negative number.

725 All trailing coefficients less than `tol` will be removed.

726

727 Returns

728 -------

729 new_series : series

730 New instance of series with trimmed coefficients.

731

732 """

733 coef = pu.trimcoef(self.coef, tol)

734 return self.__class__(coef, self.domain, self.window, self.symbol)

735

736 def truncate(self, size):

737 """Truncate series to length `size`.

738

739 Reduce the series to length `size` by discarding the high

740 degree terms. The value of `size` must be a positive integer. This

741 can be useful in least squares where the coefficients of the

742 high degree terms may be very small.

743

744 Parameters

745 ----------

746 size : positive int

747 The series is reduced to length `size` by discarding the high

748 degree terms. The value of `size` must be a positive integer.

749

750 Returns

751 -------

752 new_series : series

753 New instance of series with truncated coefficients.

754

755 """

756 isize = int(size)

757 if isize != size or isize < 1:

758 raise ValueError("size must be a positive integer")

759 if isize >= len(self.coef):

760 coef = self.coef

761 else:

762 coef = self.coef[:isize]

763 return self.__class__(coef, self.domain, self.window, self.symbol)

764

765 def convert(self, domain=None, kind=None, window=None):

766 """Convert series to a different kind and/or domain and/or window.

767

768 Parameters

769 ----------

770 domain : array_like, optional

771 The domain of the converted series. If the value is None,

772 the default domain of `kind` is used.

773 kind : class, optional

774 The polynomial series type class to which the current instance

775 should be converted. If kind is None, then the class of the

776 current instance is used.

777 window : array_like, optional

778 The window of the converted series. If the value is None,

779 the default window of `kind` is used.

780

781 Returns

782 -------

783 new_series : series

784 The returned class can be of different type than the current

785 instance and/or have a different domain and/or different

786 window.

787

788 Notes

789 -----

790 Conversion between domains and class types can result in

791 numerically ill defined series.

792

793 """

794 if kind is None:

795 kind = self.__class__

796 if domain is None:

797 domain = kind.domain

798 if window is None:

799 window = kind.window

800 return self(kind.identity(domain, window=window, symbol=self.symbol))

801

802 def mapparms(self):

803 """Return the mapping parameters.

804

805 The returned values define a linear map ``off + scl*x`` that is

806 applied to the input arguments before the series is evaluated. The

807 map depends on the ``domain`` and ``window``; if the current

808 ``domain`` is equal to the ``window`` the resulting map is the

809 identity. If the coefficients of the series instance are to be

810 used by themselves outside this class, then the linear function

811 must be substituted for the ``x`` in the standard representation of

812 the base polynomials.

813

814 Returns

815 -------

816 off, scl : float or complex

817 The mapping function is defined by ``off + scl*x``.

818

819 Notes

820 -----

821 If the current domain is the interval ``[l1, r1]`` and the window

822 is ``[l2, r2]``, then the linear mapping function ``L`` is

823 defined by the equations::

824

825 L(l1) = l2

826 L(r1) = r2

827

828 """

829 return pu.mapparms(self.domain, self.window)

830

831 def integ(self, m=1, k=[], lbnd=None):

832 """Integrate.

833

834 Return a series instance that is the definite integral of the

835 current series.

836

837 Parameters

838 ----------

839 m : non-negative int

840 The number of integrations to perform.

841 k : array_like

842 Integration constants. The first constant is applied to the

843 first integration, the second to the second, and so on. The

844 list of values must less than or equal to `m` in length and any

845 missing values are set to zero.

846 lbnd : Scalar

847 The lower bound of the definite integral.

848

849 Returns

850 -------

851 new_series : series

852 A new series representing the integral. The domain is the same

853 as the domain of the integrated series.

854

855 """

856 off, scl = self.mapparms()

857 if lbnd is None:

858 lbnd = 0

859 else:

860 lbnd = off + scl*lbnd

861 coef = self._int(self.coef, m, k, lbnd, 1./scl)

862 return self.__class__(coef, self.domain, self.window, self.symbol)

863

864 def deriv(self, m=1):

865 """Differentiate.

866

867 Return a series instance of that is the derivative of the current

868 series.

869

870 Parameters

871 ----------

872 m : non-negative int

873 Find the derivative of order `m`.

874

875 Returns

876 -------

877 new_series : series

878 A new series representing the derivative. The domain is the same

879 as the domain of the differentiated series.

880

881 """

882 off, scl = self.mapparms()

883 coef = self._der(self.coef, m, scl)

884 return self.__class__(coef, self.domain, self.window, self.symbol)

885

886 def roots(self):

887 """Return the roots of the series polynomial.

888

889 Compute the roots for the series. Note that the accuracy of the

890 roots decrease the further outside the domain they lie.

891

892 Returns

893 -------

894 roots : ndarray

895 Array containing the roots of the series.

896

897 """

898 roots = self._roots(self.coef)

899 return pu.mapdomain(roots, self.window, self.domain)

900

901 def linspace(self, n=100, domain=None):

902 """Return x, y values at equally spaced points in domain.

903

904 Returns the x, y values at `n` linearly spaced points across the

905 domain. Here y is the value of the polynomial at the points x. By

906 default the domain is the same as that of the series instance.

907 This method is intended mostly as a plotting aid.

908

909 .. versionadded:: 1.5.0

910

911 Parameters

912 ----------

913 n : int, optional

914 Number of point pairs to return. The default value is 100.

915 domain : {None, array_like}, optional

916 If not None, the specified domain is used instead of that of

917 the calling instance. It should be of the form ``[beg,end]``.

918 The default is None which case the class domain is used.

919

920 Returns

921 -------

922 x, y : ndarray

923 x is equal to linspace(self.domain[0], self.domain[1], n) and

924 y is the series evaluated at element of x.

925

926 """

927 if domain is None:

928 domain = self.domain

929 x = np.linspace(domain[0], domain[1], n)

930 y = self(x)

931 return x, y

932

933 @classmethod

934 def fit(cls, x, y, deg, domain=None, rcond=None, full=False, w=None,

935 window=None, symbol='x'):

936 """Least squares fit to data.

937

938 Return a series instance that is the least squares fit to the data

939 `y` sampled at `x`. The domain of the returned instance can be

940 specified and this will often result in a superior fit with less

941 chance of ill conditioning.

942

943 Parameters

944 ----------

945 x : array_like, shape (M,)

946 x-coordinates of the M sample points ``(x[i], y[i])``.

947 y : array_like, shape (M,)

948 y-coordinates of the M sample points ``(x[i], y[i])``.

949 deg : int or 1-D array_like

950 Degree(s) of the fitting polynomials. If `deg` is a single integer

951 all terms up to and including the `deg`'th term are included in the

952 fit. For NumPy versions >= 1.11.0 a list of integers specifying the

953 degrees of the terms to include may be used instead.

954 domain : {None, [beg, end], []}, optional

955 Domain to use for the returned series. If ``None``,

956 then a minimal domain that covers the points `x` is chosen. If

957 ``[]`` the class domain is used. The default value was the

958 class domain in NumPy 1.4 and ``None`` in later versions.

959 The ``[]`` option was added in numpy 1.5.0.

960 rcond : float, optional

961 Relative condition number of the fit. Singular values smaller

962 than this relative to the largest singular value will be

963 ignored. The default value is len(x)*eps, where eps is the

964 relative precision of the float type, about 2e-16 in most

965 cases.

966 full : bool, optional

967 Switch determining nature of return value. When it is False

968 (the default) just the coefficients are returned, when True

969 diagnostic information from the singular value decomposition is

970 also returned.

971 w : array_like, shape (M,), optional

972 Weights. If not None, the weight ``w[i]`` applies to the unsquared

973 residual ``y[i] - y_hat[i]`` at ``x[i]``. Ideally the weights are

974 chosen so that the errors of the products ``w[i]*y[i]`` all have

975 the same variance. When using inverse-variance weighting, use

976 ``w[i] = 1/sigma(y[i])``. The default value is None.

977

978 .. versionadded:: 1.5.0

979 window : {[beg, end]}, optional

980 Window to use for the returned series. The default

981 value is the default class domain

982

983 .. versionadded:: 1.6.0

984 symbol : str, optional

985 Symbol representing the independent variable. Default is 'x'.

986

987 Returns

988 -------

989 new_series : series

990 A series that represents the least squares fit to the data and

991 has the domain and window specified in the call. If the

992 coefficients for the unscaled and unshifted basis polynomials are

993 of interest, do ``new_series.convert().coef``.

994

995 [resid, rank, sv, rcond] : list

996 These values are only returned if ``full == True``

997

998 - resid -- sum of squared residuals of the least squares fit

999 - rank -- the numerical rank of the scaled Vandermonde matrix

1000 - sv -- singular values of the scaled Vandermonde matrix

1001 - rcond -- value of `rcond`.

1002

1003 For more details, see `linalg.lstsq`.

1004

1005 """

1006 if domain is None:

1007 domain = pu.getdomain(x)

1008 elif type(domain) is list and len(domain) == 0:

1009 domain = cls.domain

1010

1011 if window is None:

1012 window = cls.window

1013

1014 xnew = pu.mapdomain(x, domain, window)

1015 res = cls._fit(xnew, y, deg, w=w, rcond=rcond, full=full)

1016 if full:

1017 [coef, status] = res

1018 return (

1019 cls(coef, domain=domain, window=window, symbol=symbol), status

1020 )

1021 else:

1022 coef = res

1023 return cls(coef, domain=domain, window=window, symbol=symbol)

1024

1025 @classmethod

1026 def fromroots(cls, roots, domain=[], window=None, symbol='x'):

1027 """Return series instance that has the specified roots.

1028

1029 Returns a series representing the product

1030 ``(x - r[0])*(x - r[1])*...*(x - r[n-1])``, where ``r`` is a

1031 list of roots.

1032

1033 Parameters

1034 ----------

1035 roots : array_like

1036 List of roots.

1037 domain : {[], None, array_like}, optional

1038 Domain for the resulting series. If None the domain is the

1039 interval from the smallest root to the largest. If [] the

1040 domain is the class domain. The default is [].

1041 window : {None, array_like}, optional

1042 Window for the returned series. If None the class window is

1043 used. The default is None.

1044 symbol : str, optional

1045 Symbol representing the independent variable. Default is 'x'.

1046

1047 Returns

1048 -------

1049 new_series : series

1050 Series with the specified roots.

1051

1052 """

1053 [roots] = pu.as_series([roots], trim=False)

1054 if domain is None:

1055 domain = pu.getdomain(roots)

1056 elif type(domain) is list and len(domain) == 0:

1057 domain = cls.domain

1058

1059 if window is None:

1060 window = cls.window

1061

1062 deg = len(roots)

1063 off, scl = pu.mapparms(domain, window)

1064 rnew = off + scl*roots

1065 coef = cls._fromroots(rnew) / scl**deg

1066 return cls(coef, domain=domain, window=window, symbol=symbol)

1067

1068 @classmethod

1069 def identity(cls, domain=None, window=None, symbol='x'):

1070 """Identity function.

1071

1072 If ``p`` is the returned series, then ``p(x) == x`` for all

1073 values of x.

1074

1075 Parameters

1076 ----------

1077 domain : {None, array_like}, optional

1078 If given, the array must be of the form ``[beg, end]``, where

1079 ``beg`` and ``end`` are the endpoints of the domain. If None is

1080 given then the class domain is used. The default is None.

1081 window : {None, array_like}, optional

1082 If given, the resulting array must be if the form

1083 ``[beg, end]``, where ``beg`` and ``end`` are the endpoints of

1084 the window. If None is given then the class window is used. The

1085 default is None.

1086 symbol : str, optional

1087 Symbol representing the independent variable. Default is 'x'.

1088

1089 Returns

1090 -------

1091 new_series : series

1092 Series of representing the identity.

1093

1094 """

1095 if domain is None:

1096 domain = cls.domain

1097 if window is None:

1098 window = cls.window

1099 off, scl = pu.mapparms(window, domain)

1100 coef = cls._line(off, scl)

1101 return cls(coef, domain, window, symbol)

1102

1103 @classmethod

1104 def basis(cls, deg, domain=None, window=None, symbol='x'):

1105 """Series basis polynomial of degree `deg`.

1106

1107 Returns the series representing the basis polynomial of degree `deg`.

1108

1109 .. versionadded:: 1.7.0

1110

1111 Parameters

1112 ----------

1113 deg : int

1114 Degree of the basis polynomial for the series. Must be >= 0.

1115 domain : {None, array_like}, optional

1116 If given, the array must be of the form ``[beg, end]``, where

1117 ``beg`` and ``end`` are the endpoints of the domain. If None is

1118 given then the class domain is used. The default is None.

1119 window : {None, array_like}, optional

1120 If given, the resulting array must be if the form

1121 ``[beg, end]``, where ``beg`` and ``end`` are the endpoints of

1122 the window. If None is given then the class window is used. The

1123 default is None.

1124 symbol : str, optional

1125 Symbol representing the independent variable. Default is 'x'.

1126

1127 Returns

1128 -------

1129 new_series : series

1130 A series with the coefficient of the `deg` term set to one and

1131 all others zero.

1132

1133 """

1134 if domain is None:

1135 domain = cls.domain

1136 if window is None:

1137 window = cls.window

1138 ideg = int(deg)

1139

1140 if ideg != deg or ideg < 0:

1141 raise ValueError("deg must be non-negative integer")

1142 return cls([0]*ideg + [1], domain, window, symbol)

1143

1144 @classmethod

1145 def cast(cls, series, domain=None, window=None):

1146 """Convert series to series of this class.

1147

1148 The `series` is expected to be an instance of some polynomial

1149 series of one of the types supported by by the numpy.polynomial

1150 module, but could be some other class that supports the convert

1151 method.

1152

1153 .. versionadded:: 1.7.0

1154

1155 Parameters

1156 ----------

1157 series : series

1158 The series instance to be converted.

1159 domain : {None, array_like}, optional

1160 If given, the array must be of the form ``[beg, end]``, where

1161 ``beg`` and ``end`` are the endpoints of the domain. If None is

1162 given then the class domain is used. The default is None.

1163 window : {None, array_like}, optional

1164 If given, the resulting array must be if the form

1165 ``[beg, end]``, where ``beg`` and ``end`` are the endpoints of

1166 the window. If None is given then the class window is used. The

1167 default is None.

1168

1169 Returns

1170 -------

1171 new_series : series

1172 A series of the same kind as the calling class and equal to

1173 `series` when evaluated.

1174

1175 See Also

1176 --------

1177 convert : similar instance method

1178

1179 """

1180 if domain is None:

1181 domain = cls.domain

1182 if window is None:

1183 window = cls.window

1184 return series.convert(domain, cls, window)