Skip to content
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

Abstracting Polynomials #179

Merged
merged 96 commits into from
Apr 7, 2020
Merged

Abstracting Polynomials #179

merged 96 commits into from
Apr 7, 2020

Conversation

mileslucas
Copy link
Contributor

@mileslucas mileslucas commented Oct 3, 2019

This is a pretty big pull request with a lot of moving parts, I'll do my best to document and update my changes not only within the code but within this PR.

1. Create an AbstractPolynomial type and interface
Allows creation of an abstract interface which requires minor implementations for each type of polynomial.

  • Define AbstractPolynomial type
    Note There are some ambiguities that occur when treating AbstractPolynomial <: Number, mainly in the arithmetic it is hard to define +(p::AbstractPolynomial, n::Number) because those types cause an ambiguous function error (related to Should Polynomials subtype Number ? #173, add toml files; v1.0 forward; make subtype of Number close #173 #174)
  • Define inspection (length, size, etc.)
  • Define conversions and promotions
    The existing promotion and conversion rules will stand, in addition the promote type for two AbstractPolynomial will be Polynomial, since every special polynomial can be (somewhat easily) converted to a standard polynomial.
  • Define arithmetic operations
  • Define indexing operations
  • Define number-like interface zero, one, etc.
  • Plotting
  • Showing

2. The common interface
All polynomials should have the following common methods. This is very similar to the way Distributions.jl handles its abstract types

  • fromroots(::Type{P}, ....) create polynomial of type P from roots. Defaults to Polynomial
    this calls _fromroots(::Type{P}, ...) in the concrete implementation
  • fit(::Type{P}, x, y, ...) fit data using polynomial of type P. This relies on _vander(::Type{P}...) only.
  • roots(p) return the roots of the polynomial. This relies on _companion(p) only.
  • coeffs(p) wrapper for p.coeffs
  • domain(p), domain(::Type{P}) return the domain of the indeterminate, This relies on _domain.
  • vander(::Type{P}) just a wrapper for _vander
  • companion(p) just a wrapper for _companion
  • scale_to_domain(p, x) scale_to_domain(::Type{P}, x), return x scaled to the domain of the polynomial
  • variable(p) variable(::Type{P}) get the indeterminate of the polynomial.
  • derivative(p, k=1) return derivate of the polynomial
  • integral(p, k=0) return integral of the polynomial
  • `integrate(p, a, b)1 return definite integral of the polynomial from a to b
  • truncate truncate! round off and chop leading zeros
  • chop chop! chop leading zeros
  • degree and order
  • norm and conjugate
    This matches most of the previous implementation plus some extra. By defining fit as the least-squares solution of the vandermonde matrix _vander, we only have to implement the latter for each concrete type (same with roots and _companion).

3. Deprecate Old functionality

  • Deprecate Poly -> Polynomial
  • Deprecate polyX -> appropriate new implementation

4. New polynomials

  • ChebyshevT
  • ChebyshevU
  • Legendre
  • Hermite
  • HermiteHe
  • Laguerre
  • Lagrange

5. Documentation
Document everything with doctests and plots :)

6. Tests
Make sure there are no breaking changes via deprecation tests and maintain an equal or greater coverage of functionality

@mileslucas
Copy link
Contributor Author

At this point all of the existing implementation is re-written and properly deprecated.

@olof3
Copy link
Contributor

olof3 commented Dec 20, 2019

@jverzani Something like Polynomial(roots=[1,2,3])) looks good.

For many (typically academic) applications of charpoly I guess that one would not like to go through eigvals (if it is a matrix of Ints or Syms) but compute the polynomial directly so that it has coefficients of type eltype(A). If this is done it is would be a reasonable function for that odd time that you'd like to check a linear algebra exercise or something similar.

Edit: Seems like SymPy has this functionality: A = Sym.(rand(1:9, 3, 3)); s = Sym("s"); A.charpoly(s), so perhaps not necessary to implement it in Polynomials.
It could possibly be helpful with some kind of pointer to SymPy and Polynomial(roots=eigvals(A))) if someone comes looking for this in Polynomials.

@jverzani
Copy link
Member

I'm sorry @olof3, I think I misread your comments. You are talking about two things, right:

  • instead of fromroots, using a keyword argument to the Polynomial constructor
  • in addition, possibly adding a charpoly constructor. I don't think this is necessary, as my understanding is that it would mostly be for pedagogical uses, in which case maybe more is to be learned by calling eigvals directly.

@mileslucas
Copy link
Contributor Author

Preface: I don't have any academic knowledge of advanced polynomials and my only previous usage has been with the numpy.polynomials package.

  • The ChebyshevT type is based in this wikipedia entry and this numpy implementation. The exigence for creating this abstract implementation was to better accommodate having different types of polynomials (eg ChebyshevT are not defined in any other package as far as I could see when I looked a few months ago).

  • Polynomial(;roots=) looks good semantically, but it will require a few extra lines to enable usage like Polynomial{Float64}(roots=Float32[1, 2, 3]).

  • I have no problem removing the default definition for fit; my overall approach was to default everything to Polynomial.

  • The default order for fit existed before I rewrote the code

  • I originally chose integral over integrate because integral seems less surprising to return another AbstractPolynomial whereas integrate ought to return a scalar. Mathematica, does this, I don't know about other examples. derivative matches the grammar, no other reason.

other comments
Again, I wanted to try and create a framework that allows us to start to have more coverage of polynomial types like in numpy. This is important because if people are trying to switch from Python, they will regard a JuliaMath repo as reliable, but will be unsatisfied if a certain type is not available in Julia. I also feel like it is easier to keep these types within the single Polynomials.jl package, because the end-user interface is extremely similar for all of them and after initial implementation I don't see how there would be much need for independent and parallel development of different types.

@jverzani
Copy link
Member

jverzani commented Dec 23, 2019 via email

@mileslucas
Copy link
Contributor Author

Hm, I don't really like Polynomial([1, 2, 3], roots=true). That feels like a pythonic approach to the problem as opposed to a julian approach (ie. dispatch within a single function using kwargs instead of multiple dispatch). We should be able to dispatch fine using Polynomial(; roots=[1, 2, 3]), since it has no positional arguments, we can make the roots kwarg required, and still be able to set a different varname. I feel like this is the least surprising in terms of inputs and outputs.

I'll go ahead and make changes to integrate and diff.

I haven't done any benchmarks. I can write some basic ones up. What do you think of these-

  • Do we want to compare to numpy?
  • Polynomial evaluation across large num of coeffs and large num of inputs
  • Polynomial rootfinding
  • Polynomial fitting

I can put these into a /bench folder with their own environment.

@mileslucas
Copy link
Contributor Author

Also, in terms of breaking builds, I've made sure every single previous test still passes. Fortunately, this ensures we maintain full coverage in our testing. Unfortunately, that means we can only be as sure about not breaking as our unit tests are. I've also made sure that things like poly.A show a dep warning and recommend using coeffs(poly) or poly.coeffs, so the name changes should show deprecation warnings, too.

@mileslucas
Copy link
Contributor Author

mileslucas commented Dec 23, 2019

@jverzani Base exports diff, which gets the difference element-wise in an array. Should we duck this or find a different word?

E: I've left it at derivative now, my first attempt to duck caused a bunch of wacky test failures

@olof3
Copy link
Contributor

olof3 commented Dec 23, 2019

@mileslucas Have you checked out https://github.com/JuliaApproximation/ and in particular https://github.com/JuliaApproximation/ApproxFunOrthogonalPolynomials.jl "This package implements orthogonal polynomial based spaces to ApproxFun, including Chebyshev, Jacobi, Laguerre, and Hermite.".

I also feel like it is easier to keep these types within the single Polynomials.jl package,

For packages that include Polynomials.jl I guess it is nicer with a lightweight Polynomials.jl package that only includes typical functionality for dealing with polynomials?

@mileslucas
Copy link
Contributor Author

mileslucas commented Dec 23, 2019

I don’t see in that package where I could construct a Chebyshev series directly, which is the exact use case that led me to write this PR. Edit: I see you can create it with Fun(Chebyshev(), coeffs).

This means the only value we’re adding is a simpler interface for directly manipulating series and fitting arbitrary data.

In terms of lightening the package, I truly don’t think having the orthogonal polynomials in here will bloat the package. Each implementation is going to around 300 lines of code or fewer, and the common interface makes it easier to keep things consistent.

At most, I’d consider making three repos: AbstractPolynomials.jl, Polynomials.jl, SpecialPolynomials.jl, which takes the current organizational structure but streamlines the Polynomials.jl package. The trade-off is 3x more organizational and logistical work by devs.

@mileslucas
Copy link
Contributor Author

mileslucas commented Dec 23, 2019

So if we decide to not include the special polynomials then 90% of this rewrite is moot, since there’s no sense in having an abstract type for only one concrete subtype.

I have no issues with scrapping this, so don’t feel like my sunk time should influence this decision. What do you both think

@olof3
Copy link
Contributor

olof3 commented Dec 23, 2019

I'm not sure what most sensible approach is. I think it could be a good idea if some more people weighed in on this, but perhaps it would be useful to clarify exactly what the questions are.

Definitely a number of things that needed fixing, and great that someone got around to do it! I don't see why the Chebyshev expansions can't be in another package if they don't end up in Polynomials.jl.

@jverzani
Copy link
Member

jverzani commented Dec 24, 2019 via email


function Base.:+(p1::Polynomial, p2::Polynomial)
p1.var != p2.var && error("Polynomials must have same variable")
n = max(length(p1), length(p2))
Copy link
Member

Choose a reason for hiding this comment

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

In #188 it is seen that this should be n-1, or we could use degree in place of length

@jverzani
Copy link
Member

Hi @mileslucas . Sorry, I took a hiatus from this that took longer than I thought it would. But I would like to get your nice work merged. Here are some current thoughts. Hopefully you haven't moved on altogether from this work.

  • In the long run, your idea of having three packages (AbstractPolynomials, Polynomials, and SpecialPolynomials) may be the proper separation, but until there is a wider collection of special polynomials, let's keep as one. Though maybe a new subdirectory would be useful for the special polynomials.

  • I'd like to have a) merge this and tag a release(but then remove the deprecations, and a few other modifications) b) add deprecations and bump the version number c) release 1.0 with new names.

  • I'd still like to have a better name for fromroots. I don't really like poly for this, but prefer no change than fromroots (poly is from MatLab). Python (numpy) appears to use a logical positional argument to indicate this, which I don't like. You had reservations about a keyword argument (which I now share -- as I don't like Polynomial(; roots=[...]) anymore. Maybe just a deprecation warning to use prod(variable() .- rts) or some such construct is better.

  • I played around with this to see what adding a new polynomial basis would look like. The results are here: https://github.com/jverzani/Polynomials.jl/blob/bernstein/src/polynomials/Bernstein.jl
    In writing this, a number of methods default to this process: convert to Polynomial{T}, work there, convert back to Bernstein{T}. (For example p(x) and * do this.) Should this be a default, so that if there are no faster algorithms available, the user doesn't have to define it this way? Also, it seems the showterm method should pass an instance of AbstractPolynomial{T} not a subtype, as in the Bernstein example, access to N would be useful.

  • The roots function has some issues arising from the truncate call. We need to remove this. For example, here we have roots(fromroots(1.0:12)) has issues; but eigvals(companion(fromroots(1.0:12))) does not.

  • we should add the domain information into the plot recipe.

@mileslucas
Copy link
Contributor Author

Hey, no worries, this has also been on the back-burrner for me.

I'm happy to make any changes you see fit to make this work better not only for you as a maintainer but for the community as a whole, too.

For the new polynomial basis, I think having the fallback option be convert to polynomial -> operate -> convert back is part of the benefit of abstracting these things.

Let's work off your list here and put down concrete steps to get this merged. I fear if we sit too long this will end up dying in PR purgatory. How would you like to organize those changes?

@jverzani
Copy link
Member

Great. I have a few things going on: a branch that includes some changes against your PR and the start of a SpecialPolynomials.jl package. I'll push later today. Would love your feedback.

@jverzani
Copy link
Member

I have a few repositories to look at:

which contains some suggested changes to this main pull request.

which contains some implementations of special polynomials.

My game plan with this was the following:

Version 0.7

✓ hold off on deprecations
✓ adjust roots; doc note to alternatives
✓ drop sorting of roots output. Following issue #185, use a keyword to pass to eigvals
✓ fix variable definition (return monomial x in the polynomial basis)
✓ iteration; add basis method
✓ add round (to go with truncate, chop)
✓ remove order as synonym for length; use order term only for derivative
✓ For fit, make deg positional, not a keyword (like Matlab, as I don't like deg for degree as keyword.)
✓ change to iszero from == zero(T) in showterm instance
✓ Close issue #189 for p(A::Array) for Polynomial and Poly type. Not for ChebyshevT (as x ∉ domain(p) isn't satisfied
✓ adjust type calculation for derivative; adjust derivative(::ChebyshevT...) to use recursion for order > 1

--> Plots
✓ plot recipe respects domain if finite
✓ tighten range of plot recipe
✓ adjust tests for change of tightening

--> Chebyshev
✓ ChebyshevT: fix derivative if order > degree
✓ ChebyshevT: make copy, as derivative was modifying p
✓ ChebyshevT: make basis more explicit when printing

--> Docs
✓ docs to emphasize basis
✓ change doc order -> degree

--> run some benchmarks
✓ add Base.:+(p::Polynomial{T}, c::S) where {T,S<:Number} method
✓ add @inbounds for p*q; use one(x) in places
✓ fit: use vand \ y in place of pinv(vand) * y
✓ divrem: avoid R.(num[0:n]), as it is slower than typed comprehension
✓ slightly different format for companion

works with Unitful.jl?

X pass, needed changes to + and * are cumbersome

Version 0.8

  • add deprecations? Make a compat module? I'm now leaning towards the latter.

Version 1.0

  • make Compat module loadable by the user

@mileslucas
Copy link
Contributor Author

This sounds great. Let me know how/where I should help! Perhaps a good way would be to merge this PR into a diff branch and make PRs off that and then have a final PR when that branch is finished?

@jverzani
Copy link
Member

Why don't I make a PR against yours, and you can resolve. Once done, we can merge your branch. That will be 0.7. I'm still unclear about 0.8, but am heavily leaning towards putting the Poly part in a separate module along with the MATLAB names and exporting. That gives us the option to remove, deprecate, or keep (which I'm currently inclined to do for 1.0, but perhaps could be convinced otherwise).

@jverzani jverzani merged commit 4c99692 into JuliaMath:master Apr 7, 2020
@jverzani
Copy link
Member

jverzani commented Apr 7, 2020

Thanks so much @mileslucas !!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants