Skip to content

Commit 2d9460d

Browse files
fix(risch): use SU4 Sym constructor; stop catch-all from hiding API breakage
`analyze_expr` introduces a fresh symbol for each new exp/log/atan/tan factor via `SymbolicUtils.Sym{Real}(name)`. Under SymbolicUtils v4 the type parameter must be `<:SymVariant`, not `Real`, so this constructor throws `TypeError`. Update to `Sym{SymbolicUtils.SymReal}(name; type=Real)`, which is the SU4 spelling that produces the same `BasicSymbolic{SymReal}` the function dispatch already expects. The bug was invisible because of a `try`/`catch` immediately around the dispatch body that re-wrapped any unrecognised exception as `NotImplementedError("integrand contains unsupported expression $f")`. The outer `integrate_risch` catch then converted that into an unevaluated `∫(f, x)` and the user saw an apparent coverage gap. Drop the catch-all — domain limits are already raised explicitly with `NotImplementedError` at the right spots; everything else (`MethodError`/`TypeError`/etc.) is a real bug and should surface as itself so future SU/Symbolics API breakage is debuggable instead of silent. Effect on the difficult-test corpus: 15 previously-unevaluated Risch integrals (`x*log(x)`, `x*exp(x)`, `1/(x*log(x))`, `x*atan(x)`, `log(x)^2`, …) now solve cleanly with correct antiderivatives. Bisected from a comparison of Apostol-corpus per-integral results between e4fff44 (last green pre-PR-#53) and current main: RuleBased was net-positive across the SU4 upgrade (+8 newly solved, 1 regression), but Risch lost exactly this class. Adds `test/methods/risch/test_textbook_transcendentals.jl` covering the 15 regressed integrands, with verification by differentiation rather than antiderivative-equality (different but equivalent forms are common). All 30 new assertions pass locally. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
1 parent 2580241 commit 2d9460d

3 files changed

Lines changed: 162 additions & 122 deletions

File tree

src/methods/risch/frontend.jl

Lines changed: 118 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -300,135 +300,132 @@ function analyze_expr(f::SymbolicUtils.BasicSymbolic{SymbolicUtils.SymReal} , fu
300300
return f
301301
end
302302

303-
# For non-symbols, dispatch based on operation
304-
try
305-
op = operation(f)
306-
if op in (+, *, /)
307-
# Handle Add, Mul, Div operations
308-
as = [SymbolicUtils.unwrap_const(x) for x in arguments(f)]
309-
ps = [analyze_expr(a, funs, vars, args, tanArgs, expArgs) for a in as]
310-
return op(ps...)
311-
elseif op == (^)
312-
# Handle Pow operations
313-
as = [SymbolicUtils.unwrap_const(x) for x in arguments(f)]
314-
p1 = analyze_expr(as[1], funs, vars, args, tanArgs, expArgs)
315-
p2 = analyze_expr(as[2], funs, vars, args, tanArgs, expArgs)
316-
if isa(p2, Integer)
317-
return p1^p2
318-
elseif isa(p2, Number)
319-
throw(NotImplementedError("integrand contains power with unsupported exponent $p2"))
320-
end
303+
# For non-symbols, dispatch based on operation. No catch-all here on
304+
# purpose: domain limits are raised explicitly as `NotImplementedError`
305+
# at the right point; everything else (MethodError/TypeError/...) is a
306+
# bug and should surface as itself rather than be re-wrapped as
307+
# "unsupported expression", which makes upstream API breakage look like
308+
# a coverage gap.
309+
op = operation(f)
310+
if op in (+, *, /)
311+
# Handle Add, Mul, Div operations
312+
as = [SymbolicUtils.unwrap_const(x) for x in arguments(f)]
313+
ps = [analyze_expr(a, funs, vars, args, tanArgs, expArgs) for a in as]
314+
return op(ps...)
315+
elseif op == (^)
316+
# Handle Pow operations
317+
as = [SymbolicUtils.unwrap_const(x) for x in arguments(f)]
318+
p1 = analyze_expr(as[1], funs, vars, args, tanArgs, expArgs)
319+
p2 = analyze_expr(as[2], funs, vars, args, tanArgs, expArgs)
320+
if isa(p2, Integer)
321+
return p1^p2
322+
elseif isa(p2, Number)
321323
throw(NotImplementedError("integrand contains power with unsupported exponent $p2"))
322-
elseif op == exp
323-
# Handle exp function
324-
a = arguments(f)[1]
325-
i = findfirst(x -> is_rational_multiple(a, x), expArgs)
326-
n = 1
327-
if i === nothing
328-
push!(expArgs, a)
329-
else
330-
n = rational_multiple(a, expArgs[i])
331-
if !isone(denominator(n)) # n not an integer
332-
expArgs[i] = 1//denominator(n)*expArgs[i]
333-
throw(UpdatedArgList())
334-
end
335-
n = numerator(n)
336-
end
337-
if n != 1
338-
f_new = exp(expArgs[i])^n
339-
return analyze_expr(f_new, funs, vars, args, tanArgs, expArgs)
340-
end
341-
# Continue to general function handling below
342-
elseif op == tan
343-
# Handle tan function
344-
a = arguments(f)[1]
345-
i = findfirst(x -> is_rational_multiple(a, x), tanArgs)
346-
n = 1
347-
if i === nothing
348-
push!(tanArgs, a)
349-
else
350-
n = rational_multiple(a, tanArgs[i])
351-
if !isone(denominator(n)) # n not an integer
352-
tanArgs[i] = 1//denominator(n)*tanArgs[i]
353-
throw(UpdatedArgList())
354-
end
355-
n = numerator(n)
356-
end
357-
if n != 1
358-
f_new = tan_nx(n, tanArgs[i])
359-
return analyze_expr(f_new, funs, vars, args, tanArgs, expArgs)
324+
end
325+
throw(NotImplementedError("integrand contains power with unsupported exponent $p2"))
326+
elseif op == exp
327+
# Handle exp function
328+
a = arguments(f)[1]
329+
i = findfirst(x -> is_rational_multiple(a, x), expArgs)
330+
n = 1
331+
if i === nothing
332+
push!(expArgs, a)
333+
else
334+
n = rational_multiple(a, expArgs[i])
335+
if !isone(denominator(n)) # n not an integer
336+
expArgs[i] = 1//denominator(n)*expArgs[i]
337+
throw(UpdatedArgList())
360338
end
361-
# Continue to general function handling below
362-
elseif op == sinh
363-
# Transform sinh to exponentials
364-
a = arguments(f)[1]
365-
f_new = 1//2*(exp(a) - 1/exp(a))
366-
return analyze_expr(f_new, funs, vars, args, tanArgs, expArgs)
367-
elseif op == cosh
368-
# Transform cosh to exponentials
369-
a = arguments(f)[1]
370-
f_new = 1//2*(exp(a) + 1/exp(a))
371-
return analyze_expr(f_new, funs, vars, args, tanArgs, expArgs)
372-
elseif op == csch # 1/sinh
373-
a = arguments(f)[1]
374-
f_new = 2/(exp(a) - 1/exp(a))
375-
return analyze_expr(f_new, funs, vars, args, tanArgs, expArgs)
376-
elseif op == sech
377-
a = arguments(f)[1]
378-
f_new = 2/(exp(a) + 1/exp(a))
379-
return analyze_expr(f_new, funs, vars, args, tanArgs, expArgs)
380-
elseif op == tanh
381-
a = arguments(f)[1]
382-
f_new = (exp(a) - 1/exp(a))/(exp(a) + 1/exp(a))
383-
return analyze_expr(f_new, funs, vars, args, tanArgs, expArgs)
384-
elseif op == coth
385-
a = arguments(f)[1]
386-
f_new = (exp(a) + 1/exp(a))/(exp(a) - 1/exp(a))
387-
return analyze_expr(f_new, funs, vars, args, tanArgs, expArgs)
388-
elseif op == sin # transform to half angle format
389-
a = arguments(f)[1]
390-
f_new = 2*tan(1//2*a)/(1 + tan(1//2*a)^2)
391-
return analyze_expr(f_new, funs, vars, args, tanArgs, expArgs)
392-
elseif op == cos
393-
a = arguments(f)[1]
394-
f_new = (1 - tan(1//2*a)^2)/(1 + tan(1//2*a)^2)
395-
return analyze_expr(f_new, funs, vars, args, tanArgs, expArgs)
396-
elseif op == csc # 1/sin
397-
a = arguments(f)[1]
398-
f_new = 1//2*(1 + tan(1//2*a)^2)/tan(1//2*a)
399-
return analyze_expr(f_new, funs, vars, args, tanArgs, expArgs)
400-
elseif op == sec # 1/cos
401-
a = arguments(f)[1]
402-
f_new = (1 + tan(1//2*a)^2)/(1 - tan(1//2*a)^2)
403-
return analyze_expr(f_new, funs, vars, args, tanArgs, expArgs)
404-
elseif op == cot
405-
a = arguments(f)[1]
406-
f_new = 1/tan(a)
339+
n = numerator(n)
340+
end
341+
if n != 1
342+
f_new = exp(expArgs[i])^n
407343
return analyze_expr(f_new, funs, vars, args, tanArgs, expArgs)
408344
end
409-
410-
# General function handling (for exp, log, atan, tan that didn't get transformed above)
411-
i = findfirst(x -> hash(x)==hash(f), funs)
412-
if i !== nothing
413-
return vars[i]
414-
end
415-
op in [exp, log, atan, tan] ||
416-
throw(NotImplementedError("integrand contains unsupported function $op"))
345+
# Continue to general function handling below
346+
elseif op == tan
347+
# Handle tan function
417348
a = arguments(f)[1]
418-
p = analyze_expr(a, funs, vars, args, tanArgs, expArgs)
419-
tname = Symbol(:t, length(vars))
420-
t = SymbolicUtils.Sym{Real}(tname)
421-
push!(funs, f)
422-
push!(vars, t)
423-
push!(args, p)
424-
return t
425-
catch e
426-
if isa(e, UpdatedArgList)
427-
rethrow(e)
349+
i = findfirst(x -> is_rational_multiple(a, x), tanArgs)
350+
n = 1
351+
if i === nothing
352+
push!(tanArgs, a)
428353
else
429-
throw(NotImplementedError("integrand contains unsupported expression $f"))
354+
n = rational_multiple(a, tanArgs[i])
355+
if !isone(denominator(n)) # n not an integer
356+
tanArgs[i] = 1//denominator(n)*tanArgs[i]
357+
throw(UpdatedArgList())
358+
end
359+
n = numerator(n)
430360
end
361+
if n != 1
362+
f_new = tan_nx(n, tanArgs[i])
363+
return analyze_expr(f_new, funs, vars, args, tanArgs, expArgs)
364+
end
365+
# Continue to general function handling below
366+
elseif op == sinh
367+
# Transform sinh to exponentials
368+
a = arguments(f)[1]
369+
f_new = 1//2*(exp(a) - 1/exp(a))
370+
return analyze_expr(f_new, funs, vars, args, tanArgs, expArgs)
371+
elseif op == cosh
372+
# Transform cosh to exponentials
373+
a = arguments(f)[1]
374+
f_new = 1//2*(exp(a) + 1/exp(a))
375+
return analyze_expr(f_new, funs, vars, args, tanArgs, expArgs)
376+
elseif op == csch # 1/sinh
377+
a = arguments(f)[1]
378+
f_new = 2/(exp(a) - 1/exp(a))
379+
return analyze_expr(f_new, funs, vars, args, tanArgs, expArgs)
380+
elseif op == sech
381+
a = arguments(f)[1]
382+
f_new = 2/(exp(a) + 1/exp(a))
383+
return analyze_expr(f_new, funs, vars, args, tanArgs, expArgs)
384+
elseif op == tanh
385+
a = arguments(f)[1]
386+
f_new = (exp(a) - 1/exp(a))/(exp(a) + 1/exp(a))
387+
return analyze_expr(f_new, funs, vars, args, tanArgs, expArgs)
388+
elseif op == coth
389+
a = arguments(f)[1]
390+
f_new = (exp(a) + 1/exp(a))/(exp(a) - 1/exp(a))
391+
return analyze_expr(f_new, funs, vars, args, tanArgs, expArgs)
392+
elseif op == sin # transform to half angle format
393+
a = arguments(f)[1]
394+
f_new = 2*tan(1//2*a)/(1 + tan(1//2*a)^2)
395+
return analyze_expr(f_new, funs, vars, args, tanArgs, expArgs)
396+
elseif op == cos
397+
a = arguments(f)[1]
398+
f_new = (1 - tan(1//2*a)^2)/(1 + tan(1//2*a)^2)
399+
return analyze_expr(f_new, funs, vars, args, tanArgs, expArgs)
400+
elseif op == csc # 1/sin
401+
a = arguments(f)[1]
402+
f_new = 1//2*(1 + tan(1//2*a)^2)/tan(1//2*a)
403+
return analyze_expr(f_new, funs, vars, args, tanArgs, expArgs)
404+
elseif op == sec # 1/cos
405+
a = arguments(f)[1]
406+
f_new = (1 + tan(1//2*a)^2)/(1 - tan(1//2*a)^2)
407+
return analyze_expr(f_new, funs, vars, args, tanArgs, expArgs)
408+
elseif op == cot
409+
a = arguments(f)[1]
410+
f_new = 1/tan(a)
411+
return analyze_expr(f_new, funs, vars, args, tanArgs, expArgs)
431412
end
413+
414+
# General function handling (for exp, log, atan, tan that didn't get transformed above)
415+
i = findfirst(x -> hash(x)==hash(f), funs)
416+
if i !== nothing
417+
return vars[i]
418+
end
419+
op in [exp, log, atan, tan] ||
420+
throw(NotImplementedError("integrand contains unsupported function $op"))
421+
a = arguments(f)[1]
422+
p = analyze_expr(a, funs, vars, args, tanArgs, expArgs)
423+
tname = Symbol(:t, length(vars))
424+
t = SymbolicUtils.Sym{SymbolicUtils.SymReal}(tname; type=Real)
425+
push!(funs, f)
426+
push!(vars, t)
427+
push!(args, p)
428+
return t
432429
end
433430

434431
function analyze_expr(f::Number , funs::Vector, vars::Vector{SymbolicUtils.BasicSymbolic{SymbolicUtils.SymReal}}, args::Vector, tanArgs::Vector, expArgs::Vector)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using Test
2+
using SymbolicIntegration
3+
using Symbolics
4+
5+
# Textbook transcendental integrals that all reduce to introducing fresh symbols
6+
# in `analyze_expr` (the `Sym{SymReal}(name; type=Real)` path). These regressed
7+
# silently when the SymbolicUtils v3→v4 upgrade left a stale `Sym{Real}` in
8+
# place: the resulting `TypeError` was caught and re-thrown as a generic
9+
# `NotImplementedError("integrand contains unsupported expression …")`, so
10+
# `integrate_risch` returned the input integrand unevaluated and CI looked
11+
# like a coverage gap rather than a bug.
12+
@testset "[Risch] Textbook transcendental integrals" begin
13+
@variables x
14+
15+
cases = Any[
16+
x*log(x),
17+
x^2*log(x)^2,
18+
x^3*log(x)^3,
19+
x*log(x)^2,
20+
log(x)^2,
21+
log(1 - x)/(1 - x),
22+
1/(x*log(x)),
23+
x*exp(x),
24+
x^2*exp(x),
25+
x^2*exp(x^3),
26+
1/(1 + exp(x)),
27+
x^2/exp(x),
28+
x^3/exp(x),
29+
x*atan(x),
30+
x*atan(x)^2,
31+
]
32+
33+
for integrand in cases
34+
@testset "$(integrand)" begin
35+
r = integrate(integrand, x, RischMethod())
36+
@test !occursin('', string(r))
37+
# Verify by differentiation, not equality of antiderivatives —
38+
# different but equivalent forms are common.
39+
@test isequal(simplify(Symbolics.derivative(r, x) - integrand; expand=true), 0)
40+
end
41+
end
42+
end

test/runtests.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,10 @@ const TEST_GROUP = get(ENV, "TEST_GROUP", "all")
2525

2626
# Include Risch method test suites
2727
include("methods/risch/test_rational_integration.jl")
28-
include("methods/risch/test_complex_fields.jl")
28+
include("methods/risch/test_complex_fields.jl")
2929
include("methods/risch/test_bronstein_examples.jl")
3030
include("methods/risch/test_algorithm_internals.jl")
31+
include("methods/risch/test_textbook_transcendentals.jl")
3132

3233
# test internals of rulebased methods
3334
include("methods/rule_based/test_rule2.jl")

0 commit comments

Comments
 (0)