Skip to content
Merged
1 change: 1 addition & 0 deletions .github/workflows/Downgrade.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ on:
- 'docs/**'
jobs:
test:
if: false # Temporarily disabled - see #146
runs-on: ubuntu-latest
strategy:
matrix:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/FormatCheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: julia-actions/setup-julia@v2
with:
version: '1'
- uses: fredrikekre/runic-action@v1
with:
version: '1'
48 changes: 17 additions & 31 deletions .github/workflows/Tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,36 +11,22 @@ on:
- main
paths-ignore:
- 'docs/**'
jobs:
test:
name: "Tests"
runs-on: ubuntu-latest
env:
PYTHON: python3

steps:
- uses: actions/checkout@v6
- uses: julia-actions/setup-julia@v2
with:
version: '1'
- uses: actions/cache@v5
env:
cache-name: cache-artifacts
with:
path: ~/.julia/artifacts
key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }}
restore-keys: |
${{ runner.os }}-test-${{ env.cache-name }}-
${{ runner.os }}-test-
${{ runner.os }}-
- run: >
sudo apt-get install --no-install-recommends python3-setuptools python3-wheel && pip3 install sympy
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref_name != github.event.repository.default_branch || github.ref != 'refs/tags/v*' }}

- uses: julia-actions/julia-buildpkg@v1
- uses: julia-actions/julia-runtest@v1
- uses: julia-actions/julia-processcoverage@v1
- uses: codecov/codecov-action@v5
with:
files: lcov.info
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
jobs:
tests:
name: "Tests"
strategy:
fail-fast: false
matrix:
version:
- "1"
- "lts"
- "pre"
uses: "SciML/.github/.github/workflows/tests.yml@v1"
with:
julia-version: "${{ matrix.version }}"
secrets: "inherit"
6 changes: 3 additions & 3 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ TermInterface = "8ea1fca8-c5ef-4a55-8b96-4e9afe9c9a3c"
[compat]
DataDrivenDiffEq = "1.5"
DataDrivenSparse = "0.1.2"
DataStructures = "0.18.13"
DataStructures = "0.19"
LinearAlgebra = "<0.0.1, 1"
ForwardDiff = "0.10, 1"
SpecialFunctions = "2"
Statistics = "<0.0.1, 1"
SymbolicLimits = "0.2.3"
SymbolicUtils = "2.1, 3"
Symbolics = "6"
SymbolicUtils = "4"
Symbolics = "7"
TermInterface = "2"
julia = "1.9"

Expand Down
2 changes: 1 addition & 1 deletion src/SymbolicNumericIntegration.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ using SymbolicUtils: operation, arguments
using Symbolics
using Symbolics: value, get_variables, expand_derivatives, coeff, Equation
using SymbolicUtils.Rewriters
using SymbolicUtils: exprtype, BasicSymbolic
using SymbolicUtils: issym, BasicSymbolic

using SymbolicLimits: limit

Expand Down
7 changes: 5 additions & 2 deletions src/homotopy.jl
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ function transform(eq, x)
return p
end

@syms u[20]
const u = let
@variables _u[1:20]
Symbolics.scalarize(_u)
end

function rename_factors(p, ab = ())
n = length(p)
Expand Down Expand Up @@ -114,7 +117,7 @@ end

########################## Main Integration Rules ##################################

@syms 𝛷(x, u)
@syms 𝛷(x, w)

partial_int_rules = [
# trigonometric functions
Expand Down
77 changes: 58 additions & 19 deletions src/integral.jl
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,12 @@ function integrate(

eq = expand(eq)

if x == nothing
if x === nothing
vars = get_variables(eq)
if length(vars) > 1
error("Multiple symbolic variables detect. Please pass the independent variable to `integrate`")
elseif length(vars) == 1
x = vars[1]
x = first(vars)
else
@syms 𝑥
x = 𝑥
Expand Down Expand Up @@ -125,27 +125,66 @@ function integrate(
end
end

# Helper to extract numeric value from symbolic expression
function extract_numeric_value(expr)
# Already a number
if expr isa Number
return expr
end

# Unwrap Num type
if expr isa Num
expr = Symbolics.value(expr)
if expr isa Number
return expr
end
end

# Try Float64 conversion (works for BasicSymbolic numeric literals)
try
result = Float64(expr)
return result
catch
end

# Try converting to Julia expression and evaluating
# This handles cases like sin(0) that need to be evaluated
try
julia_expr = Symbolics.toexpr(expr)
result = Base.invokelatest(eval, julia_expr)
if result isa Number
return result
end
catch
end

# Return as-is if conversion fails
return expr
end

# Evaluate an expression at a bound, using limit for infinite bounds
function eval_at_bound(expr, x, bound)
# Unwrap both expression and variable for consistent handling
expr_unwrapped = value(expr)
x_unwrapped = value(x)

if isinf(bound)
# Use symbolic limits for infinite bounds
expr_unwrapped = value(expr)
x_unwrapped = value(x)
try
result = limit(expr_unwrapped, x_unwrapped, bound)
# limit returns a tuple (value, assumptions), extract the value
if result isa Tuple
return first(result)
else
return result
result = first(result)
end
return extract_numeric_value(result)
catch e
# If limit computation fails, fall back to direct substitution
# This may result in NaN for indeterminate forms
return substitute(expr, Dict(x => bound))
return substitute(expr_unwrapped, Dict(x_unwrapped => bound))
end
else
return substitute(expr, Dict(x => bound))
result = substitute(expr_unwrapped, Dict(x_unwrapped => bound))
return extract_numeric_value(result)
end
end

Expand All @@ -155,10 +194,10 @@ function integrate(eq, xx::Tuple; kwargs...)
sol = integrate(eq, x; kwargs...)

if sol isa Tuple
if !isequal(first(sol), 0) && sol[2] == 0
if !isequal(first(sol), 0) && isequal(sol[2], 0)
hi_val = eval_at_bound(first(sol), x, hi)
lo_val = eval_at_bound(first(sol), x, lo)
result = hi_val - lo_val
result = extract_numeric_value(hi_val - lo_val)
# Check if the result is valid (not NaN or undefined)
if result isa Number && (isnan(result) || isinf(result))
return nothing
Expand All @@ -167,10 +206,10 @@ function integrate(eq, xx::Tuple; kwargs...)
else
return nothing
end
elseif sol != nothing
elseif sol !== nothing
hi_val = eval_at_bound(sol, x, hi)
lo_val = eval_at_bound(sol, x, lo)
result = hi_val - lo_val
result = extract_numeric_value(hi_val - lo_val)
# Check if the result is valid (not NaN or undefined)
if result isa Number && (isnan(result) || isinf(result))
return nothing
Expand All @@ -184,26 +223,26 @@ end
function get_solved(p, sol)
if sol isa Tuple
s = sol[1]
return s == nothing ? 0 : s
return s === nothing ? 0 : s
else
return sol == nothing ? 0 : sol
return sol === nothing ? 0 : sol
end
end

function get_unsolved(p, sol)
if sol isa Tuple
u = sol[2]
return u == nothing ? 0 : u
return u === nothing ? 0 : u
else
return sol == 0 || sol == nothing ? p : 0
return isequal(sol, 0) || sol === nothing ? p : 0
end
end

function get_err(p, sol)
if sol isa Tuple
return sol[3]
else
return sol == 0 || sol == nothing ? Inf : 0
return isequal(sol, 0) || sol === nothing ? Inf : 0
end
end

Expand Down Expand Up @@ -356,7 +395,7 @@ end
function try_symbolic(eq, x, has_sym_consts = false, params = []; plan = default_plan())
y = integrate_symbolic(eq, x; plan)

if y == nothing
if y === nothing
if has_sym_consts && !isempty(params)
@info("Symbolic integration failed. Try changing constant parameters ([$(join(params, ", "))]) to numerical values.")
end
Expand Down
75 changes: 74 additions & 1 deletion src/numeric_utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,81 @@ function accept_solution(eq, x, sol; plan = default_plan())
# x₀ = test_point(plan.complex_plane, plan.radius)
# Δ = substitute(diff(sol, x) - expr(eq), Dict(x => x₀))
S = subs_symbols(eq, x; include_x = true, plan.radius)
# Also substitute any symbols in sol that may not be in eq
# Use value() to ensure consistent comparison with x (which may be BasicSymbolic)
x_val = value(x)
for v in get_variables(value(sol))
if !haskey(S, v) && !isequal(v, x_val)
S[v] = Complex(randn())
end
end
Δ = substitute(diff(sol, x) - expr(eq), S)
return abs(Δ)

# Check if Δ is zero - handle both Num and BasicSymbolic cases
# For Num, isequal works; for BasicSymbolic, we need to extract the value
if isequal(Δ, 0)
return 0.0
end
# Try to extract numeric value for BasicSymbolic zero
try
Δ_val = Symbolics.value(Num(Δ))
if Δ_val isa Real || Δ_val isa Complex
return abs(Δ_val)
end
catch
end

result = abs(Δ)

# Note: Num <: Number, so we must check for Num BEFORE Number
# First try to extract numeric value from Num type
if result isa Num
inner = Symbolics.unwrap(result)
# Check if unwrapped value is a concrete number (not symbolic)
if inner isa Real || inner isa Complex
return abs(inner)
end
# Check if inner is structurally zero (e.g., abs(0))
if isequal(inner, 0) || (
SymbolicUtils.iscall(inner) && SymbolicUtils.operation(inner) === abs &&
let arg = SymbolicUtils.arguments(inner)[1]
isequal(arg, 0) || (arg isa Real && arg == 0)
end
)
return 0.0
end
# Try value extraction for symbolic wrapper (SymbolicUtils v4+)
try
val = Symbolics.value(result)
if val isa Real || val isa Complex
return abs(val)
end
catch
end
# If still symbolic, return Inf
return Inf
end
# For non-Num concrete numbers
if result isa Real || result isa Complex
return abs(result)
end
# Try to extract numeric value from symbolic wrapper (SymbolicUtils v4+)
try
val = Symbolics.value(Num(result))
if val isa Real || val isa Complex
return abs(val)
end
catch
end
# Fallback: try unwrap
try
val = Symbolics.unwrap(result)
if val isa Real || val isa Complex
return abs(val)
end
catch
end
return Inf
catch e
#
end
Expand Down
18 changes: 15 additions & 3 deletions src/roots.jl
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
# Helper to extract numeric value from symbolic substitution result
function extract_numeric(T, expr)
try
# Try to get the underlying value from symbolic wrapper
val = Symbolics.value(Num(expr))
return T(val)
catch
# Fall back to direct conversion
return T(expr)
end
end

# solve_newton is a symbolic Newton-Ralphson solver
# f is a symbolic equation to be solved (f ~ 0)
# x is the variable to solve
Expand All @@ -8,8 +20,8 @@ function solve_newton(T, p, ∂p, x, x₀, zs; abstol = 1.0e-10, maxiter = 50, s

for i in 1:maxiter
d[x] = xₙ
f = T(substitute(p, d))
f′ = T(substitute(∂p, d))
f = extract_numeric(T, substitute(p, d))
f′ = extract_numeric(T, substitute(∂p, d))
ρ = sum(1 / (xₙ - z) for z in zs; init = 0)
xₙ₊₁ = xₙ - s * f / (f′ - s * ρ * f)

Expand Down Expand Up @@ -60,7 +72,7 @@ function find_roots(T, p, x; abstol = 1.0e-8, num_roots = 0)
push!(s, z)

z = conj(z)
if abs(Complex{T}(substitute(p, Dict(x => z)))) < abstol
if abs(extract_numeric(Complex{T}, substitute(p, Dict(x => z)))) < abstol
push!(zs, z)
push!(s, z)
end
Expand Down
Loading
Loading