Skip to content

Commit ac9625a

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 a77519d commit ac9625a

4 files changed

Lines changed: 160 additions & 24 deletions

File tree

src/integral.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ function integrate(
8181

8282
eq = expand(eq)
8383

84-
if x == nothing
84+
if x === nothing
8585
vars = get_variables(eq)
8686
if length(vars) > 1
8787
error("Multiple symbolic variables detect. Please pass the independent variable to `integrate`")
@@ -356,7 +356,7 @@ end
356356
function try_symbolic(eq, x, has_sym_consts = false, params = []; plan = default_plan())
357357
y = integrate_symbolic(eq, x; plan)
358358

359-
if y == nothing
359+
if y === nothing
360360
if has_sym_consts && !isempty(params)
361361
@info("Symbolic integration failed. Try changing constant parameters ([$(join(params, ", "))]) to numerical values.")
362362
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 = 1.0e-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 = 1.0e-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,10 +64,15 @@ 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
5873
p_rules = [
59-
@rule Ω(+(~~xs)) => max(map(Ω, ~~xs)...)
60-
@rule Ω(~x * ~y) => Ω(~x) + Ω(~y)
74+
@rule Ω(+(~~xs)) => apply_max_omega(~~xs)
75+
@rule Ω(*(~~xs)) => apply_sum_omega(~~xs)
6176
@rule Ω(^(~x, ~k::is_proper)) => is_pos_int(~k) ? ~k * Ω(~x) : NaN
6277
@rule Ω(~x::is_number) => 0
6378
@rule Ω(~x::is_var) => 1
@@ -66,16 +81,66 @@ p_rules = [
6681

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

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

75140
# rules to extract the kernel (the holomorphic portion) of an expression
76141
s_rules = [
77-
@rule Ω(+(~~xs)) => sum(map(Ω, ~~xs))
78-
@rule Ω(*(~~xs)) => prod(map(Ω, ~~xs))
142+
@rule Ω(+(~~xs)) => apply_sum_omega_kernel(~~xs)
143+
@rule Ω(*(~~xs)) => apply_prod_omega_kernel(~~xs)
79144
# @rule Ω(~x / ~y) => Ω(~x) * Ω(^(~y, -1))
80145
@rule Ω(^(~x::is_linear_poly, ~k::is_pos_int)) => ^(~x, ~k)
81146
@rule Ω(~x::is_var) => ~x
@@ -264,9 +329,9 @@ function decompose_rational(eq)
264329
for i in 1:n
265330
x₀ = test_point(false, 1.0)
266331
d = Dict(x => x₀)
267-
b[i] = 1 / substitute(eq, d)
332+
b[i] = 1 / extract_numeric_val(Complex, substitute(eq, d))
268333
for j in 1:n
269-
A[i, j] = substitute(F[j], d)
334+
A[i, j] = extract_numeric_val(Complex, substitute(F[j], d))
270335
end
271336
end
272337

@@ -276,9 +341,14 @@ function decompose_rational(eq)
276341
return p
277342
end
278343

344+
# Helper for rational rules - distribute Ω over sum
345+
apply_sum_omega_rat(xs) = length(xs) == 1 ? Ω(xs[1]) : Ω(xs[1]) + apply_sum_omega_rat(xs[2:end])
346+
# Helper for rational rules - distribute Ω over product
347+
apply_prod_omega_rat(xs) = length(xs) == 1 ? Ω(xs[1]) : Ω(xs[1]) * apply_prod_omega_rat(xs[2:end])
348+
279349
rational_rules = [
280-
@rule Ω(+(~~xs)) => sum(map(Ω, ~~xs))
281-
@rule Ω(*(~~xs)) => prod(map(Ω, ~~xs))
350+
@rule Ω(+(~~xs)) => apply_sum_omega_rat(~~xs)
351+
@rule Ω(*(~~xs)) => apply_prod_omega_rat(~~xs)
282352
@rule Ω(^(~x::is_poly, ~k::is_neg_int)) => expand(
283353
^(
284354
decompose_rational(~x),

0 commit comments

Comments
 (0)