8000 ENH: Add `polyvalfromroots` to numpy.polynomial by e-q · Pull Request #7542 · numpy/numpy · GitHub
[go: up one dir, main page]

Skip to content

ENH: Add polyvalfromroots to numpy.polynomial #7542

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed

ENH: Add polyvalfromroots to numpy.polynomial #7542

wants to merge 1 commit into from

Conversation

e-q
Copy link
Contributor
@e-q e-q commented Apr 12, 2016

As one can easily encounter when working with high-order signal processing
filters, converting a high-order polynomial from its roots to its polynomial
coefficients can be quite lossy, leading to inaccuracies in the filter's
properties.

I'm proposing a new function, polyrootval - based on polyval - that
evaluates a polynomial given a list of its roots. The benefit of calculating it
this way can be seen at scipy Issue #6059. Some tests are included, as well.

There are a few things where I'm not 100% sure what the best choice is, though

  • The return value for an empty list of roots is zeros_like(x); maybe there's another value that makes more sense, or even an error?
  • How can one specify -x**n? roots=[0]*n can only give you x**n.

@charris
Copy link
Member
charris commented Apr 13, 2016

Couple of questions.

  • What degree are you looking to handle
  • Where do the roots come from
  • Is it important to evaluate the monic polynomial

Apart from that, might look at adapting the way the polynomial from roots is constructed, it tries to group and multiply the terms in a numerically better way. Whether that matters or not for numerical evaluation, I don't know. Note that high degree polynomials grow really fast outside of the set of zeros.

@e-q
Copy link
Contributor Author
e-q commented Apr 13, 2016

Degrees of a few dozen isn't terribly uncommon in use cases I've come across.

Many signal processing filters that are "ideal" in some way or another are specified as as the complex poles and zeros of a rational function on the complex plane (e.g. Chebyshev / Elliptic filters), in so called "zeros, poles, gain" format. Thus, evaluating the frequency response of the filter comes down to evaluating the polynomials in the numerator and denominator. (Currently scipy.signal does this by converting the roots to polynomial coefficients with np.lib.poly) Generally, these are monic polynomials, any overall scale is factored out into the "gain" parameter.

I'm not sure that the manner of conversion from roots to coefficients is the issue; rather, at higher orders, I think that any conversion is poorly conditioned, such that errors in the coefficients at machine precision levels can lead to a substantial error in the location of the roots, and thereby from the intended numerical values.

@charris
Copy link
Member
charris commented Apr 13, 2016

I was suggesting doing the same conversion as for creating the polynomial but using the actual roots rather than x - r. I suspect it doesn't make a difference grouping the multiplications like that, but was curious. Anyway, I think this is a reasonable function to add. Another possibility for the name would be polyvalfromroots, which would be consistant with polyfromroots.

EDIT: actual x values rather than symbolic x - r

@charris
Copy link
Member
charris commented Apr 13, 2016

BTW, with centering and scaling you might be able to get a useful polynomial up to about degree 50. I experimented a bit a while back and got up about degree sixty with equally spaced roots on a line. Probably sensitive to root location though.

@charris
Copy link
Member
charris commented Apr 13, 2016

Should also add something to the 1.12 release notes and doc/source/reference/routines.polynomials.polynomial.rst.

@e-q
Copy link
Contributor Author
e-q commented Apr 15, 2016

Thanks for the feedback! I've added the requisite edits.

I think part of what makes this tough for things like sharp bandpass filters is that the root locations can span multiple orders of magnitude.

over the columns of `r` for the evaluation. This keyword is useful
when `r` is multidimensional. The default value is True.

.. versionadded:: 1.12
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be after the summary, it is here for polyval because it was added in a later release.

@charris charris added this to the 1.12.0 release milestone Apr 16, 2016
@e-q
Copy link
Contributor Author
e-q commented Apr 20, 2016

Comments largely addressed, thanks for taking the time to look at this

@e-q e-q changed the title ENH: Add polyrootval to numpy.polynomial ENH: Add polyvalfromroots to numpy.polynomial Apr 22, 2016

If `r` is a 1-D array, then `p(x)` will have the same shape as `x`. If
`r` is multidimensional, then the shape of the result depends on the
value of `tensor`. If `tensor` is true the shape will be r.shape[1:] +
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No tensor anymore...

@e-q
Copy link
Contributor Author
e-q commented Apr 26, 2016

Thanks for the feedback! When making these changes, I also found that it failed for scalar x, so I accounted for that and added appropriate testing.

Incidentally, this brought to my attention that np.lib.polyval and np.polynomial.polynomial.polyval are distinct implementations; the latter handles scalar x inputs fine, while the former crashes when trying to do len(x).

return np.ones_like(x)
if np.isscalar(x) or isinstance(x, (tuple, list)):
x = np.array(x, ndmin=1, copy=0) + 0.0
if x.size == 0:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If x is not an array it doesn't have the attribute size. Same for x.ndim below. Note how polyval handles this, just assume tensor=True. I'll guess that you don't need to check for scalar above if you do this. You should also test the scalar case for x. Case where x is neither a scalar nor tuple/list would be x an instance of Polynomial.

@charris
Copy link
Member
charris commented Apr 28, 2016

Hmm, and I must apologize. Now that I've understood what I did with tensor, I think we can keep it here. The difference was that if tensor=True, each column of r gets evaluated for every value of x, whereas if tensor=False, each column of r gets evaluated only for the corresponding broadcast value of x.

8000
@homu
Copy link
Contributor
homu commented May 13, 2016

☔ The latest upstream changes (presumably #6872) made this pull request unmergeable. Please resolve the merge conflicts.

As one can easily encounter when working with high-order signal processing
filters, converting a high-order polynomial from its roots to its polynomial
coefficients can be quite lossy, leading to inaccuracies in the filter's
properties.

This PR adds a new function, `polyrootval` - based on `polyval` -  that
evaluates a polynomial given a list of its roots. The benefit of calculating it
this way can be seen at scipy/scipy:6059. Some tests are included, as well.
@e-q
Copy link
Contributor Author
e-q commented Jun 2, 2016

Thanks for the clarification about tensor, I didn't really grok it before. I've reimplemented it and added a test and example for True and False.

>>> polyvalfromroots(b, r, tensor=False)
array([-0., 0.])
"""
r = np.array(r, ndmin=1, copy=0) + 0.0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be like

    r = np.array(r, ndmin=1, copy=0)
    if r.dtype.char in '?bBhHiIlLqQpP':
        # astype fails with NA
        r = r + 0.0
    if isinstance(x, (tuple, list)):
        x = np.asarray(x)
    if isinstance(x, np.ndarray) and tensor:
        r = r.reshape(r.shape + (1,)*x.ndim)
    if isinstance(x, np.ndarray) and x.size == 0 and not tensor:
        raise ValueError("Empty x cannot broadcast when tensor == False")
    return np.prod(x - r, axis=0)

The error situation is actually a bit tricky, as sometimes broadcasting empty x does work, but it is a sometimes thing, for instance r = [1] works, but r = [1,2] does not. The first only works because the number of elements in the row is 1, which is a special "broadcasting" value. So probably better to just error out in that case. The prod function already returns 1 for empty first axis, so no need to special case that. For testing you can reshape empty arrays to get various other empty arrays. For instance

In [47]: array([]).reshape(0,2,3).shape
Out[47]: (0, 2, 3)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe make that

    if isinstance(x, np.ndarray) and x.ndim >= r.ndim:
        raise ValueError("x.ndim must be < r.ndim when tensor == False")

@charris
Copy link
Member
charris commented Jun 12, 2016

@e-q I've made a couple of fixups in #7734, so closing this. If you have comments, please make them there.

@charris charris closed this Jun 12, 2016
@e-q e-q deleted the polyrootval branch June 13, 2016 16:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants
0