Arbitrary Degree Polynomial Root Finder (real numbers only)

As promised in this post! :open_mouth:

It will find all the real roots as long as the coefficients are reasonable (a.k.a. not infinity) and it will sometimes even finds roots when they’re unreasonable. I’ve found that it works pretty well even with polynomials of degree 100. It does not work well with polynomials of degree 200.

Example of an unreasonable polynomial that worked anyway:
0.66853180993545+1.7558181104719 x-0.42926850647086 x^2+0.69164854420848 x^3+1.3273258012241 x^4+0.43469577626489 x^5+1.4850991614606e-006 x^6=0
x=-292701.82949164,-0.34498050186085

The issue here is that the leading coefficient is super small compared to the coefficient of the next degree down, so there is a root way out around -292702. Oh well, the finder found it anyway.

I just expect it to work under normal conditions, like finding the closest point on an ellipsoid to some other point (my next post), or getting eigenvalues and eigenvectors of a matrix.

Woohoo free code

--By xXxMoNkEyMaNxXx
local select=select

local abs=math.abs
local sqrt=math.sqrt
local cos,sin=math.cos,math.sin
local atan2=math.atan2

local sort=table.sort
local insert=table.insert
local remove=table.remove
local unpack=unpack or table.unpack

local sign=function(x)
	if x==0 then
		return 0
	elseif x>0 then
		return 1
	else
		return -1
	end
end

local function cpow(re,im,p) --Raise a complex number to an arbitrary real power
	local magnitude=(re*re+im*im)^(p/2)
	local theta=atan2(im,re)*p
	return magnitude*cos(theta),magnitude*sin(theta)
end

local function csqrt(re,im) -- Square root of a complex number with no trig! :>
	if im and im~=0 then
		local m=sqrt(re*re+im*im)
		return sqrt((m+re)/2),re>=0 and (sqrt(m+im)-sqrt(m-im))/2 or im>=0 and (sqrt(m+im)+sqrt(m-im))/2 or (sqrt(m+im)+sqrt(m-im))/-2 --shut up this is the fastest way
	else
		return re>0 and sqrt(re) or 0,re<0 and sqrt(-re) or 0
	end
end

local function cbrt(x) --wow. just wow.
	return x<0 and -(-x)^(1/3) or x^(1/3)
end

local acc=1e-10 --floating point error tolerance
local r3=0.75^0.5 -- common constant
local function Zeroes(a0,a1,a2,a3,a4)
	local z={}
	if not a4 or abs(a4)<acc then
		if not a3 or abs(a3)<acc then
			if not a2 or abs(a2)<acc then
				z[1]=-a0/a1
			else
				local p1=a1/(-2*a2)
				local p2=a1*a1-4*a0*a2
				if p2>acc then
					local p3=sqrt(p2)/(2*a2)
					if p3>0 then
						z[1]=p1-p3
						z[2]=p1+p3
					else
						z[1]=p1+p3
						z[2]=p1-p3
					end
				elseif p2>=0 then
					z[1]=p1
				end
			end
		else -- No guarantees for sorting from now on
			local p1=a2*a2-3*a3*a1
			local p2=a2*a2*a2-4.5*a3*a2*a1+13.5*a3*a3*a0
			local p3=p2*p2-p1*p1*p1
			if abs(p3)<acc then
				if abs(p1)<acc then
					z[1]=a2/(-3*a3)
				else
					z[1]=(9*a3*a0-a2*a1)/(2*p1)
					z[2]=(4*a2*a1-9*a3*a0-a2*a2*a2/a3)/p1
				end
			else
				local p5r,p5i
				local p6r,p6i
				if p3<0 then
					local p4i=sqrt(-p3)
					p5r,p5i=cpow(p2,p4i,1/3)
					p6r,p6i=cpow(p2,-p4i,1/3)
				else
					local p4=sqrt(p3)
					p5r,p5i=cbrt(p2+p4),0
					p6r,p6i=cbrt(p2-p4),0
				end
				local z1i,z2i,z3i=(p5i+p6i)/(-3*a3),((p5r-p6r)*r3-(p5i+p6i)/2)/(-3*a3),((p6r-p5r)*r3-(p5i+p6i)/2)/(-3*a3)
				if abs(z1i)<acc then
					z[#z+1]=(a2+p5r+p6r)/(-3*a3)
				end
				if abs(z2i)<acc then -- wow I actually have to do this?????? ok FPU
					z[#z+1]=(a2-(p5r+p6r)/2+(p6i-p5i)*r3)/(-3*a3)
				end
				if abs(z3i)<acc then
					z[#z+1]=(a2-(p5r+p6r)/2+(p5i-p6i)*r3)/(-3*a3)
				end
			end
		end
	else
		local p1=a2*a2-3*a3*a1+12*a4*a0
		local p2=a2*a2*a2-4.5*a3*a2*a1+13.5*(a3*a3*a0+a4*a1*a1)-36*a4*a2*a0
		local p3=2*a2/a4-3*a3*a3/(4*a4*a4)
		local p4=(a3*a3*a3-4*a4*a3*a2+8*a4*a4*a1)/(4*a4*a4*a4)
		local p5=p2*p2-p1*p1*p1
		--print(p1,p2,p3,p4,p5)
		local p6
		if abs(p1)<acc then
			p6=cbrt(2*p2)
		elseif p5<0 then
			p6=2*(p2*p2-p5)^(1/6)*cos(atan2(sqrt(-p5),p2)/3) -- r u mad
		else
			local p5r=sqrt(p5)
			p6=cbrt(p2+p5r)+cbrt(p2-p5r)
		end
		local p7=(p6/a4-p3)/3
		if p7<-acc then --This part does not seem to ever run, probably needs more imagination
			local p7i=sqrt(-p7)
			local p8r,p8i=csqrt(-p7-p3,p4*p7i)
			--local p9r,p9i=csqrt(-p7-p3,p4*p7i)
			if abs(-p8i-p7i)/2<acc then
				z[#z+1]=a3/(-4*a4)+p8r/2
			end
			if abs(p8i-p7i)/2<acc then
				z[#z+1]=a3/(-4*a4)-p8r/2
			end
			if abs(p7i+p8i)/2<acc then
				z[#z+1]=a3/(-4*a4)+p8r/2
			end
			if abs(p7i-p8i)/2<acc then
				z[#z+1]=a3/(-4*a4)-p8r/2
			end
		elseif p7>acc then
			local p7r=sqrt(p7)
			local p8=p4/p7r-p7-p3
			local p9=-p4/p7r-p7-p3
			if p8>acc then
				local p8r=sqrt(p8)/2
				z[#z+1]=a3/(-4*a4)-p7r/2+p8r
				z[#z+1]=a3/(-4*a4)-p7r/2-p8r
			elseif p8>=0 then
				z[#z+1]=a3/(-4*a4)-p7r/2
			end
			if p9>acc then
				local p9r=sqrt(p9)/2
				z[#z+1]=a3/(-4*a4)+p7r/2+p9r
				z[#z+1]=a3/(-4*a4)+p7r/2-p9r
			elseif p9>=0 then
				z[#z+1]=a3/(-4*a4)+p7r/2
			end
		else
			local p1=a2/(-2*a4)
			local p2=a2*a2-4*a0*a4
			if p2>acc then
				local p3=sqrt(p2)/(2*a4)
				local p4=p1+p3
				local p5=p1-p3
				if p4>acc then
					local p4r=sqrt(p4)
					z[#z+1]=p4r
					z[#z+1]=-p4r
				elseif p4>=0 then
					z[#z+1]=0
				end
				if p5>acc then
					local p5r=sqrt(p5)
					z[#z+1]=p5r
					z[#z+1]=-p5r
				elseif p5>=0 then
					z[#z+1]=0
				end
			elseif abs(p2)<acc then
				if p1>acc then
					local p1r=sqrt(p1)
					z[#z+1]=p1r
					z[#z+1]=-p1r
				elseif p1>=0 then
					z[#z+1]=0
				end
			end
		end
	end
	--sort(z)
	return z
end

local function Evaluate(coefficients,x)
	local Result=coefficients[0]
	local Multiplier=1
	for Coefficient=1,#coefficients do
		Multiplier=Multiplier*x
		Result=Result+coefficients[Coefficient]*Multiplier
	end
	return Result
end

local function EvaluateWithDerivative(coefficients,x)
	local Result,Derivative=coefficients[0],0
	local Multiplier=1
	for Coefficient=1,#coefficients do
		local c=coefficients[Coefficient]
		Derivative=Derivative+Coefficient*c*Multiplier
		Multiplier=Multiplier*x
		Result=Result+c*Multiplier
	end
	return Result,Derivative
end

--Searches for 0 starting from <value> in the direction of <interval>.  Pass unBounded as true if the zero may be out of the interval.  This is pretty slow compared to the other functions...
local function BinarySearch(coefficients,value,interval,unBounded)
	local LastResult=Evaluate(coefficients,value)
	local InitialSign=sign(LastResult)
	local Guess=0.5
	local Multiplier=0.5
	for i=1,75 do
		local Result=Evaluate(coefficients,value+Guess*interval)
		local ResultSign=sign(Result)
		if ResultSign==0 or Result==LastResult then
			return value+Guess*interval,Result
		else
			if unBounded then
				Guess=Guess+InitialSign*ResultSign*Multiplier
				if InitialSign==ResultSign then
					Multiplier=Multiplier*2
				else
					unBounded=false
				end
			else
				Multiplier=Multiplier/2
				Guess=Guess+InitialSign*ResultSign*Multiplier
			end
			LastResult=Result
		end
	end
	return value+Guess*interval,LastResult
end

--Uses Newton's method
local function IntervalSearch(coefficients,lowerBound,upperBound)
	local LowerResult,UpperResult=Evaluate(coefficients,lowerBound),Evaluate(coefficients,upperBound)
	local AverageSlope=(UpperResult-LowerResult)/(upperBound-lowerBound)
	local LastGuess=lowerBound-LowerResult/AverageSlope
	local LastResult,LastDerivative=EvaluateWithDerivative(coefficients,LastGuess)
	for i=1,25 do
		local Guess=LastGuess-LastResult/LastDerivative
		if Guess<=lowerBound or upperBound<=Guess then
			Guess=LastGuess-LastResult/AverageSlope -- Should be guaranteed to move the guess towards the right answer...
		end
		local Result,Derivative=EvaluateWithDerivative(coefficients,Guess)
		if Result==0 or Result==LastResult then
			return Guess,Result
		else
			LastGuess,LastResult,LastDerivative=Guess,Result,Derivative
		end
	end
	return BinarySearch(coefficients,lowerBound,upperBound-lowerBound)
end

local function OneSidedSearch(coefficients,bound,side)
	local LastGuess=bound+side*abs(Evaluate(coefficients,bound)/coefficients[#coefficients])^(1/#coefficients)
	local LastResult,LastDerivative=EvaluateWithDerivative(coefficients,LastGuess)
	for i=1,25 do
		local Guess=LastGuess-LastResult/LastDerivative
		if (Guess-bound)/side<0 then
			Guess=LastGuess-LastResult/side -- Not sure if it will be in bounds for certain...
			if (Guess-bound)/side<0 then
				return BinarySearch(coefficients,bound,side,true)
			end
		end
		local Result,Derivative=EvaluateWithDerivative(coefficients,Guess)
		if Result==0 or Result==LastResult then
			return Guess,Result
		else
			LastGuess,LastResult,LastDerivative=Guess,Result,Derivative
		end
	end
	return BinarySearch(coefficients,bound,side,true)
end

local function FindRoots(...) --Found roots are guaranteed to be in ascending order.
	local Offset=1
	while select(Offset,...)==0 do
		Offset=Offset+1
	end
	local Coefficients={[0]=select(Offset,...),select(Offset+1,...)}
	local Degree=#Coefficients
	while Coefficients[Degree]==0 do
		remove(Coefficients)
		Degree=Degree-1
	end
	local Roots
	if Degree>4 then
		Roots={}
		local DifferentiatedCoefficients={}
		for Coefficient=1,Degree do
			DifferentiatedCoefficients[Coefficient-1]=Coefficient*Coefficients[Coefficient]
		end
		local Extremities=FindRoots(unpack(DifferentiatedCoefficients,0))
		if #Extremities>0 then
			local LeadingCoefficient=Coefficients[Degree]
			local LastExtremity=Extremities[1]
			local LastExtremityResult=Evaluate(Coefficients,LastExtremity)
			if LastExtremityResult==0 then
				Roots[#Roots+1]=LastExtremity
			elseif LastExtremityResult*LeadingCoefficient*(1-Degree%2*2)<0 then
				Roots[#Roots+1]=OneSidedSearch(Coefficients,LastExtremity,-1)
			end
			for ExtremityIndex=2,#Extremities do
				local Extremity=Extremities[ExtremityIndex]
				local ExtremityResult=Evaluate(Coefficients,Extremity)
				if ExtremityResult==0 then
					Roots[#Roots+1]=Extremity
				elseif ExtremityResult*LastExtremityResult<0 then
					Roots[#Roots+1]=IntervalSearch(Coefficients,LastExtremity,Extremity)
				end
				LastExtremity=Extremity
				LastExtremityResult=ExtremityResult
			end
			if LastExtremityResult*LeadingCoefficient<0 then
				Roots[#Roots+1]=OneSidedSearch(Coefficients,LastExtremity,1)
			end
		else
			local Intercept=Coefficients[0]
			if Intercept==0 then
				Roots[#Roots+1]=0
			else
				Roots[#Roots+1]=OneSidedSearch(Coefficients,0,-sign(Intercept)*sign(Coefficients[Degree]))
			end
		end
	elseif Degree>2 then
		Roots=Zeroes(unpack(Coefficients,0))
		sort(Roots)
	elseif Degree>0 then
		Roots=Zeroes(unpack(Coefficients,0))
	end
	if Offset>1 then --Polynomial was degenerate with a root at 0
		local BooleanValueThatTellsYouWetherOrNotTheZeroAtZeroHasBeenInsertedIntoTheRootsTable=true
		for i=1,#Roots do
			if Roots[i]>0 then
				insert(Roots,i,0)
				BooleanValueThatTellsYouWetherOrNotTheZeroAtZeroHasBeenInsertedIntoTheRootsTable=false
				break
			end
		end
		if BooleanValueThatTellsYouWetherOrNotTheZeroAtZeroHasBeenInsertedIntoTheRootsTable then
			Roots[#Roots+1]=0
		end
	end
	return Roots
end

return FindRoots

My algorithm is to go from extremity to extremity (by finding the roots of the derivative) and see if two extremities are on opposite sides of 0. If so, there must be a root in between, and the curve is not flat anywhere between the extremities. First I try newton’s method, but if that fails, I use a binary search. There are special cases for multiplicities and roots that don’t lie between two extremities.

I made it into a module because modules are nice. I hope someone actually uses this because it was fun to make ._.

Awesome! =D

I had to write a poly root finder for my calculator a few years ago while I was taking Calc 1 in highschool. I used to love programming math, but it’s been a while since I’ve really messed around with it.