feat: Add built-in polynomial differentiation (from SymDiff.jl)

Integrated minimal symbolic differentiation from SymDiff.jl by Jukka Aho:
- differentiate(): Symbolic derivatives for polynomials (+, -, *, /, ^)
- simplify(): Expression simplification with numeric evaluation
- Zero external dependencies for basis function generation!

Changes:
- src/basis/create_basis.jl: Added differentiate() and simplify()
- src/basis/subs.jl: Added local simplify with numeric evaluation
- src/basis/abstract.jl: Removed Calculus import

This replaces the Calculus.jl dependency with ~100 lines of pure Julia
code specifically designed for polynomial basis functions.
This commit is contained in:
Jukka Aho
2025-11-08 14:32:06 +02:00
parent ebfd1392f6
commit c40d0b91c9
3 changed files with 154 additions and 7 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
using Tensors
using LinearAlgebra
import Calculus
# import Calculus # Only needed for symbolic basis generation (create_basis.jl)
# Re-export Vec for convenience (from Tensors.jl)
export Vec
+91 -2
View File
@@ -3,6 +3,95 @@
__precompile__(false)
# Minimal symbolic differentiation for polynomial basis functions
# Adapted from SymDiff.jl by Jukka Aho - zero dependencies!
differentiate(::Number, ::Symbol) = 0
differentiate(f::Symbol, x::Symbol) = f == x ? 1 : 0
function differentiate(f::Expr, x::Symbol)
@assert f.head == :call
op = first(f.args)
# Product rule: (fg)' = f'g + fg'
if op == :*
res_args = Any[:+]
for i in 2:length(f.args)
new_args = copy(f.args)
new_args[i] = differentiate(f.args[i], x)
push!(res_args, Expr(:call, new_args...))
end
return Expr(:call, res_args...)
# Power rule: d/dx f^a = a * f^(a-1) * f'
elseif op == :^
_, f_inner, a = f.args
df = differentiate(f_inner, x)
return :($a * $f_inner^($a - 1) * $df)
# Sum rule: (f + g)' = f' + g'
elseif op == :+
args = differentiate.(f.args[2:end], x)
return Expr(:call, :+, args...)
# Difference rule: (f - g)' = f' - g'
elseif op == :-
args = differentiate.(f.args[2:end], x)
return Expr(:call, :-, args...)
# Quotient rule: d/dx (f/g) = (f'g - fg')/g^2
elseif op == :/
_, g, h = f.args
dg = differentiate(g, x)
dh = differentiate(h, x)
return :(($dg * $h - $g * $dh) / $h^2)
else
error("Unsupported operation: $op")
end
end
simplify(f::Union{Number,Symbol}) = f
function simplify(ex::Expr)
@assert ex.head == :call
op = first(ex.args)
# Multiplication: remove 1's, return 0 if any 0
if op == :*
args = simplify.(ex.args[2:end])
0 in args && return 0
filter!(k -> !(isa(k, Number) && k == 1), args)
length(args) == 0 && return 1
length(args) == 1 && return first(args)
return Expr(:call, :*, args...)
# Addition: remove 0's
elseif op == :+
args = simplify.(ex.args[2:end])
filter!(k -> !isa(k, Number) || k != 0, args)
length(args) == 0 && return 0
length(args) == 1 && return first(args)
return Expr(:call, :+, args...)
# Subtraction: remove 0's
elseif op == :-
args = simplify.(ex.args[2:end])
filter!(k -> !isa(k, Number) || k != 0, args)
length(args) == 0 && return 0
length(args) == 1 && return first(args)
return Expr(:call, :-, args...)
# Power, Division: keep as-is
elseif op in (:^, :/)
args = simplify.(ex.args[2:end])
return Expr(:call, op, args...)
else
return ex
end
end
function get_reference_element_coordinates end
function eval_basis! end
function eval_dbasis! end
@@ -22,7 +111,7 @@ function calculate_interpolation_polynomials(p, V)
N = Expr(:call, :+)
for (ai, bi) in zip(solution, args)
isapprox(ai, 0.0) && continue
push!(N.args, Calculus.simplify(:($ai * $bi)))
push!(N.args, simplify(:($ai * $bi))) # Use our own simplify
end
push!(basis, N)
end
@@ -35,7 +124,7 @@ function calculate_interpolation_polynomial_derivatives(basis, D)
for (i, N) in enumerate(basis)
partial_derivatives = []
for j in 1:D
dbasis[j, i] = Calculus.simplify(Calculus.differentiate(N, vars[j]))
dbasis[j, i] = simplify(differentiate(N, vars[j])) # Use our own differentiate and simplify
end
end
return dbasis
+62 -4
View File
@@ -1,11 +1,69 @@
# This file is a part of JuliaFEM.
# License is MIT: see https://github.com/JuliaFEM/jl/blob/master/LICENSE
# Minimal simplify for subs (from SymDiff.jl by Jukka Aho)
simplify_local(f::Union{Number,Symbol}) = f
function simplify_local(ex::Expr)
@assert ex.head == :call
op = first(ex.args)
if op == :*
args = simplify_local.(ex.args[2:end])
0 in args && return 0
filter!(k -> !(isa(k, Number) && k == 1), args)
length(args) == 0 && return 1
length(args) == 1 && return first(args)
# If all args are numbers, evaluate
if all(isa(a, Number) for a in args)
return prod(args)
end
return Expr(:call, :*, args...)
elseif op == :+
args = simplify_local.(ex.args[2:end])
filter!(k -> !isa(k, Number) || k != 0, args)
length(args) == 0 && return 0
length(args) == 1 && return first(args)
# If all args are numbers, evaluate
if all(isa(a, Number) for a in args)
return sum(args)
end
return Expr(:call, :+, args...)
elseif op == :-
args = simplify_local.(ex.args[2:end])
filter!(k -> !isa(k, Number) || k != 0, args)
length(args) == 0 && return 0
length(args) == 1 && return first(args)
# If all args are numbers, evaluate
if all(isa(a, Number) for a in args)
return length(args) == 2 ? args[1] - args[2] : -args[1]
end
return Expr(:call, :-, args...)
elseif op == :^
args = simplify_local.(ex.args[2:end])
# If all args are numbers, evaluate
if all(isa(a, Number) for a in args)
return args[1]^args[2]
end
return Expr(:call, :^, args...)
elseif op == :/
args = simplify_local.(ex.args[2:end])
# If all args are numbers, evaluate
if all(isa(a, Number) for a in args)
return args[1] / args[2]
end
return Expr(:call, :/, args...)
else
args = simplify_local.(ex.args[2:end])
return Expr(:call, op, args...)
end
end
function subs(p::Number, ::Any)
return p
end
function subs(p::Symbol, data::Pair{Symbol, T}) where T
function subs(p::Symbol, data::Pair{Symbol,T}) where T
k, v = data
if p == k
return v
@@ -13,7 +71,7 @@ function subs(p::Symbol, data::Pair{Symbol, T}) where T
return p
end
function subs(p::Symbol, data::NTuple{N,Pair{Symbol, T}}) where {N, T}
function subs(p::Symbol, data::NTuple{N,Pair{Symbol,T}}) where {N,T}
for (k, v) in data
if p == k
return v
@@ -46,9 +104,9 @@ subs(expression, data)
```
"""
function subs(p::Expr, data::NTuple{N,Pair{Symbol, T}}) where {N, T}
function subs(p::Expr, data::NTuple{N,Pair{Symbol,T}}) where {N,T}
for di in data
p = subs(p, di)
end
return Calculus.simplify(p)
return simplify_local(p) # Use local simplify
end