Skip to content

Commit a0e6d3e

Browse files
feat: add specialized fast implementations of Polynomial + constant and Polynomial + Variable (#179)
* feat: add fast in-place `Polynomial + constant` implementation * feat: add fast in-place `Polynomial + Variable` implementation * Update src/operators.jl Co-authored-by: Benoît Legat <[email protected]> * refactor: rename `_constant_term_idx` to `_lowest_term_idx` --------- Co-authored-by: Benoît Legat <[email protected]>
1 parent 632b2a6 commit a0e6d3e

File tree

2 files changed

+133
-0
lines changed

2 files changed

+133
-0
lines changed

src/operators.jl

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,74 @@ function MA.operate_to!(
9292
return output
9393
end
9494

95+
"""
96+
_lowest_term_idx(p::Polynomial)
97+
98+
Return the index of the lowest term in `p` according to its monomial ordering.
99+
"""
100+
_lowest_term_idx(p::Polynomial{V, M, T}) where {V, M <: Reverse, T} = lastindex(p.x)
101+
_lowest_term_idx(p::Polynomial) = firstindex(p.x)
102+
103+
"""
104+
_insert_constant_term!(p::Polynomial)
105+
106+
Insert a constant (degree 0) term into polynomial `p` at the appropriate position for the
107+
monomial ordering of `p`. Assume that a constant term does not already exists.
108+
"""
109+
function _insert_constant_term!(p::Polynomial{V, M, T}) where {V, M <: Reverse, T}
110+
push!(MP.coefficients(p), zero(T))
111+
push!(MP.monomials(p).Z, zeros(Int, length(MP.variables(p))))
112+
return p
113+
end
114+
115+
function _insert_constant_term!(p::Polynomial{V, M, T}) where {V, M, T}
116+
insert!(MP.coefficients(p), 1, zero(T))
117+
insert!(MP.monomials(p).Z, 1, zeros(Int, length(MP.variables(p))))
118+
return p
119+
end
120+
121+
function MA.operate!(op::Union{typeof(+), typeof(-)}, p::Polynomial{V, M, T}, x::T) where {V, M, T}
122+
c_idx = _lowest_term_idx(p)
123+
if MP.nterms(p) == 0 || !MP.isconstant(MP.terms(p)[c_idx])
124+
_insert_constant_term!(p)
125+
c_idx = _lowest_term_idx(p)
126+
end
127+
coeffs = MP.coefficients(p)
128+
coeffs[c_idx] = op(coeffs[c_idx], x)
129+
if iszero(coeffs[c_idx])
130+
deleteat!(coeffs, c_idx)
131+
deleteat!(MP.monomials(p), c_idx)
132+
end
133+
return p
134+
end
135+
136+
function MA.operate!(op::Union{typeof(+), typeof(-)}, p::Polynomial{V, M, T}, x::Variable{V, M}) where {V, M, T}
137+
vars = MP.variables(p)
138+
idx = searchsortedfirst(vars, x; rev = true)
139+
monos = MP.monomials(p)
140+
if idx > length(vars) || !isequal(vars[idx], x)
141+
for mono in monos
142+
insert!(MP.exponents(mono), idx, 0)
143+
end
144+
insert!(vars, idx, x)
145+
end
146+
mono = Monomial{V, M}(vars, zeros(Int, length(vars)))
147+
mono.z[idx] = 1
148+
idx = searchsortedfirst(monos, mono)
149+
coeffs = MP.coefficients(p)
150+
N = MP.nterms(p)
151+
if idx > N || !isequal(monos[idx], mono)
152+
insert!(monos.Z, idx, MP.exponents(mono))
153+
insert!(coeffs, idx, zero(T))
154+
end
155+
coeffs[idx] = op(coeffs[idx], one(T))
156+
if iszero(coeffs[idx])
157+
deleteat!(coeffs, idx)
158+
deleteat!(monos, idx)
159+
end
160+
return p
161+
end
162+
95163
function MA.operate!(
96164
op::Union{typeof(+),typeof(-)},
97165
p::Polynomial,

test/mutable_arithmetics.jl

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,68 @@ using DynamicPolynomials
3535
@test q == x + y + 1
3636
end
3737
end
38+
39+
@testset "Fast path cases: $ord" for ord in [
40+
InverseLexOrder,
41+
LexOrder,
42+
Graded{InverseLexOrder},
43+
Graded{LexOrder},
44+
Reverse{InverseLexOrder},
45+
Reverse{LexOrder},
46+
Graded{Reverse{InverseLexOrder}},
47+
Graded{Reverse{LexOrder}},
48+
Reverse{Graded{InverseLexOrder}},
49+
Reverse{Graded{LexOrder}},
50+
]
51+
@polyvar x y z monomial_order=ord
52+
53+
# allocation tests vary between Julia versions, so they're upper bounds
54+
@testset "Polynomial + constant" begin
55+
poly = 2 * x^2 + 3 * x * y + z * y^2
56+
poly2 = copy(poly)
57+
result = poly + 2
58+
MA.operate!(+, poly, 2)
59+
@test isequal(poly, result)
60+
# down from 576 using the generic method
61+
@test (@allocated MA.operate!(+, poly2, 2)) <= 272
62+
# subsequent additions don't allocate
63+
@test (@allocated MA.operate!(+, poly2, 2)) == 0
64+
65+
# also test `-`
66+
poly = 2 * x^2 + 3 * x * y + z * y^2
67+
poly2 = copy(poly)
68+
result = poly - 2
69+
MA.operate!(-, poly, 2)
70+
@test isequal(poly, result)
71+
# down from 576 using the generic method
72+
@test (@allocated MA.operate!(-, poly2, 2)) <= 272
73+
# subsequent additions don't allocate
74+
@test (@allocated MA.operate!(-, poly2, 2)) == 0
75+
end
76+
77+
@testset "Polynomial + Variable" begin
78+
poly = 2 * x^2 + 3 * x * y + z * y^2
79+
poly2 = copy(poly)
80+
result = poly + x
81+
MA.operate!(+, poly, x)
82+
@test isequal(poly, result)
83+
# down from 18752 using the generic method
84+
# 368 or 304 depending on ordering, more for different version
85+
# pre is especially bad
86+
@test (@allocated MA.operate!(+, poly2, x)) <= 400
87+
# down from 1904 using the generic method
88+
@test (@allocated MA.operate!(+, poly2, x)) <= 144
89+
90+
# also test `-`
91+
poly = 2 * x^2 + 3 * x * y + z * y^2
92+
poly2 = copy(poly)
93+
result = poly - x
94+
MA.operate!(-, poly, x)
95+
@test isequal(poly, result)
96+
# down from 18752 using the generic method
97+
# 368 or 304 depending on ordering
98+
@test (@allocated MA.operate!(-, poly2, x)) <= 400
99+
# down from 1904 using the generic method
100+
@test (@allocated MA.operate!(-, poly2, x)) <= 144
101+
end
102+
end

0 commit comments

Comments
 (0)