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