1"""
2Utility classes and functions for the polynomial modules.
3
4This module provides: error and warning objects; a polynomial base class;
5and some routines used in both the `polynomial` and `chebyshev` modules.
6
7Warning objects
8---------------
9
10.. autosummary::
11 :toctree: generated/
12
13 RankWarning raised in least-squares fit for rank-deficient matrix.
14
15Functions
16---------
17
18.. autosummary::
19 :toctree: generated/
20
21 as_series convert list of array_likes into 1-D arrays of common type.
22 trimseq remove trailing zeros.
23 trimcoef remove small trailing coefficients.
24 getdomain return the domain appropriate for a given set of abscissae.
25 mapdomain maps points between domains.
26 mapparms parameters of the linear map between domains.
27
28"""
29import operator
30import functools
31import warnings
32
33import numpy as np
34
35from numpy.core.multiarray import dragon4_positional, dragon4_scientific
36from numpy.core.umath import absolute
37
38__all__ = [
39 'RankWarning', 'as_series', 'trimseq',
40 'trimcoef', 'getdomain', 'mapdomain', 'mapparms',
41 'format_float']
42
43#
44# Warnings and Exceptions
45#
46
47class RankWarning(UserWarning):
48 """Issued by chebfit when the design matrix is rank deficient."""
49 pass
50
51#
52# Helper functions to convert inputs to 1-D arrays
53#
54def trimseq(seq):
55 """Remove small Poly series coefficients.
56
57 Parameters
58 ----------
59 seq : sequence
60 Sequence of Poly series coefficients. This routine fails for
61 empty sequences.
62
63 Returns
64 -------
65 series : sequence
66 Subsequence with trailing zeros removed. If the resulting sequence
67 would be empty, return the first element. The returned sequence may
68 or may not be a view.
69
70 Notes
71 -----
72 Do not lose the type info if the sequence contains unknown objects.
73
74 """
75 if len(seq) == 0:
76 return seq
77 else:
78 for i in range(len(seq) - 1, -1, -1):
79 if seq[i] != 0:
80 break
81 return seq[:i+1]
82
83
84def as_series(alist, trim=True):
85 """
86 Return argument as a list of 1-d arrays.
87
88 The returned list contains array(s) of dtype double, complex double, or
89 object. A 1-d argument of shape ``(N,)`` is parsed into ``N`` arrays of
90 size one; a 2-d argument of shape ``(M,N)`` is parsed into ``M`` arrays
91 of size ``N`` (i.e., is "parsed by row"); and a higher dimensional array
92 raises a Value Error if it is not first reshaped into either a 1-d or 2-d
93 array.
94
95 Parameters
96 ----------
97 alist : array_like
98 A 1- or 2-d array_like
99 trim : boolean, optional
100 When True, trailing zeros are removed from the inputs.
101 When False, the inputs are passed through intact.
102
103 Returns
104 -------
105 [a1, a2,...] : list of 1-D arrays
106 A copy of the input data as a list of 1-d arrays.
107
108 Raises
109 ------
110 ValueError
111 Raised when `as_series` cannot convert its input to 1-d arrays, or at
112 least one of the resulting arrays is empty.
113
114 Examples
115 --------
116 >>> from numpy.polynomial import polyutils as pu
117 >>> a = np.arange(4)
118 >>> pu.as_series(a)
119 [array([0.]), array([1.]), array([2.]), array([3.])]
120 >>> b = np.arange(6).reshape((2,3))
121 >>> pu.as_series(b)
122 [array([0., 1., 2.]), array([3., 4., 5.])]
123
124 >>> pu.as_series((1, np.arange(3), np.arange(2, dtype=np.float16)))
125 [array([1.]), array([0., 1., 2.]), array([0., 1.])]
126
127 >>> pu.as_series([2, [1.1, 0.]])
128 [array([2.]), array([1.1])]
129
130 >>> pu.as_series([2, [1.1, 0.]], trim=False)
131 [array([2.]), array([1.1, 0. ])]
132
133 """
134 arrays = [np.array(a, ndmin=1, copy=False) for a in alist]
135 if min([a.size for a in arrays]) == 0:
136 raise ValueError("Coefficient array is empty")
137 if any(a.ndim != 1 for a in arrays):
138 raise ValueError("Coefficient array is not 1-d")
139 if trim:
140 arrays = [trimseq(a) for a in arrays]
141
142 if any(a.dtype == np.dtype(object) for a in arrays):
143 ret = []
144 for a in arrays:
145 if a.dtype != np.dtype(object):
146 tmp = np.empty(len(a), dtype=np.dtype(object))
147 tmp[:] = a[:]
148 ret.append(tmp)
149 else:
150 ret.append(a.copy())
151 else:
152 try:
153 dtype = np.common_type(*arrays)
154 except Exception as e:
155 raise ValueError("Coefficient arrays have no common type") from e
156 ret = [np.array(a, copy=True, dtype=dtype) for a in arrays]
157 return ret
158
159
160def trimcoef(c, tol=0):
161 """
162 Remove "small" "trailing" coefficients from a polynomial.
163
164 "Small" means "small in absolute value" and is controlled by the
165 parameter `tol`; "trailing" means highest order coefficient(s), e.g., in
166 ``[0, 1, 1, 0, 0]`` (which represents ``0 + x + x**2 + 0*x**3 + 0*x**4``)
167 both the 3-rd and 4-th order coefficients would be "trimmed."
168
169 Parameters
170 ----------
171 c : array_like
172 1-d array of coefficients, ordered from lowest order to highest.
173 tol : number, optional
174 Trailing (i.e., highest order) elements with absolute value less
175 than or equal to `tol` (default value is zero) are removed.
176
177 Returns
178 -------
179 trimmed : ndarray
180 1-d array with trailing zeros removed. If the resulting series
181 would be empty, a series containing a single zero is returned.
182
183 Raises
184 ------
185 ValueError
186 If `tol` < 0
187
188 See Also
189 --------
190 trimseq
191
192 Examples
193 --------
194 >>> from numpy.polynomial import polyutils as pu
195 >>> pu.trimcoef((0,0,3,0,5,0,0))
196 array([0., 0., 3., 0., 5.])
197 >>> pu.trimcoef((0,0,1e-3,0,1e-5,0,0),1e-3) # item == tol is trimmed
198 array([0.])
199 >>> i = complex(0,1) # works for complex
200 >>> pu.trimcoef((3e-4,1e-3*(1-i),5e-4,2e-5*(1+i)), 1e-3)
201 array([0.0003+0.j , 0.001 -0.001j])
202
203 """
204 if tol < 0:
205 raise ValueError("tol must be non-negative")
206
207 [c] = as_series([c])
208 [ind] = np.nonzero(np.abs(c) > tol)
209 if len(ind) == 0:
210 return c[:1]*0
211 else:
212 return c[:ind[-1] + 1].copy()
213
214def getdomain(x):
215 """
216 Return a domain suitable for given abscissae.
217
218 Find a domain suitable for a polynomial or Chebyshev series
219 defined at the values supplied.
220
221 Parameters
222 ----------
223 x : array_like
224 1-d array of abscissae whose domain will be determined.
225
226 Returns
227 -------
228 domain : ndarray
229 1-d array containing two values. If the inputs are complex, then
230 the two returned points are the lower left and upper right corners
231 of the smallest rectangle (aligned with the axes) in the complex
232 plane containing the points `x`. If the inputs are real, then the
233 two points are the ends of the smallest interval containing the
234 points `x`.
235
236 See Also
237 --------
238 mapparms, mapdomain
239
240 Examples
241 --------
242 >>> from numpy.polynomial import polyutils as pu
243 >>> points = np.arange(4)**2 - 5; points
244 array([-5, -4, -1, 4])
245 >>> pu.getdomain(points)
246 array([-5., 4.])
247 >>> c = np.exp(complex(0,1)*np.pi*np.arange(12)/6) # unit circle
248 >>> pu.getdomain(c)
249 array([-1.-1.j, 1.+1.j])
250
251 """
252 [x] = as_series([x], trim=False)
253 if x.dtype.char in np.typecodes['Complex']:
254 rmin, rmax = x.real.min(), x.real.max()
255 imin, imax = x.imag.min(), x.imag.max()
256 return np.array((complex(rmin, imin), complex(rmax, imax)))
257 else:
258 return np.array((x.min(), x.max()))
259
260def mapparms(old, new):
261 """
262 Linear map parameters between domains.
263
264 Return the parameters of the linear map ``offset + scale*x`` that maps
265 `old` to `new` such that ``old[i] -> new[i]``, ``i = 0, 1``.
266
267 Parameters
268 ----------
269 old, new : array_like
270 Domains. Each domain must (successfully) convert to a 1-d array
271 containing precisely two values.
272
273 Returns
274 -------
275 offset, scale : scalars
276 The map ``L(x) = offset + scale*x`` maps the first domain to the
277 second.
278
279 See Also
280 --------
281 getdomain, mapdomain
282
283 Notes
284 -----
285 Also works for complex numbers, and thus can be used to calculate the
286 parameters required to map any line in the complex plane to any other
287 line therein.
288
289 Examples
290 --------
291 >>> from numpy.polynomial import polyutils as pu
292 >>> pu.mapparms((-1,1),(-1,1))
293 (0.0, 1.0)
294 >>> pu.mapparms((1,-1),(-1,1))
295 (-0.0, -1.0)
296 >>> i = complex(0,1)
297 >>> pu.mapparms((-i,-1),(1,i))
298 ((1+1j), (1-0j))
299
300 """
301 oldlen = old[1] - old[0]
302 newlen = new[1] - new[0]
303 off = (old[1]*new[0] - old[0]*new[1])/oldlen
304 scl = newlen/oldlen
305 return off, scl
306
307def mapdomain(x, old, new):
308 """
309 Apply linear map to input points.
310
311 The linear map ``offset + scale*x`` that maps the domain `old` to
312 the domain `new` is applied to the points `x`.
313
314 Parameters
315 ----------
316 x : array_like
317 Points to be mapped. If `x` is a subtype of ndarray the subtype
318 will be preserved.
319 old, new : array_like
320 The two domains that determine the map. Each must (successfully)
321 convert to 1-d arrays containing precisely two values.
322
323 Returns
324 -------
325 x_out : ndarray
326 Array of points of the same shape as `x`, after application of the
327 linear map between the two domains.
328
329 See Also
330 --------
331 getdomain, mapparms
332
333 Notes
334 -----
335 Effectively, this implements:
336
337 .. math::
338 x\\_out = new[0] + m(x - old[0])
339
340 where
341
342 .. math::
343 m = \\frac{new[1]-new[0]}{old[1]-old[0]}
344
345 Examples
346 --------
347 >>> from numpy.polynomial import polyutils as pu
348 >>> old_domain = (-1,1)
349 >>> new_domain = (0,2*np.pi)
350 >>> x = np.linspace(-1,1,6); x
351 array([-1. , -0.6, -0.2, 0.2, 0.6, 1. ])
352 >>> x_out = pu.mapdomain(x, old_domain, new_domain); x_out
353 array([ 0. , 1.25663706, 2.51327412, 3.76991118, 5.02654825, # may vary
354 6.28318531])
355 >>> x - pu.mapdomain(x_out, new_domain, old_domain)
356 array([0., 0., 0., 0., 0., 0.])
357
358 Also works for complex numbers (and thus can be used to map any line in
359 the complex plane to any other line therein).
360
361 >>> i = complex(0,1)
362 >>> old = (-1 - i, 1 + i)
363 >>> new = (-1 + i, 1 - i)
364 >>> z = np.linspace(old[0], old[1], 6); z
365 array([-1. -1.j , -0.6-0.6j, -0.2-0.2j, 0.2+0.2j, 0.6+0.6j, 1. +1.j ])
366 >>> new_z = pu.mapdomain(z, old, new); new_z
367 array([-1.0+1.j , -0.6+0.6j, -0.2+0.2j, 0.2-0.2j, 0.6-0.6j, 1.0-1.j ]) # may vary
368
369 """
370 x = np.asanyarray(x)
371 off, scl = mapparms(old, new)
372 return off + scl*x
373
374
375def _nth_slice(i, ndim):
376 sl = [np.newaxis] * ndim
377 sl[i] = slice(None)
378 return tuple(sl)
379
380
381def _vander_nd(vander_fs, points, degrees):
382 r"""
383 A generalization of the Vandermonde matrix for N dimensions
384
385 The result is built by combining the results of 1d Vandermonde matrices,
386
387 .. math::
388 W[i_0, \ldots, i_M, j_0, \ldots, j_N] = \prod_{k=0}^N{V_k(x_k)[i_0, \ldots, i_M, j_k]}
389
390 where
391
392 .. math::
393 N &= \texttt{len(points)} = \texttt{len(degrees)} = \texttt{len(vander\_fs)} \\
394 M &= \texttt{points[k].ndim} \\
395 V_k &= \texttt{vander\_fs[k]} \\
396 x_k &= \texttt{points[k]} \\
397 0 \le j_k &\le \texttt{degrees[k]}
398
399 Expanding the one-dimensional :math:`V_k` functions gives:
400
401 .. math::
402 W[i_0, \ldots, i_M, j_0, \ldots, j_N] = \prod_{k=0}^N{B_{k, j_k}(x_k[i_0, \ldots, i_M])}
403
404 where :math:`B_{k,m}` is the m'th basis of the polynomial construction used along
405 dimension :math:`k`. For a regular polynomial, :math:`B_{k, m}(x) = P_m(x) = x^m`.
406
407 Parameters
408 ----------
409 vander_fs : Sequence[function(array_like, int) -> ndarray]
410 The 1d vander function to use for each axis, such as ``polyvander``
411 points : Sequence[array_like]
412 Arrays of point coordinates, all of the same shape. The dtypes
413 will be converted to either float64 or complex128 depending on
414 whether any of the elements are complex. Scalars are converted to
415 1-D arrays.
416 This must be the same length as `vander_fs`.
417 degrees : Sequence[int]
418 The maximum degree (inclusive) to use for each axis.
419 This must be the same length as `vander_fs`.
420
421 Returns
422 -------
423 vander_nd : ndarray
424 An array of shape ``points[0].shape + tuple(d + 1 for d in degrees)``.
425 """
426 n_dims = len(vander_fs)
427 if n_dims != len(points):
428 raise ValueError(
429 f"Expected {n_dims} dimensions of sample points, got {len(points)}")
430 if n_dims != len(degrees):
431 raise ValueError(
432 f"Expected {n_dims} dimensions of degrees, got {len(degrees)}")
433 if n_dims == 0:
434 raise ValueError("Unable to guess a dtype or shape when no points are given")
435
436 # convert to the same shape and type
437 points = tuple(np.array(tuple(points), copy=False) + 0.0)
438
439 # produce the vandermonde matrix for each dimension, placing the last
440 # axis of each in an independent trailing axis of the output
441 vander_arrays = (
442 vander_fs[i](points[i], degrees[i])[(...,) + _nth_slice(i, n_dims)]
443 for i in range(n_dims)
444 )
445
446 # we checked this wasn't empty already, so no `initial` needed
447 return functools.reduce(operator.mul, vander_arrays)
448
449
450def _vander_nd_flat(vander_fs, points, degrees):
451 """
452 Like `_vander_nd`, but flattens the last ``len(degrees)`` axes into a single axis
453
454 Used to implement the public ``<type>vander<n>d`` functions.
455 """
456 v = _vander_nd(vander_fs, points, degrees)
457 return v.reshape(v.shape[:-len(degrees)] + (-1,))
458
459
460def _fromroots(line_f, mul_f, roots):
461 """
462 Helper function used to implement the ``<type>fromroots`` functions.
463
464 Parameters
465 ----------
466 line_f : function(float, float) -> ndarray
467 The ``<type>line`` function, such as ``polyline``
468 mul_f : function(array_like, array_like) -> ndarray
469 The ``<type>mul`` function, such as ``polymul``
470 roots
471 See the ``<type>fromroots`` functions for more detail
472 """
473 if len(roots) == 0:
474 return np.ones(1)
475 else:
476 [roots] = as_series([roots], trim=False)
477 roots.sort()
478 p = [line_f(-r, 1) for r in roots]
479 n = len(p)
480 while n > 1:
481 m, r = divmod(n, 2)
482 tmp = [mul_f(p[i], p[i+m]) for i in range(m)]
483 if r:
484 tmp[0] = mul_f(tmp[0], p[-1])
485 p = tmp
486 n = m
487 return p[0]
488
489
490def _valnd(val_f, c, *args):
491 """
492 Helper function used to implement the ``<type>val<n>d`` functions.
493
494 Parameters
495 ----------
496 val_f : function(array_like, array_like, tensor: bool) -> array_like
497 The ``<type>val`` function, such as ``polyval``
498 c, args
499 See the ``<type>val<n>d`` functions for more detail
500 """
501 args = [np.asanyarray(a) for a in args]
502 shape0 = args[0].shape
503 if not all((a.shape == shape0 for a in args[1:])):
504 if len(args) == 3:
505 raise ValueError('x, y, z are incompatible')
506 elif len(args) == 2:
507 raise ValueError('x, y are incompatible')
508 else:
509 raise ValueError('ordinates are incompatible')
510 it = iter(args)
511 x0 = next(it)
512
513 # use tensor on only the first
514 c = val_f(x0, c)
515 for xi in it:
516 c = val_f(xi, c, tensor=False)
517 return c
518
519
520def _gridnd(val_f, c, *args):
521 """
522 Helper function used to implement the ``<type>grid<n>d`` functions.
523
524 Parameters
525 ----------
526 val_f : function(array_like, array_like, tensor: bool) -> array_like
527 The ``<type>val`` function, such as ``polyval``
528 c, args
529 See the ``<type>grid<n>d`` functions for more detail
530 """
531 for xi in args:
532 c = val_f(xi, c)
533 return c
534
535
536def _div(mul_f, c1, c2):
537 """
538 Helper function used to implement the ``<type>div`` functions.
539
540 Implementation uses repeated subtraction of c2 multiplied by the nth basis.
541 For some polynomial types, a more efficient approach may be possible.
542
543 Parameters
544 ----------
545 mul_f : function(array_like, array_like) -> array_like
546 The ``<type>mul`` function, such as ``polymul``
547 c1, c2
548 See the ``<type>div`` functions for more detail
549 """
550 # c1, c2 are trimmed copies
551 [c1, c2] = as_series([c1, c2])
552 if c2[-1] == 0:
553 raise ZeroDivisionError()
554
555 lc1 = len(c1)
556 lc2 = len(c2)
557 if lc1 < lc2:
558 return c1[:1]*0, c1
559 elif lc2 == 1:
560 return c1/c2[-1], c1[:1]*0
561 else:
562 quo = np.empty(lc1 - lc2 + 1, dtype=c1.dtype)
563 rem = c1
564 for i in range(lc1 - lc2, - 1, -1):
565 p = mul_f([0]*i + [1], c2)
566 q = rem[-1]/p[-1]
567 rem = rem[:-1] - q*p[:-1]
568 quo[i] = q
569 return quo, trimseq(rem)
570
571
572def _add(c1, c2):
573 """ Helper function used to implement the ``<type>add`` functions. """
574 # c1, c2 are trimmed copies
575 [c1, c2] = as_series([c1, c2])
576 if len(c1) > len(c2):
577 c1[:c2.size] += c2
578 ret = c1
579 else:
580 c2[:c1.size] += c1
581 ret = c2
582 return trimseq(ret)
583
584
585def _sub(c1, c2):
586 """ Helper function used to implement the ``<type>sub`` functions. """
587 # c1, c2 are trimmed copies
588 [c1, c2] = as_series([c1, c2])
589 if len(c1) > len(c2):
590 c1[:c2.size] -= c2
591 ret = c1
592 else:
593 c2 = -c2
594 c2[:c1.size] += c1
595 ret = c2
596 return trimseq(ret)
597
598
599def _fit(vander_f, x, y, deg, rcond=None, full=False, w=None):
600 """
601 Helper function used to implement the ``<type>fit`` functions.
602
603 Parameters
604 ----------
605 vander_f : function(array_like, int) -> ndarray
606 The 1d vander function, such as ``polyvander``
607 c1, c2
608 See the ``<type>fit`` functions for more detail
609 """
610 x = np.asarray(x) + 0.0
611 y = np.asarray(y) + 0.0
612 deg = np.asarray(deg)
613
614 # check arguments.
615 if deg.ndim > 1 or deg.dtype.kind not in 'iu' or deg.size == 0:
616 raise TypeError("deg must be an int or non-empty 1-D array of int")
617 if deg.min() < 0:
618 raise ValueError("expected deg >= 0")
619 if x.ndim != 1:
620 raise TypeError("expected 1D vector for x")
621 if x.size == 0:
622 raise TypeError("expected non-empty vector for x")
623 if y.ndim < 1 or y.ndim > 2:
624 raise TypeError("expected 1D or 2D array for y")
625 if len(x) != len(y):
626 raise TypeError("expected x and y to have same length")
627
628 if deg.ndim == 0:
629 lmax = deg
630 order = lmax + 1
631 van = vander_f(x, lmax)
632 else:
633 deg = np.sort(deg)
634 lmax = deg[-1]
635 order = len(deg)
636 van = vander_f(x, lmax)[:, deg]
637
638 # set up the least squares matrices in transposed form
639 lhs = van.T
640 rhs = y.T
641 if w is not None:
642 w = np.asarray(w) + 0.0
643 if w.ndim != 1:
644 raise TypeError("expected 1D vector for w")
645 if len(x) != len(w):
646 raise TypeError("expected x and w to have same length")
647 # apply weights. Don't use inplace operations as they
648 # can cause problems with NA.
649 lhs = lhs * w
650 rhs = rhs * w
651
652 # set rcond
653 if rcond is None:
654 rcond = len(x)*np.finfo(x.dtype).eps
655
656 # Determine the norms of the design matrix columns.
657 if issubclass(lhs.dtype.type, np.complexfloating):
658 scl = np.sqrt((np.square(lhs.real) + np.square(lhs.imag)).sum(1))
659 else:
660 scl = np.sqrt(np.square(lhs).sum(1))
661 scl[scl == 0] = 1
662
663 # Solve the least squares problem.
664 c, resids, rank, s = np.linalg.lstsq(lhs.T/scl, rhs.T, rcond)
665 c = (c.T/scl).T
666
667 # Expand c to include non-fitted coefficients which are set to zero
668 if deg.ndim > 0:
669 if c.ndim == 2:
670 cc = np.zeros((lmax+1, c.shape[1]), dtype=c.dtype)
671 else:
672 cc = np.zeros(lmax+1, dtype=c.dtype)
673 cc[deg] = c
674 c = cc
675
676 # warn on rank reduction
677 if rank != order and not full:
678 msg = "The fit may be poorly conditioned"
679 warnings.warn(msg, RankWarning, stacklevel=2)
680
681 if full:
682 return c, [resids, rank, s, rcond]
683 else:
684 return c
685
686
687def _pow(mul_f, c, pow, maxpower):
688 """
689 Helper function used to implement the ``<type>pow`` functions.
690
691 Parameters
692 ----------
693 mul_f : function(array_like, array_like) -> ndarray
694 The ``<type>mul`` function, such as ``polymul``
695 c : array_like
696 1-D array of array of series coefficients
697 pow, maxpower
698 See the ``<type>pow`` functions for more detail
699 """
700 # c is a trimmed copy
701 [c] = as_series([c])
702 power = int(pow)
703 if power != pow or power < 0:
704 raise ValueError("Power must be a non-negative integer.")
705 elif maxpower is not None and power > maxpower:
706 raise ValueError("Power is too large")
707 elif power == 0:
708 return np.array([1], dtype=c.dtype)
709 elif power == 1:
710 return c
711 else:
712 # This can be made more efficient by using powers of two
713 # in the usual way.
714 prd = c
715 for i in range(2, power + 1):
716 prd = mul_f(prd, c)
717 return prd
718
719
720def _deprecate_as_int(x, desc):
721 """
722 Like `operator.index`, but emits a deprecation warning when passed a float
723
724 Parameters
725 ----------
726 x : int-like, or float with integral value
727 Value to interpret as an integer
728 desc : str
729 description to include in any error message
730
731 Raises
732 ------
733 TypeError : if x is a non-integral float or non-numeric
734 DeprecationWarning : if x is an integral float
735 """
736 try:
737 return operator.index(x)
738 except TypeError as e:
739 # Numpy 1.17.0, 2019-03-11
740 try:
741 ix = int(x)
742 except TypeError:
743 pass
744 else:
745 if ix == x:
746 warnings.warn(
747 f"In future, this will raise TypeError, as {desc} will "
748 "need to be an integer not just an integral float.",
749 DeprecationWarning,
750 stacklevel=3
751 )
752 return ix
753
754 raise TypeError(f"{desc} must be an integer") from e
755
756
757def format_float(x, parens=False):
758 if not np.issubdtype(type(x), np.floating):
759 return str(x)
760
761 opts = np.get_printoptions()
762
763 if np.isnan(x):
764 return opts['nanstr']
765 elif np.isinf(x):
766 return opts['infstr']
767
768 exp_format = False
769 if x != 0:
770 a = absolute(x)
771 if a >= 1.e8 or a < 10**min(0, -(opts['precision']-1)//2):
772 exp_format = True
773
774 trim, unique = '0', True
775 if opts['floatmode'] == 'fixed':
776 trim, unique = 'k', False
777
778 if exp_format:
779 s = dragon4_scientific(x, precision=opts['precision'],
780 unique=unique, trim=trim,
781 sign=opts['sign'] == '+')
782 if parens:
783 s = '(' + s + ')'
784 else:
785 s = dragon4_positional(x, precision=opts['precision'],
786 fractional=True,
787 unique=unique, trim=trim,
788 sign=opts['sign'] == '+')
789 return s