1"""
2Abstract base class for the various polynomial Classes.
3
4The ABCPolyBase class provides the methods needed to implement the common API
5for the various polynomial classes. It operates as a mixin, but uses the
6abc module from the stdlib, hence it is only available for Python >= 2.6.
7
8"""
9import os
10import abc
11import numbers
12
13import numpy as np
14from . import polyutils as pu
15
16__all__ = ['ABCPolyBase']
17
18class 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 Examples
686 --------
687
688 Create a polynomial object for ``1 + 7*x + 4*x**2``:
689
690 >>> poly = np.polynomial.Polynomial([1, 7, 4])
691 >>> print(poly)
692 1.0 + 7.0·x + 4.0·x²
693 >>> poly.degree()
694 2
695
696 Note that this method does not check for non-zero coefficients.
697 You must trim the polynomial to remove any trailing zeroes:
698
699 >>> poly = np.polynomial.Polynomial([1, 7, 0])
700 >>> print(poly)
701 1.0 + 7.0·x + 0.0·x²
702 >>> poly.degree()
703 2
704 >>> poly.trim().degree()
705 1
706
707 """
708 return len(self) - 1
709
710 def cutdeg(self, deg):
711 """Truncate series to the given degree.
712
713 Reduce the degree of the series to `deg` by discarding the
714 high order terms. If `deg` is greater than the current degree a
715 copy of the current series is returned. This can be useful in least
716 squares where the coefficients of the high degree terms may be very
717 small.
718
719 .. versionadded:: 1.5.0
720
721 Parameters
722 ----------
723 deg : non-negative int
724 The series is reduced to degree `deg` by discarding the high
725 order terms. The value of `deg` must be a non-negative integer.
726
727 Returns
728 -------
729 new_series : series
730 New instance of series with reduced degree.
731
732 """
733 return self.truncate(deg + 1)
734
735 def trim(self, tol=0):
736 """Remove trailing coefficients
737
738 Remove trailing coefficients until a coefficient is reached whose
739 absolute value greater than `tol` or the beginning of the series is
740 reached. If all the coefficients would be removed the series is set
741 to ``[0]``. A new series instance is returned with the new
742 coefficients. The current instance remains unchanged.
743
744 Parameters
745 ----------
746 tol : non-negative number.
747 All trailing coefficients less than `tol` will be removed.
748
749 Returns
750 -------
751 new_series : series
752 New instance of series with trimmed coefficients.
753
754 """
755 coef = pu.trimcoef(self.coef, tol)
756 return self.__class__(coef, self.domain, self.window, self.symbol)
757
758 def truncate(self, size):
759 """Truncate series to length `size`.
760
761 Reduce the series to length `size` by discarding the high
762 degree terms. The value of `size` must be a positive integer. This
763 can be useful in least squares where the coefficients of the
764 high degree terms may be very small.
765
766 Parameters
767 ----------
768 size : positive int
769 The series is reduced to length `size` by discarding the high
770 degree terms. The value of `size` must be a positive integer.
771
772 Returns
773 -------
774 new_series : series
775 New instance of series with truncated coefficients.
776
777 """
778 isize = int(size)
779 if isize != size or isize < 1:
780 raise ValueError("size must be a positive integer")
781 if isize >= len(self.coef):
782 coef = self.coef
783 else:
784 coef = self.coef[:isize]
785 return self.__class__(coef, self.domain, self.window, self.symbol)
786
787 def convert(self, domain=None, kind=None, window=None):
788 """Convert series to a different kind and/or domain and/or window.
789
790 Parameters
791 ----------
792 domain : array_like, optional
793 The domain of the converted series. If the value is None,
794 the default domain of `kind` is used.
795 kind : class, optional
796 The polynomial series type class to which the current instance
797 should be converted. If kind is None, then the class of the
798 current instance is used.
799 window : array_like, optional
800 The window of the converted series. If the value is None,
801 the default window of `kind` is used.
802
803 Returns
804 -------
805 new_series : series
806 The returned class can be of different type than the current
807 instance and/or have a different domain and/or different
808 window.
809
810 Notes
811 -----
812 Conversion between domains and class types can result in
813 numerically ill defined series.
814
815 """
816 if kind is None:
817 kind = self.__class__
818 if domain is None:
819 domain = kind.domain
820 if window is None:
821 window = kind.window
822 return self(kind.identity(domain, window=window, symbol=self.symbol))
823
824 def mapparms(self):
825 """Return the mapping parameters.
826
827 The returned values define a linear map ``off + scl*x`` that is
828 applied to the input arguments before the series is evaluated. The
829 map depends on the ``domain`` and ``window``; if the current
830 ``domain`` is equal to the ``window`` the resulting map is the
831 identity. If the coefficients of the series instance are to be
832 used by themselves outside this class, then the linear function
833 must be substituted for the ``x`` in the standard representation of
834 the base polynomials.
835
836 Returns
837 -------
838 off, scl : float or complex
839 The mapping function is defined by ``off + scl*x``.
840
841 Notes
842 -----
843 If the current domain is the interval ``[l1, r1]`` and the window
844 is ``[l2, r2]``, then the linear mapping function ``L`` is
845 defined by the equations::
846
847 L(l1) = l2
848 L(r1) = r2
849
850 """
851 return pu.mapparms(self.domain, self.window)
852
853 def integ(self, m=1, k=[], lbnd=None):
854 """Integrate.
855
856 Return a series instance that is the definite integral of the
857 current series.
858
859 Parameters
860 ----------
861 m : non-negative int
862 The number of integrations to perform.
863 k : array_like
864 Integration constants. The first constant is applied to the
865 first integration, the second to the second, and so on. The
866 list of values must less than or equal to `m` in length and any
867 missing values are set to zero.
868 lbnd : Scalar
869 The lower bound of the definite integral.
870
871 Returns
872 -------
873 new_series : series
874 A new series representing the integral. The domain is the same
875 as the domain of the integrated series.
876
877 """
878 off, scl = self.mapparms()
879 if lbnd is None:
880 lbnd = 0
881 else:
882 lbnd = off + scl*lbnd
883 coef = self._int(self.coef, m, k, lbnd, 1./scl)
884 return self.__class__(coef, self.domain, self.window, self.symbol)
885
886 def deriv(self, m=1):
887 """Differentiate.
888
889 Return a series instance of that is the derivative of the current
890 series.
891
892 Parameters
893 ----------
894 m : non-negative int
895 Find the derivative of order `m`.
896
897 Returns
898 -------
899 new_series : series
900 A new series representing the derivative. The domain is the same
901 as the domain of the differentiated series.
902
903 """
904 off, scl = self.mapparms()
905 coef = self._der(self.coef, m, scl)
906 return self.__class__(coef, self.domain, self.window, self.symbol)
907
908 def roots(self):
909 """Return the roots of the series polynomial.
910
911 Compute the roots for the series. Note that the accuracy of the
912 roots decreases the further outside the `domain` they lie.
913
914 Returns
915 -------
916 roots : ndarray
917 Array containing the roots of the series.
918
919 """
920 roots = self._roots(self.coef)
921 return pu.mapdomain(roots, self.window, self.domain)
922
923 def linspace(self, n=100, domain=None):
924 """Return x, y values at equally spaced points in domain.
925
926 Returns the x, y values at `n` linearly spaced points across the
927 domain. Here y is the value of the polynomial at the points x. By
928 default the domain is the same as that of the series instance.
929 This method is intended mostly as a plotting aid.
930
931 .. versionadded:: 1.5.0
932
933 Parameters
934 ----------
935 n : int, optional
936 Number of point pairs to return. The default value is 100.
937 domain : {None, array_like}, optional
938 If not None, the specified domain is used instead of that of
939 the calling instance. It should be of the form ``[beg,end]``.
940 The default is None which case the class domain is used.
941
942 Returns
943 -------
944 x, y : ndarray
945 x is equal to linspace(self.domain[0], self.domain[1], n) and
946 y is the series evaluated at element of x.
947
948 """
949 if domain is None:
950 domain = self.domain
951 x = np.linspace(domain[0], domain[1], n)
952 y = self(x)
953 return x, y
954
955 @classmethod
956 def fit(cls, x, y, deg, domain=None, rcond=None, full=False, w=None,
957 window=None, symbol='x'):
958 """Least squares fit to data.
959
960 Return a series instance that is the least squares fit to the data
961 `y` sampled at `x`. The domain of the returned instance can be
962 specified and this will often result in a superior fit with less
963 chance of ill conditioning.
964
965 Parameters
966 ----------
967 x : array_like, shape (M,)
968 x-coordinates of the M sample points ``(x[i], y[i])``.
969 y : array_like, shape (M,)
970 y-coordinates of the M sample points ``(x[i], y[i])``.
971 deg : int or 1-D array_like
972 Degree(s) of the fitting polynomials. If `deg` is a single integer
973 all terms up to and including the `deg`'th term are included in the
974 fit. For NumPy versions >= 1.11.0 a list of integers specifying the
975 degrees of the terms to include may be used instead.
976 domain : {None, [beg, end], []}, optional
977 Domain to use for the returned series. If ``None``,
978 then a minimal domain that covers the points `x` is chosen. If
979 ``[]`` the class domain is used. The default value was the
980 class domain in NumPy 1.4 and ``None`` in later versions.
981 The ``[]`` option was added in numpy 1.5.0.
982 rcond : float, optional
983 Relative condition number of the fit. Singular values smaller
984 than this relative to the largest singular value will be
985 ignored. The default value is len(x)*eps, where eps is the
986 relative precision of the float type, about 2e-16 in most
987 cases.
988 full : bool, optional
989 Switch determining nature of return value. When it is False
990 (the default) just the coefficients are returned, when True
991 diagnostic information from the singular value decomposition is
992 also returned.
993 w : array_like, shape (M,), optional
994 Weights. If not None, the weight ``w[i]`` applies to the unsquared
995 residual ``y[i] - y_hat[i]`` at ``x[i]``. Ideally the weights are
996 chosen so that the errors of the products ``w[i]*y[i]`` all have
997 the same variance. When using inverse-variance weighting, use
998 ``w[i] = 1/sigma(y[i])``. The default value is None.
999
1000 .. versionadded:: 1.5.0
1001 window : {[beg, end]}, optional
1002 Window to use for the returned series. The default
1003 value is the default class domain
1004
1005 .. versionadded:: 1.6.0
1006 symbol : str, optional
1007 Symbol representing the independent variable. Default is 'x'.
1008
1009 Returns
1010 -------
1011 new_series : series
1012 A series that represents the least squares fit to the data and
1013 has the domain and window specified in the call. If the
1014 coefficients for the unscaled and unshifted basis polynomials are
1015 of interest, do ``new_series.convert().coef``.
1016
1017 [resid, rank, sv, rcond] : list
1018 These values are only returned if ``full == True``
1019
1020 - resid -- sum of squared residuals of the least squares fit
1021 - rank -- the numerical rank of the scaled Vandermonde matrix
1022 - sv -- singular values of the scaled Vandermonde matrix
1023 - rcond -- value of `rcond`.
1024
1025 For more details, see `linalg.lstsq`.
1026
1027 """
1028 if domain is None:
1029 domain = pu.getdomain(x)
1030 elif type(domain) is list and len(domain) == 0:
1031 domain = cls.domain
1032
1033 if window is None:
1034 window = cls.window
1035
1036 xnew = pu.mapdomain(x, domain, window)
1037 res = cls._fit(xnew, y, deg, w=w, rcond=rcond, full=full)
1038 if full:
1039 [coef, status] = res
1040 return (
1041 cls(coef, domain=domain, window=window, symbol=symbol), status
1042 )
1043 else:
1044 coef = res
1045 return cls(coef, domain=domain, window=window, symbol=symbol)
1046
1047 @classmethod
1048 def fromroots(cls, roots, domain=[], window=None, symbol='x'):
1049 """Return series instance that has the specified roots.
1050
1051 Returns a series representing the product
1052 ``(x - r[0])*(x - r[1])*...*(x - r[n-1])``, where ``r`` is a
1053 list of roots.
1054
1055 Parameters
1056 ----------
1057 roots : array_like
1058 List of roots.
1059 domain : {[], None, array_like}, optional
1060 Domain for the resulting series. If None the domain is the
1061 interval from the smallest root to the largest. If [] the
1062 domain is the class domain. The default is [].
1063 window : {None, array_like}, optional
1064 Window for the returned series. If None the class window is
1065 used. The default is None.
1066 symbol : str, optional
1067 Symbol representing the independent variable. Default is 'x'.
1068
1069 Returns
1070 -------
1071 new_series : series
1072 Series with the specified roots.
1073
1074 """
1075 [roots] = pu.as_series([roots], trim=False)
1076 if domain is None:
1077 domain = pu.getdomain(roots)
1078 elif type(domain) is list and len(domain) == 0:
1079 domain = cls.domain
1080
1081 if window is None:
1082 window = cls.window
1083
1084 deg = len(roots)
1085 off, scl = pu.mapparms(domain, window)
1086 rnew = off + scl*roots
1087 coef = cls._fromroots(rnew) / scl**deg
1088 return cls(coef, domain=domain, window=window, symbol=symbol)
1089
1090 @classmethod
1091 def identity(cls, domain=None, window=None, symbol='x'):
1092 """Identity function.
1093
1094 If ``p`` is the returned series, then ``p(x) == x`` for all
1095 values of x.
1096
1097 Parameters
1098 ----------
1099 domain : {None, array_like}, optional
1100 If given, the array must be of the form ``[beg, end]``, where
1101 ``beg`` and ``end`` are the endpoints of the domain. If None is
1102 given then the class domain is used. The default is None.
1103 window : {None, array_like}, optional
1104 If given, the resulting array must be if the form
1105 ``[beg, end]``, where ``beg`` and ``end`` are the endpoints of
1106 the window. If None is given then the class window is used. The
1107 default is None.
1108 symbol : str, optional
1109 Symbol representing the independent variable. Default is 'x'.
1110
1111 Returns
1112 -------
1113 new_series : series
1114 Series of representing the identity.
1115
1116 """
1117 if domain is None:
1118 domain = cls.domain
1119 if window is None:
1120 window = cls.window
1121 off, scl = pu.mapparms(window, domain)
1122 coef = cls._line(off, scl)
1123 return cls(coef, domain, window, symbol)
1124
1125 @classmethod
1126 def basis(cls, deg, domain=None, window=None, symbol='x'):
1127 """Series basis polynomial of degree `deg`.
1128
1129 Returns the series representing the basis polynomial of degree `deg`.
1130
1131 .. versionadded:: 1.7.0
1132
1133 Parameters
1134 ----------
1135 deg : int
1136 Degree of the basis polynomial for the series. Must be >= 0.
1137 domain : {None, array_like}, optional
1138 If given, the array must be of the form ``[beg, end]``, where
1139 ``beg`` and ``end`` are the endpoints of the domain. If None is
1140 given then the class domain is used. The default is None.
1141 window : {None, array_like}, optional
1142 If given, the resulting array must be if the form
1143 ``[beg, end]``, where ``beg`` and ``end`` are the endpoints of
1144 the window. If None is given then the class window is used. The
1145 default is None.
1146 symbol : str, optional
1147 Symbol representing the independent variable. Default is 'x'.
1148
1149 Returns
1150 -------
1151 new_series : series
1152 A series with the coefficient of the `deg` term set to one and
1153 all others zero.
1154
1155 """
1156 if domain is None:
1157 domain = cls.domain
1158 if window is None:
1159 window = cls.window
1160 ideg = int(deg)
1161
1162 if ideg != deg or ideg < 0:
1163 raise ValueError("deg must be non-negative integer")
1164 return cls([0]*ideg + [1], domain, window, symbol)
1165
1166 @classmethod
1167 def cast(cls, series, domain=None, window=None):
1168 """Convert series to series of this class.
1169
1170 The `series` is expected to be an instance of some polynomial
1171 series of one of the types supported by by the numpy.polynomial
1172 module, but could be some other class that supports the convert
1173 method.
1174
1175 .. versionadded:: 1.7.0
1176
1177 Parameters
1178 ----------
1179 series : series
1180 The series instance to be converted.
1181 domain : {None, array_like}, optional
1182 If given, the array must be of the form ``[beg, end]``, where
1183 ``beg`` and ``end`` are the endpoints of the domain. If None is
1184 given then the class domain is used. The default is None.
1185 window : {None, array_like}, optional
1186 If given, the resulting array must be if the form
1187 ``[beg, end]``, where ``beg`` and ``end`` are the endpoints of
1188 the window. If None is given then the class window is used. The
1189 default is None.
1190
1191 Returns
1192 -------
1193 new_series : series
1194 A series of the same kind as the calling class and equal to
1195 `series` when evaluated.
1196
1197 See Also
1198 --------
1199 convert : similar instance method
1200
1201 """
1202 if domain is None:
1203 domain = cls.domain
1204 if window is None:
1205 window = cls.window
1206 return series.convert(domain, cls, window)