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 """
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)