codekingpro/portable-devtools
115k
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 abc
10import numbers
11import os
12from collections.abc import Callable
13
14import numpy as np
15
16from . import polyutils as pu
17
18__all__ = ['ABCPolyBase']
19
20class ABCPolyBase(abc.ABC):
21 """An abstract base class for immutable series classes.
22
23 ABCPolyBase provides the standard Python numerical methods
24 '+', '-', '*', '//', '%', 'divmod', '**', and '()' along with the
25 methods listed below.
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 Parameters
193 ----------
194 other : class instance
195 The other class must have the ``coef`` attribute.
196
197 Returns
198 -------
199 bool : boolean
200 True if the coefficients are the same, False otherwise.
201
202 """
203 return (
204 len(self.coef) == len(other.coef)
205 and np.all(self.coef == other.coef)
206 )
207
208 def has_samedomain(self, other):
209 """Check if domains match.
210
211 Parameters
212 ----------
213 other : class instance
214 The other class must have the ``domain`` attribute.
215
216 Returns
217 -------
218 bool : boolean
219 True if the domains are the same, False otherwise.
220
221 """
222 return np.all(self.domain == other.domain)
223
224 def has_samewindow(self, other):
225 """Check if windows match.
226
227 Parameters
228 ----------
229 other : class instance
230 The other class must have the ``window`` attribute.
231
232 Returns
233 -------
234 bool : boolean
235 True if the windows are the same, False otherwise.
236
237 """
238 return np.all(self.window == other.window)
239
240 def has_sametype(self, other):
241 """Check if types match.
242
243 Parameters
244 ----------
245 other : object
246 Class instance.
247
248 Returns
249 -------
250 bool : boolean
251 True if other is same class as self
252
253 """
254 return isinstance(other, self.__class__)
255
256 def _get_coefficients(self, other):
257 """Interpret other as polynomial coefficients.
258
259 The `other` argument is checked to see if it is of the same
260 class as self with identical domain and window. If so,
261 return its coefficients, otherwise return `other`.
262
263 Parameters
264 ----------
265 other : anything
266 Object to be checked.
267
268 Returns
269 -------
270 coef
271 The coefficients of`other` if it is a compatible instance,
272 of ABCPolyBase, otherwise `other`.
273
274 Raises
275 ------
276 TypeError
277 When `other` is an incompatible instance of ABCPolyBase.
278
279 """
280 if isinstance(other, ABCPolyBase):
281 if not isinstance(other, self.__class__):
282 raise TypeError("Polynomial types differ")
283 elif not np.all(self.domain == other.domain):
284 raise TypeError("Domains differ")
285 elif not np.all(self.window == other.window):
286 raise TypeError("Windows differ")
287 elif self.symbol != other.symbol:
288 raise ValueError("Polynomial symbols differ")
289 return other.coef
290 return other
291
292 def __init__(self, coef, domain=None, window=None, symbol='x'):
293 [coef] = pu.as_series([coef], trim=False)
294 self.coef = coef
295
296 if domain is not None:
297 [domain] = pu.as_series([domain], trim=False)
298 if len(domain) != 2:
299 raise ValueError("Domain has wrong number of elements.")
300 self.domain = domain
301
302 if window is not None:
303 [window] = pu.as_series([window], trim=False)
304 if len(window) != 2:
305 raise ValueError("Window has wrong number of elements.")
306 self.window = window
307
308 # Validation for symbol
309 try:
310 if not symbol.isidentifier():
311 raise ValueError(
312 "Symbol string must be a valid Python identifier"
313 )
314 # If a user passes in something other than a string, the above
315 # results in an AttributeError. Catch this and raise a more
316 # informative exception
317 except AttributeError:
318 raise TypeError("Symbol must be a non-empty string")
319
320 self._symbol = symbol
321
322 def __repr__(self):
323 coef = repr(self.coef)[6:-1]
324 domain = repr(self.domain)[6:-1]
325 window = repr(self.window)[6:-1]
326 name = self.__class__.__name__
327 return (f"{name}({coef}, domain={domain}, window={window}, "
328 f"symbol='{self.symbol}')")
329
330 def __format__(self, fmt_str):
331 if fmt_str == '':
332 return self.__str__()
333 if fmt_str not in ('ascii', 'unicode'):
334 raise ValueError(
335 f"Unsupported format string '{fmt_str}' passed to "
336 f"{self.__class__}.__format__. Valid options are "
337 f"'ascii' and 'unicode'"
338 )
339 if fmt_str == 'ascii':
340 return self._generate_string(self._str_term_ascii)
341 return self._generate_string(self._str_term_unicode)
342
343 def __str__(self):
344 if self._use_unicode:
345 return self._generate_string(self._str_term_unicode)
346 return self._generate_string(self._str_term_ascii)
347
348 def _generate_string(self, term_method):
349 """
350 Generate the full string representation of the polynomial, using
351 ``term_method`` to generate each polynomial term.
352 """
353 # Get configuration for line breaks
354 linewidth = np.get_printoptions().get('linewidth', 75)
355 if linewidth < 1:
356 linewidth = 1
357 out = pu.format_float(self.coef[0])
358
359 off, scale = self.mapparms()
360
361 scaled_symbol, needs_parens = self._format_term(pu.format_float,
362 off, scale)
363 if needs_parens:
364 scaled_symbol = '(' + scaled_symbol + ')'
365
366 for i, coef in enumerate(self.coef[1:]):
367 out += " "
368 power = str(i + 1)
369 # Polynomial coefficient
370 # The coefficient array can be an object array with elements that
371 # will raise a TypeError with >= 0 (e.g. strings or Python
372 # complex). In this case, represent the coefficient as-is.
373 try:
374 if coef >= 0:
375 next_term = "+ " + pu.format_float(coef, parens=True)
376 else:
377 next_term = "- " + pu.format_float(-coef, parens=True)
378 except TypeError:
379 next_term = f"+ {coef}"
380 # Polynomial term
381 next_term += term_method(power, scaled_symbol)
382 # Length of the current line with next term added
383 line_len = len(out.split('\n')[-1]) + len(next_term)
384 # If not the last term in the polynomial, it will be two
385 # characters longer due to the +/- with the next term
386 if i < len(self.coef[1:]) - 1:
387 line_len += 2
388 # Handle linebreaking
389 if line_len >= linewidth:
390 next_term = next_term.replace(" ", "\n", 1)
391 out += next_term
392 return out
393
394 @classmethod
395 def _str_term_unicode(cls, i, arg_str):
396 """
397 String representation of single polynomial term using unicode
398 characters for superscripts and subscripts.
399 """
400 if cls.basis_name is None:
401 raise NotImplementedError(
402 "Subclasses must define either a basis_name, or override "
403 "_str_term_unicode(cls, i, arg_str)"
404 )
405 return (f"·{cls.basis_name}{i.translate(cls._subscript_mapping)}"
406 f"({arg_str})")
407
408 @classmethod
409 def _str_term_ascii(cls, i, arg_str):
410 """
411 String representation of a single polynomial term using ** and _ to
412 represent superscripts and subscripts, respectively.
413 """
414 if cls.basis_name is None:
415 raise NotImplementedError(
416 "Subclasses must define either a basis_name, or override "
417 "_str_term_ascii(cls, i, arg_str)"
418 )
419 return f" {cls.basis_name}_{i}({arg_str})"
420
421 @classmethod
422 def _repr_latex_term(cls, i, arg_str, needs_parens):
423 if cls.basis_name is None:
424 raise NotImplementedError(
425 "Subclasses must define either a basis name, or override "
426 "_repr_latex_term(i, arg_str, needs_parens)")
427 # since we always add parens, we don't care if the expression needs them
428 return f"{{{cls.basis_name}}}_{{{i}}}({arg_str})"
429
430 @staticmethod
431 def _repr_latex_scalar(x, parens=False):
432 # TODO: we're stuck with disabling math formatting until we handle
433 # exponents in this function
434 return fr'\text{{{pu.format_float(x, parens=parens)}}}'
435
436 def _format_term(self, scalar_format: Callable, off: float, scale: float):
437 """ Format a single term in the expansion """
438 if off == 0 and scale == 1:
439 term = self.symbol
440 needs_parens = False
441 elif scale == 1:
442 term = f"{scalar_format(off)} + {self.symbol}"
443 needs_parens = True
444 elif off == 0:
445 term = f"{scalar_format(scale)}{self.symbol}"
446 needs_parens = True
447 else:
448 term = (
449 f"{scalar_format(off)} + "
450 f"{scalar_format(scale)}{self.symbol}"
451 )
452 needs_parens = True
453 return term, needs_parens
454
455 def _repr_latex_(self):
456 # get the scaled argument string to the basis functions
457 off, scale = self.mapparms()
458 term, needs_parens = self._format_term(self._repr_latex_scalar,
459 off, scale)
460
461 mute = r"\color{{LightGray}}{{{}}}".format
462
463 parts = []
464 for i, c in enumerate(self.coef):
465 # prevent duplication of + and - signs
466 if i == 0:
467 coef_str = f"{self._repr_latex_scalar(c)}"
468 elif not isinstance(c, numbers.Real):
469 coef_str = f" + ({self._repr_latex_scalar(c)})"
470 elif c >= 0:
471 coef_str = f" + {self._repr_latex_scalar(c, parens=True)}"
472 else:
473 coef_str = f" - {self._repr_latex_scalar(-c, parens=True)}"
474
475 # produce the string for the term
476 term_str = self._repr_latex_term(i, term, needs_parens)
477 if term_str == '1':
478 part = coef_str
479 else:
480 part = rf"{coef_str}\,{term_str}"
481
482 if c == 0:
483 part = mute(part)
484
485 parts.append(part)
486
487 if parts:
488 body = ''.join(parts)
489 else:
490 # in case somehow there are no coefficients at all
491 body = '0'
492
493 return rf"${self.symbol} \mapsto {body}$"
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 arg = pu.mapdomain(arg, self.domain, self.window)
512 return self._val(arg, self.coef)
513
514 def __iter__(self):
515 return iter(self.coef)
516
517 def __len__(self):
518 return len(self.coef)
519
520 # Numeric properties.
521
522 def __neg__(self):
523 return self.__class__(
524 -self.coef, self.domain, self.window, self.symbol
525 )
526
527 def __pos__(self):
528 return self
529
530 def __add__(self, other):
531 othercoef = self._get_coefficients(other)
532 try:
533 coef = self._add(self.coef, othercoef)
534 except Exception:
535 return NotImplemented
536 return self.__class__(coef, self.domain, self.window, self.symbol)
537
538 def __sub__(self, other):
539 othercoef = self._get_coefficients(other)
540 try:
541 coef = self._sub(self.coef, othercoef)
542 except Exception:
543 return NotImplemented
544 return self.__class__(coef, self.domain, self.window, self.symbol)
545
546 def __mul__(self, other):
547 othercoef = self._get_coefficients(other)
548 try:
549 coef = self._mul(self.coef, othercoef)
550 except Exception:
551 return NotImplemented
552 return self.__class__(coef, self.domain, self.window, self.symbol)
553
554 def __truediv__(self, other):
555 # there is no true divide if the rhs is not a Number, although it
556 # could return the first n elements of an infinite series.
557 # It is hard to see where n would come from, though.
558 if not isinstance(other, numbers.Number) or isinstance(other, bool):
559 raise TypeError(
560 f"unsupported types for true division: "
561 f"'{type(self)}', '{type(other)}'"
562 )
563 return self.__floordiv__(other)
564
565 def __floordiv__(self, other):
566 res = self.__divmod__(other)
567 if res is NotImplemented:
568 return res
569 return res[0]
570
571 def __mod__(self, other):
572 res = self.__divmod__(other)
573 if res is NotImplemented:
574 return res
575 return res[1]
576
577 def __divmod__(self, other):
578 othercoef = self._get_coefficients(other)
579 try:
580 quo, rem = self._div(self.coef, othercoef)
581 except ZeroDivisionError:
582 raise
583 except Exception:
584 return NotImplemented
585 quo = self.__class__(quo, self.domain, self.window, self.symbol)
586 rem = self.__class__(rem, self.domain, self.window, self.symbol)
587 return quo, rem
588
589 def __pow__(self, other):
590 coef = self._pow(self.coef, other, maxpower=self.maxpower)
591 res = self.__class__(coef, self.domain, self.window, self.symbol)
592 return res
593
594 def __radd__(self, other):
595 try:
596 coef = self._add(other, self.coef)
597 except Exception:
598 return NotImplemented
599 return self.__class__(coef, self.domain, self.window, self.symbol)
600
601 def __rsub__(self, other):
602 try:
603 coef = self._sub(other, self.coef)
604 except Exception:
605 return NotImplemented
606 return self.__class__(coef, self.domain, self.window, self.symbol)
607
608 def __rmul__(self, other):
609 try:
610 coef = self._mul(other, self.coef)
611 except Exception:
612 return NotImplemented
613 return self.__class__(coef, self.domain, self.window, self.symbol)
614
615 def __rtruediv__(self, other):
616 # An instance of ABCPolyBase is not considered a
617 # Number.
618 return NotImplemented
619
620 def __rfloordiv__(self, other):
621 res = self.__rdivmod__(other)
622 if res is NotImplemented:
623 return res
624 return res[0]
625
626 def __rmod__(self, other):
627 res = self.__rdivmod__(other)
628 if res is NotImplemented:
629 return res
630 return res[1]
631
632 def __rdivmod__(self, other):
633 try:
634 quo, rem = self._div(other, self.coef)
635 except ZeroDivisionError:
636 raise
637 except Exception:
638 return NotImplemented
639 quo = self.__class__(quo, self.domain, self.window, self.symbol)
640 rem = self.__class__(rem, self.domain, self.window, self.symbol)
641 return quo, rem
642
643 def __eq__(self, other):
644 res = (isinstance(other, self.__class__) and
645 np.all(self.domain == other.domain) and
646 np.all(self.window == other.window) and
647 (self.coef.shape == other.coef.shape) and
648 np.all(self.coef == other.coef) and
649 (self.symbol == other.symbol))
650 return res
651
652 def __ne__(self, other):
653 return not self.__eq__(other)
654
655 #
656 # Extra methods.
657 #
658
659 def copy(self):
660 """Return a copy.
661
662 Returns
663 -------
664 new_series : series
665 Copy of self.
666
667 """
668 return self.__class__(self.coef, self.domain, self.window, self.symbol)
669
670 def degree(self):
671 """The degree of the series.
672
673 Returns
674 -------
675 degree : int
676 Degree of the series, one less than the number of coefficients.
677
678 Examples
679 --------
680
681 Create a polynomial object for ``1 + 7*x + 4*x**2``:
682
683 >>> np.polynomial.set_default_printstyle("unicode")
684 >>> poly = np.polynomial.Polynomial([1, 7, 4])
685 >>> print(poly)
686 1.0 + 7.0·x + 4.0·x²
687 >>> poly.degree()
688 2
689
690 Note that this method does not check for non-zero coefficients.
691 You must trim the polynomial to remove any trailing zeroes:
692
693 >>> poly = np.polynomial.Polynomial([1, 7, 0])
694 >>> print(poly)
695 1.0 + 7.0·x + 0.0·x²
696 >>> poly.degree()
697 2
698 >>> poly.trim().degree()
699 1
700
701 """
702 return len(self) - 1
703
704 def cutdeg(self, deg):
705 """Truncate series to the given degree.
706
707 Reduce the degree of the series to `deg` by discarding the
708 high order terms. If `deg` is greater than the current degree a
709 copy of the current series is returned. This can be useful in least
710 squares where the coefficients of the high degree terms may be very
711 small.
712
713 Parameters
714 ----------
715 deg : non-negative int
716 The series is reduced to degree `deg` by discarding the high
717 order terms. The value of `deg` must be a non-negative integer.
718
719 Returns
720 -------
721 new_series : series
722 New instance of series with reduced degree.
723
724 """
725 return self.truncate(deg + 1)
726
727 def trim(self, tol=0):
728 """Remove trailing coefficients
729
730 Remove trailing coefficients until a coefficient is reached whose
731 absolute value greater than `tol` or the beginning of the series is
732 reached. If all the coefficients would be removed the series is set
733 to ``[0]``. A new series instance is returned with the new
734 coefficients. The current instance remains unchanged.
735
736 Parameters
737 ----------
738 tol : non-negative number.
739 All trailing coefficients less than `tol` will be removed.
740
741 Returns
742 -------
743 new_series : series
744 New instance of series with trimmed coefficients.
745
746 """
747 coef = pu.trimcoef(self.coef, tol)
748 return self.__class__(coef, self.domain, self.window, self.symbol)
749
750 def truncate(self, size):
751 """Truncate series to length `size`.
752
753 Reduce the series to length `size` by discarding the high
754 degree terms. The value of `size` must be a positive integer. This
755 can be useful in least squares where the coefficients of the
756 high degree terms may be very small.
757
758 Parameters
759 ----------
760 size : positive int
761 The series is reduced to length `size` by discarding the high
762 degree terms. The value of `size` must be a positive integer.
763
764 Returns
765 -------
766 new_series : series
767 New instance of series with truncated coefficients.
768
769 """
770 isize = int(size)
771 if isize != size or isize < 1:
772 raise ValueError("size must be a positive integer")
773 if isize >= len(self.coef):
774 coef = self.coef
775 else:
776 coef = self.coef[:isize]
777 return self.__class__(coef, self.domain, self.window, self.symbol)
778
779 def convert(self, domain=None, kind=None, window=None):
780 """Convert series to a different kind and/or domain and/or window.
781
782 Parameters
783 ----------
784 domain : array_like, optional
785 The domain of the converted series. If the value is None,
786 the default domain of `kind` is used.
787 kind : class, optional
788 The polynomial series type class to which the current instance
789 should be converted. If kind is None, then the class of the
790 current instance is used.
791 window : array_like, optional
792 The window of the converted series. If the value is None,
793 the default window of `kind` is used.
794
795 Returns
796 -------
797 new_series : series
798 The returned class can be of different type than the current
799 instance and/or have a different domain and/or different
800 window.
801
802 Notes
803 -----
804 Conversion between domains and class types can result in
805 numerically ill defined series.
806
807 """
808 if kind is None:
809 kind = self.__class__
810 if domain is None:
811 domain = kind.domain
812 if window is None:
813 window = kind.window
814 return self(kind.identity(domain, window=window, symbol=self.symbol))
815
816 def mapparms(self):
817 """Return the mapping parameters.
818
819 The returned values define a linear map ``off + scl*x`` that is
820 applied to the input arguments before the series is evaluated. The
821 map depends on the ``domain`` and ``window``; if the current
822 ``domain`` is equal to the ``window`` the resulting map is the
823 identity. If the coefficients of the series instance are to be
824 used by themselves outside this class, then the linear function
825 must be substituted for the ``x`` in the standard representation of
826 the base polynomials.
827
828 Returns
829 -------
830 off, scl : float or complex
831 The mapping function is defined by ``off + scl*x``.
832
833 Notes
834 -----
835 If the current domain is the interval ``[l1, r1]`` and the window
836 is ``[l2, r2]``, then the linear mapping function ``L`` is
837 defined by the equations::
838
839 L(l1) = l2
840 L(r1) = r2
841
842 """
843 return pu.mapparms(self.domain, self.window)
844
845 def integ(self, m=1, k=[], lbnd=None):
846 """Integrate.
847
848 Return a series instance that is the definite integral of the
849 current series.
850
851 Parameters
852 ----------
853 m : non-negative int
854 The number of integrations to perform.
855 k : array_like
856 Integration constants. The first constant is applied to the
857 first integration, the second to the second, and so on. The
858 list of values must less than or equal to `m` in length and any
859 missing values are set to zero.
860 lbnd : Scalar
861 The lower bound of the definite integral.
862
863 Returns
864 -------
865 new_series : series
866 A new series representing the integral. The domain is the same
867 as the domain of the integrated series.
868
869 """
870 off, scl = self.mapparms()
871 if lbnd is None:
872 lbnd = 0
873 else:
874 lbnd = off + scl * lbnd
875 coef = self._int(self.coef, m, k, lbnd, 1. / scl)
876 return self.__class__(coef, self.domain, self.window, self.symbol)
877
878 def deriv(self, m=1):
879 """Differentiate.
880
881 Return a series instance of that is the derivative of the current
882 series.
883
884 Parameters
885 ----------
886 m : non-negative int
887 Find the derivative of order `m`.
888
889 Returns
890 -------
891 new_series : series
892 A new series representing the derivative. The domain is the same
893 as the domain of the differentiated series.
894
895 """
896 off, scl = self.mapparms()
897 coef = self._der(self.coef, m, scl)
898 return self.__class__(coef, self.domain, self.window, self.symbol)
899
900 def roots(self):
901 """Return the roots of the series polynomial.
902
903 Compute the roots for the series. Note that the accuracy of the
904 roots decreases the further outside the `domain` they lie.
905
906 Returns
907 -------
908 roots : ndarray
909 Array containing the roots of the series.
910
911 """
912 roots = self._roots(self.coef)
913 return pu.mapdomain(roots, self.window, self.domain)
914
915 def linspace(self, n=100, domain=None):
916 """Return x, y values at equally spaced points in domain.
917
918 Returns the x, y values at `n` linearly spaced points across the
919 domain. Here y is the value of the polynomial at the points x. By
920 default the domain is the same as that of the series instance.
921 This method is intended mostly as a plotting aid.
922
923 Parameters
924 ----------
925 n : int, optional
926 Number of point pairs to return. The default value is 100.
927 domain : {None, array_like}, optional
928 If not None, the specified domain is used instead of that of
929 the calling instance. It should be of the form ``[beg,end]``.
930 The default is None which case the class domain is used.
931
932 Returns
933 -------
934 x, y : ndarray
935 x is equal to linspace(self.domain[0], self.domain[1], n) and
936 y is the series evaluated at element of x.
937
938 """
939 if domain is None:
940 domain = self.domain
941 x = np.linspace(domain[0], domain[1], n)
942 y = self(x)
943 return x, y
944
945 @classmethod
946 def fit(cls, x, y, deg, domain=None, rcond=None, full=False, w=None,
947 window=None, symbol='x'):
948 """Least squares fit to data.
949
950 Return a series instance that is the least squares fit to the data
951 `y` sampled at `x`. The domain of the returned instance can be
952 specified and this will often result in a superior fit with less
953 chance of ill conditioning.
954
955 Parameters
956 ----------
957 x : array_like, shape (M,)
958 x-coordinates of the M sample points ``(x[i], y[i])``.
959 y : array_like, shape (M,)
960 y-coordinates of the M sample points ``(x[i], y[i])``.
961 deg : int or 1-D array_like
962 Degree(s) of the fitting polynomials. If `deg` is a single integer
963 all terms up to and including the `deg`'th term are included in the
964 fit. For NumPy versions >= 1.11.0 a list of integers specifying the
965 degrees of the terms to include may be used instead.
966 domain : {None, [beg, end], []}, optional
967 Domain to use for the returned series. If ``None``,
968 then a minimal domain that covers the points `x` is chosen. If
969 ``[]`` the class domain is used. The default value was the
970 class domain in NumPy 1.4 and ``None`` in later versions.
971 The ``[]`` option was added in numpy 1.5.0.
972 rcond : float, optional
973 Relative condition number of the fit. Singular values smaller
974 than this relative to the largest singular value will be
975 ignored. The default value is ``len(x)*eps``, where eps is the
976 relative precision of the float type, about 2e-16 in most
977 cases.
978 full : bool, optional
979 Switch determining nature of return value. When it is False
980 (the default) just the coefficients are returned, when True
981 diagnostic information from the singular value decomposition is
982 also returned.
983 w : array_like, shape (M,), optional
984 Weights. If not None, the weight ``w[i]`` applies to the unsquared
985 residual ``y[i] - y_hat[i]`` at ``x[i]``. Ideally the weights are
986 chosen so that the errors of the products ``w[i]*y[i]`` all have
987 the same variance. When using inverse-variance weighting, use
988 ``w[i] = 1/sigma(y[i])``. The default value is None.
989 window : {[beg, end]}, optional
990 Window to use for the returned series. The default
991 value is the default class domain
992 symbol : str, optional
993 Symbol representing the independent variable. Default is 'x'.
994
995 Returns
996 -------
997 new_series : series
998 A series that represents the least squares fit to the data and
999 has the domain and window specified in the call. If the
1000 coefficients for the unscaled and unshifted basis polynomials are
1001 of interest, do ``new_series.convert().coef``.
1002
1003 [resid, rank, sv, rcond] : list
1004 These values are only returned if ``full == True``
1005
1006 - resid -- sum of squared residuals of the least squares fit
1007 - rank -- the numerical rank of the scaled Vandermonde matrix
1008 - sv -- singular values of the scaled Vandermonde matrix
1009 - rcond -- value of `rcond`.
1010
1011 For more details, see `linalg.lstsq`.
1012
1013 """
1014 if domain is None:
1015 domain = pu.getdomain(x)
1016 if domain[0] == domain[1]:
1017 domain[0] -= 1
1018 domain[1] += 1
1019 elif isinstance(domain, list) and len(domain) == 0:
1020 domain = cls.domain
1021
1022 if window is None:
1023 window = cls.window
1024
1025 xnew = pu.mapdomain(x, domain, window)
1026 res = cls._fit(xnew, y, deg, w=w, rcond=rcond, full=full)
1027 if full:
1028 [coef, status] = res
1029 return (
1030 cls(coef, domain=domain, window=window, symbol=symbol), status
1031 )
1032 else:
1033 coef = res
1034 return cls(coef, domain=domain, window=window, symbol=symbol)
1035
1036 @classmethod
1037 def fromroots(cls, roots, domain=[], window=None, symbol='x'):
1038 """Return series instance that has the specified roots.
1039
1040 Returns a series representing the product
1041 ``(x - r[0])*(x - r[1])*...*(x - r[n-1])``, where ``r`` is a
1042 list of roots.
1043
1044 Parameters
1045 ----------
1046 roots : array_like
1047 List of roots.
1048 domain : {[], None, array_like}, optional
1049 Domain for the resulting series. If None the domain is the
1050 interval from the smallest root to the largest. If [] the
1051 domain is the class domain. The default is [].
1052 window : {None, array_like}, optional
1053 Window for the returned series. If None the class window is
1054 used. The default is None.
1055 symbol : str, optional
1056 Symbol representing the independent variable. Default is 'x'.
1057
1058 Returns
1059 -------
1060 new_series : series
1061 Series with the specified roots.
1062
1063 """
1064 [roots] = pu.as_series([roots], trim=False)
1065 if domain is None:
1066 domain = pu.getdomain(roots)
1067 elif isinstance(domain, list) and len(domain) == 0:
1068 domain = cls.domain
1069
1070 if window is None:
1071 window = cls.window
1072
1073 deg = len(roots)
1074 off, scl = pu.mapparms(domain, window)
1075 rnew = off + scl * roots
1076 coef = cls._fromroots(rnew) / scl**deg
1077 return cls(coef, domain=domain, window=window, symbol=symbol)
1078
1079 @classmethod
1080 def identity(cls, domain=None, window=None, symbol='x'):
1081 """Identity function.
1082
1083 If ``p`` is the returned series, then ``p(x) == x`` for all
1084 values of x.
1085
1086 Parameters
1087 ----------
1088 domain : {None, array_like}, optional
1089 If given, the array must be of the form ``[beg, end]``, where
1090 ``beg`` and ``end`` are the endpoints of the domain. If None is
1091 given then the class domain is used. The default is None.
1092 window : {None, array_like}, optional
1093 If given, the resulting array must be if the form
1094 ``[beg, end]``, where ``beg`` and ``end`` are the endpoints of
1095 the window. If None is given then the class window is used. The
1096 default is None.
1097 symbol : str, optional
1098 Symbol representing the independent variable. Default is 'x'.
1099
1100 Returns
1101 -------
1102 new_series : series
1103 Series of representing the identity.
1104
1105 """
1106 if domain is None:
1107 domain = cls.domain
1108 if window is None:
1109 window = cls.window
1110 off, scl = pu.mapparms(window, domain)
1111 coef = cls._line(off, scl)
1112 return cls(coef, domain, window, symbol)
1113
1114 @classmethod
1115 def basis(cls, deg, domain=None, window=None, symbol='x'):
1116 """Series basis polynomial of degree `deg`.
1117
1118 Returns the series representing the basis polynomial of degree `deg`.
1119
1120 Parameters
1121 ----------
1122 deg : int
1123 Degree of the basis polynomial for the series. Must be >= 0.
1124 domain : {None, array_like}, optional
1125 If given, the array must be of the form ``[beg, end]``, where
1126 ``beg`` and ``end`` are the endpoints of the domain. If None is
1127 given then the class domain is used. The default is None.
1128 window : {None, array_like}, optional
1129 If given, the resulting array must be if the form
1130 ``[beg, end]``, where ``beg`` and ``end`` are the endpoints of
1131 the window. If None is given then the class window is used. The
1132 default is None.
1133 symbol : str, optional
1134 Symbol representing the independent variable. Default is 'x'.
1135
1136 Returns
1137 -------
1138 new_series : series
1139 A series with the coefficient of the `deg` term set to one and
1140 all others zero.
1141
1142 """
1143 if domain is None:
1144 domain = cls.domain
1145 if window is None:
1146 window = cls.window
1147 ideg = int(deg)
1148
1149 if ideg != deg or ideg < 0:
1150 raise ValueError("deg must be non-negative integer")
1151 return cls([0] * ideg + [1], domain, window, symbol)
1152
1153 @classmethod
1154 def cast(cls, series, domain=None, window=None):
1155 """Convert series to series of this class.
1156
1157 The `series` is expected to be an instance of some polynomial
1158 series of one of the types supported by by the numpy.polynomial
1159 module, but could be some other class that supports the convert
1160 method.
1161
1162 Parameters
1163 ----------
1164 series : series
1165 The series instance to be converted.
1166 domain : {None, array_like}, optional
1167 If given, the array must be of the form ``[beg, end]``, where
1168 ``beg`` and ``end`` are the endpoints of the domain. If None is
1169 given then the class domain is used. The default is None.
1170 window : {None, array_like}, optional
1171 If given, the resulting array must be if the form
1172 ``[beg, end]``, where ``beg`` and ``end`` are the endpoints of
1173 the window. If None is given then the class window is used. The
1174 default is None.
1175
1176 Returns
1177 -------
1178 new_series : series
1179 A series of the same kind as the calling class and equal to
1180 `series` when evaluated.
1181
1182 See Also
1183 --------
1184 convert : similar instance method
1185
1186 """
1187 if domain is None:
1188 domain = cls.domain
1189 if window is None:
1190 window = cls.window
1191 return series.convert(domain, cls, window)
1192 