Skip to content

Commit 6e26d09

Browse files
committed
Fix SymbolicUtils v4+ compatibility issues
This commit addresses multiple API compatibility issues with SymbolicUtils v4.9+ and Symbolics v7.2+ where literal numbers are wrapped in symbolic containers. Changes: - rules.jl: Use recursive helpers for variadic rules instead of map/prod/sum which create symbolic mapreduce expressions. Add eval_numeric helper to evaluate numeric expressions like max(0,1). Fix poly_deg to return Julia numbers instead of symbolic wrappers. Add extract_numeric_val helper for decompose_rational. - roots.jl: Add extract_numeric helper function to extract numeric values from symbolic substitution results in solve_newton and find_roots. - integral.jl: Use isequal() instead of == for symbolic comparison, and === for nothing comparison to avoid symbolic boolean context errors. - numeric_utils.jl: Comprehensive rewrite of accept_solution to properly handle both Num and BasicSymbolic types. Check for Num before Number (since Num <: Number). Extract numeric values from symbolic zero representations using Symbolics.value(Num(...)). These changes ensure that the package works correctly with the new SymbolicUtils v4+ internal representation where even literal numbers are wrapped in BasicSymbolic containers. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 75a828c commit 6e26d09

4 files changed

Lines changed: 167 additions & 31 deletions

File tree

src/integral.jl

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ function integrate(eq, x = nothing;
7979

8080
eq = expand(eq)
8181

82-
if x == nothing
82+
if x === nothing
8383
vars = get_variables(eq)
8484
if length(vars) > 1
8585
error("Multiple symbolic variables detect. Please pass the independent variable to `integrate`")
@@ -127,13 +127,13 @@ function integrate(eq, xx::Tuple; kwargs...)
127127
sol = integrate(eq, x; kwargs...)
128128

129129
if sol isa Tuple
130-
if !isequal(first(sol), 0) && sol[2] == 0
130+
if !isequal(first(sol), 0) && isequal(sol[2], 0)
131131
return substitute(first(sol), Dict(x => hi)) -
132132
substitute(first(sol), Dict(x => lo))
133133
else
134134
return nothing
135135
end
136-
elseif sol != nothing
136+
elseif sol !== nothing
137137
return substitute(sol, Dict(x => hi)) - substitute(sol, Dict(x => lo))
138138
end
139139

@@ -143,26 +143,26 @@ end
143143
function get_solved(p, sol)
144144
if sol isa Tuple
145145
s = sol[1]
146-
return s == nothing ? 0 : s
146+
return s === nothing ? 0 : s
147147
else
148-
return sol == nothing ? 0 : sol
148+
return sol === nothing ? 0 : sol
149149
end
150150
end
151151

152152
function get_unsolved(p, sol)
153153
if sol isa Tuple
154154
u = sol[2]
155-
return u == nothing ? 0 : u
155+
return u === nothing ? 0 : u
156156
else
157-
return sol == 0 || sol == nothing ? p : 0
157+
return isequal(sol, 0) || sol === nothing ? p : 0
158158
end
159159
end
160160

161161
function get_err(p, sol)
162162
if sol isa Tuple
163163
return sol[3]
164164
else
165-
return sol == 0 || sol == nothing ? Inf : 0
165+
return isequal(sol, 0) || sol === nothing ? Inf : 0
166166
end
167167
end
168168

@@ -315,7 +315,7 @@ end
315315
function try_symbolic(eq, x, has_sym_consts = false, params = []; plan = default_plan())
316316
y = integrate_symbolic(eq, x; plan)
317317

318-
if y == nothing
318+
if y === nothing
319319
if has_sym_consts && !isempty(params)
320320
@info("Symbolic integration failed. Try changing constant parameters ([$(join(params, ", "))]) to numerical values.")
321321
end

src/numeric_utils.jl

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,24 +25,78 @@ function accept_solution(eq, x, sol; plan = default_plan())
2525
# Δ = substitute(diff(sol, x) - expr(eq), Dict(x => x₀))
2626
S = subs_symbols(eq, x; include_x = true, plan.radius)
2727
# Also substitute any symbols in sol that may not be in eq
28+
# Use value() to ensure consistent comparison with x (which may be BasicSymbolic)
29+
x_val = value(x)
2830
for v in get_variables(value(sol))
29-
if !haskey(S, v) && !isequal(v, x)
31+
if !haskey(S, v) && !isequal(v, x_val)
3032
S[v] = Complex(randn())
3133
end
3234
end
3335
Δ = substitute(diff(sol, x) - expr(eq), S)
36+
37+
# Check if Δ is zero - handle both Num and BasicSymbolic cases
38+
# For Num, isequal works; for BasicSymbolic, we need to extract the value
39+
if isequal(Δ, 0)
40+
return 0.0
41+
end
42+
# Try to extract numeric value for BasicSymbolic zero
43+
try
44+
Δ_val = Symbolics.value(Num(Δ))
45+
if Δ_val isa Real || Δ_val isa Complex
46+
return abs(Δ_val)
47+
end
48+
catch
49+
end
50+
3451
result = abs(Δ)
35-
# Ensure result is numeric, not symbolic
36-
if result isa Number
37-
return result
38-
else
39-
# Try to extract numeric value
52+
53+
# Note: Num <: Number, so we must check for Num BEFORE Number
54+
# First try to extract numeric value from Num type
55+
if result isa Num
56+
inner = Symbolics.unwrap(result)
57+
# Check if unwrapped value is a concrete number (not symbolic)
58+
if inner isa Real || inner isa Complex
59+
return abs(inner)
60+
end
61+
# Check if inner is structurally zero (e.g., abs(0))
62+
if isequal(inner, 0) || (SymbolicUtils.iscall(inner) && SymbolicUtils.operation(inner) === abs &&
63+
let arg = SymbolicUtils.arguments(inner)[1]
64+
isequal(arg, 0) || (arg isa Real && arg == 0)
65+
end)
66+
return 0.0
67+
end
68+
# Try value extraction for symbolic wrapper (SymbolicUtils v4+)
69+
try
70+
val = Symbolics.value(result)
71+
if val isa Real || val isa Complex
72+
return abs(val)
73+
end
74+
catch
75+
end
76+
# If still symbolic, return Inf
77+
return Inf
78+
end
79+
# For non-Num concrete numbers
80+
if result isa Real || result isa Complex
81+
return abs(result)
82+
end
83+
# Try to extract numeric value from symbolic wrapper (SymbolicUtils v4+)
84+
try
85+
val = Symbolics.value(Num(result))
86+
if val isa Real || val isa Complex
87+
return abs(val)
88+
end
89+
catch
90+
end
91+
# Fallback: try unwrap
92+
try
4093
val = Symbolics.unwrap(result)
41-
if val isa Number
94+
if val isa Real || val isa Complex
4295
return abs(val)
4396
end
44-
return Inf
97+
catch
4598
end
99+
return Inf
46100
catch e
47101
#
48102
end

src/roots.jl

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
# Helper to extract numeric value from symbolic substitution result
2+
function extract_numeric(T, expr)
3+
try
4+
# Try to get the underlying value from symbolic wrapper
5+
val = Symbolics.value(Num(expr))
6+
return T(val)
7+
catch
8+
# Fall back to direct conversion
9+
return T(expr)
10+
end
11+
end
12+
113
# solve_newton is a symbolic Newton-Ralphson solver
214
# f is a symbolic equation to be solved (f ~ 0)
315
# x is the variable to solve
@@ -8,8 +20,8 @@ function solve_newton(T, p, ∂p, x, x₀, zs; abstol = 1e-10, maxiter = 50, s =
820

921
for i in 1:maxiter
1022
d[x] = xₙ
11-
f = T(substitute(p, d))
12-
f′ = T(substitute(∂p, d))
23+
f = extract_numeric(T, substitute(p, d))
24+
f′ = extract_numeric(T, substitute(∂p, d))
1325
ρ = sum(1 / (xₙ - z) for z in zs; init = 0)
1426
xₙ₊₁ = xₙ - s * f / (f′ - s * ρ * f)
1527

@@ -60,7 +72,7 @@ function find_roots(T, p, x; abstol = 1e-8, num_roots = 0)
6072
push!(s, z)
6173

6274
z = conj(z)
63-
if abs(Complex{T}(substitute(p, Dict(x => z)))) < abstol
75+
if abs(extract_numeric(Complex{T}, substitute(p, Dict(x => z)))) < abstol
6476
push!(zs, z)
6577
push!(s, z)
6678
end

src/rules.jl

Lines changed: 81 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
########################## Predicates #########################################
22

3+
# Helper to extract numeric value from symbolic substitution result
4+
function extract_numeric_val(T, expr)
5+
try
6+
val = Symbolics.value(Num(expr))
7+
return T(val)
8+
catch
9+
return T(expr)
10+
end
11+
end
12+
313
is_pos_int(x) = is_proper(x) && isinteger(x) && x > 0
414
is_neg_int(x) = is_proper(x) && isinteger(x) && x < 0
515
is_int_gt_one(x) = is_proper(x) && isinteger(x) && x > 1
@@ -54,25 +64,80 @@ is_var(eq) = isequal(eq, var(eq))
5464

5565
@syms Ω(x)
5666

67+
# Helper to compute max of applying Ω to a list of expressions
68+
apply_max_omega(xs) = length(xs) == 1 ? Ω(xs[1]) : max(Ω(xs[1]), apply_max_omega(xs[2:end]))
69+
# Helper to compute sum of applying Ω to a list of expressions
70+
apply_sum_omega(xs) = length(xs) == 1 ? Ω(xs[1]) : Ω(xs[1]) + apply_sum_omega(xs[2:end])
71+
5772
# rules to calculate the degree of a polynomial expression or NaN if not a poly
58-
p_rules = [@rule Ω(+(~~xs)) => max(map(Ω, ~~xs)...)
59-
@rule Ω(~x * ~y) => Ω(~x) + Ω(~y)
73+
p_rules = [@rule Ω(+(~~xs)) => apply_max_omega(~~xs)
74+
@rule Ω(*(~~xs)) => apply_sum_omega(~~xs)
6075
@rule Ω(^(~x, ~k::is_proper)) => is_pos_int(~k) ? ~k * Ω(~x) : NaN
6176
@rule Ω(~x::is_number) => 0
6277
@rule Ω(~x::is_var) => 1
6378
@rule Ω(~x) => NaN]
6479

6580
function poly_deg(eq)
6681
ω = Prewalk(Chain(p_rules))(Ω(value(eq)))
67-
substitute(ω, Dict())
82+
result = substitute(ω, Dict())
83+
# Evaluate numeric expressions like max(0, 1) -> 1
84+
result = eval_numeric(result)
85+
# Convert symbolic numbers to Julia numbers
86+
# This is needed because SymbolicUtils v4+ wraps literal numbers in symbolic containers
87+
try
88+
return Symbolics.value(Num(result))
89+
catch
90+
return result # Return as-is if conversion fails
91+
end
6892
end
6993

70-
is_poly(eq) = !isnan(poly_deg(eq))
71-
is_linear_poly(eq) = isequal(poly_deg(eq), 1)
94+
# Helper function to evaluate purely numeric symbolic expressions
95+
function eval_numeric(expr)
96+
# First try to extract as Julia number
97+
if expr isa Number
98+
return expr
99+
end
100+
101+
if !SymbolicUtils.iscall(expr)
102+
# Not a call - might be a symbolic literal number or symbol
103+
# Try to extract value
104+
try
105+
return Symbolics.value(Num(expr))
106+
catch
107+
return expr
108+
end
109+
end
110+
111+
op = SymbolicUtils.operation(expr)
112+
args = SymbolicUtils.arguments(expr)
113+
114+
# Recursively evaluate arguments
115+
evaluated_args = [eval_numeric(a) for a in args]
116+
117+
# Check if all arguments are Julia numbers now
118+
if all(a -> a isa Number, evaluated_args)
119+
# Evaluate the operation
120+
try
121+
return op(evaluated_args...)
122+
catch
123+
return expr
124+
end
125+
end
126+
127+
return expr
128+
end
129+
130+
is_poly(eq) = let deg = poly_deg(eq); is_number(deg) && !isnan(deg) end
131+
is_linear_poly(eq) = poly_deg(eq) == 1
132+
133+
# Helper to distribute Ω over a list with sum
134+
apply_sum_omega_kernel(xs) = length(xs) == 1 ? Ω(xs[1]) : Ω(xs[1]) + apply_sum_omega_kernel(xs[2:end])
135+
# Helper to distribute Ω over a list with product
136+
apply_prod_omega_kernel(xs) = length(xs) == 1 ? Ω(xs[1]) : Ω(xs[1]) * apply_prod_omega_kernel(xs[2:end])
72137

73138
# rules to extract the kernel (the holomorphic portion) of an expression
74-
s_rules = [@rule Ω(+(~~xs)) => sum(map(Ω, ~~xs))
75-
@rule Ω(*(~~xs)) => prod(map(Ω, ~~xs))
139+
s_rules = [@rule Ω(+(~~xs)) => apply_sum_omega_kernel(~~xs)
140+
@rule Ω(*(~~xs)) => apply_prod_omega_kernel(~~xs)
76141
# @rule Ω(~x / ~y) => Ω(~x) * Ω(^(~y, -1))
77142
@rule Ω(^(~x::is_linear_poly, ~k::is_pos_int)) => ^(~x, ~k)
78143
@rule Ω(~x::is_var) => ~x
@@ -232,9 +297,9 @@ function decompose_rational(eq)
232297
for i in 1:n
233298
x₀ = test_point(false, 1.0)
234299
d = Dict(x => x₀)
235-
b[i] = 1 / substitute(eq, d)
300+
b[i] = 1 / extract_numeric_val(Complex, substitute(eq, d))
236301
for j in 1:n
237-
A[i, j] = substitute(F[j], d)
302+
A[i, j] = extract_numeric_val(Complex, substitute(F[j], d))
238303
end
239304
end
240305

@@ -244,8 +309,13 @@ function decompose_rational(eq)
244309
return p
245310
end
246311

247-
rational_rules = [@rule Ω(+(~~xs)) => sum(map(Ω, ~~xs))
248-
@rule Ω(*(~~xs)) => prod(map(Ω, ~~xs))
312+
# Helper for rational rules - distribute Ω over sum
313+
apply_sum_omega_rat(xs) = length(xs) == 1 ? Ω(xs[1]) : Ω(xs[1]) + apply_sum_omega_rat(xs[2:end])
314+
# Helper for rational rules - distribute Ω over product
315+
apply_prod_omega_rat(xs) = length(xs) == 1 ? Ω(xs[1]) : Ω(xs[1]) * apply_prod_omega_rat(xs[2:end])
316+
317+
rational_rules = [@rule Ω(+(~~xs)) => apply_sum_omega_rat(~~xs)
318+
@rule Ω(*(~~xs)) => apply_prod_omega_rat(~~xs)
249319
@rule Ω(^(~x::is_poly, ~k::is_neg_int)) => expand(^(
250320
decompose_rational(~x),
251321
-~k))

0 commit comments

Comments
 (0)