From f3b9477ffc22c1f72abfe31cf0e7daf6fef8c792 Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Wed, 25 Jun 2025 22:16:54 -0400 Subject: [PATCH 01/52] Created basic solvers for 1st order separable eqs and linear systems --- src/diffeqs.jl | 145 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 src/diffeqs.jl diff --git a/src/diffeqs.jl b/src/diffeqs.jl new file mode 100644 index 000000000..e9b962300 --- /dev/null +++ b/src/diffeqs.jl @@ -0,0 +1,145 @@ +using Groebner, Nemo + +""" +Solve first order separable ODE + +(mostly me getting used to Symbolics, not super useful in practice) + +For example, dx/dt + p(t)x ~ 0 +""" +function firstorder_separable_ode_solve(ex, x, t) + x, t = Symbolics.value(x), Symbolics.value(t) + p = Symbolics.coeff(ex.lhs, x) # function of t + P = sympy_integrate(p, t) + @variables C + return simplify(C * exp(-P)) +end + +""" +Returns evolution matrix e^(tD) +""" +evo_mat(D::Matrix{<:Number}, t::Num) = diagm(exp.(t .* diag(D))) + +""" +Solve linear continuous dynamical system of differential equations of the form Ax = x' with initial condition x0 + +# Arguments +- `A`: matrix of coefficients +- `x0`: intial conditions vector +- `t`: independent variable + +# Returns +- vector of symbolic solutions +""" +function solve_linear_system(A::Matrix{<:Number}, x0::Vector{<:Number}, t::Num) + # Check A is square + if size(A, 1) != size(A, 2) + throw(ArgumentError("Matrix A must be square.")) + end + + # Check x0 matches size of A + if size(A, 1) != length(x0) + throw(ArgumentError("Initial condition vector x0 must match the size of matrix A.")) + end + + if isdiag(A) + # If A is diagonal, use uncoupled system solver + return solve_uncoupled_system(A, x0, t) + end + + S, D = diagonalize(A) + + return simplify.(S * evo_mat(D, t) * S^-1 * x0) +end + +""" +Solve a system of uncoupled ODEs of the form: + + dx/dt = A*x + +where A is a diagonal matrix. +""" +function solve_uncoupled_system(A::Matrix{<:Number}, x0::Vector{<:Number}, t::Num) + # Check A is diagonal + if !isdiag(A) + throw(ArgumentError("Matrix A must be diagonal.")) + end + + return evo_mat(A, t) * x0 +end + +""" +Diagonalize matrix A, returning matrix S with eigenvectors as columns and diagonal matrix D with eigenvalues +""" +function diagonalize(A::Matrix{<:Number})::Tuple{Matrix{<:Number},Matrix{<:Number}} + eigs::Eigen = symbolic_eigen(A) + S = eigs.vectors + D = diagm(eigs.values) + return S, D +end + + +""" +Replacement for `LinearAlgebra.eigen` function that uses symbolic functions to avoid floating-point inaccuracies +""" +function symbolic_eigen(A::Matrix{<:Number}) + @variables λ # eigenvalue + v = Symbolics.variables(:v, 1:size(A, 1)) # vector of subscripted variables to represent eigenvector + + # find eigenvalues first + p = det(λ*I - A) ~ 0 # polynomial to solve + values = symbolic_solve(p, λ) # solve polynomial + + # then, find eigenvectors + S::Matrix{Num} = Matrix(I, size(A, 1), 0) # matrix storing vertical eigenvectors + + for value in values + eqs = (value*I - A) * v# .~ zeros(size(A, 1)) # equations to give eigenvectors + eqs = substitute(eqs, Dict(v[1] => 1)) # set first element to 1 to constrain solution space + + sol = symbolic_solve(eqs[1:end-1], v[2:end]) # solve all but one equation (because of constraining solutions above) + + # parse multivar solutions into Vector (in order) + if sol[1] isa Dict + sol = [sol[1][var] for var in v[2:end]] + end + vec::Vector{Num} = prepend!(sol, [1]) # add back the 1 (representing v_1) from substitution + S = [S vec] # add vec to matrix + end + + return Eigen(values, S) +end + +# tests +@variables x, t + +Dt = Differential(t) +ex = Dt(x) + 2 * t * x ~ 0 +println(firstorder_separable_ode_solve(ex, x, t)) +println() + +A = [1 0; 0 -1] +x0 = [1, -1] +println(solve_uncoupled_system(A, x0, t)) +println() + +# commented out below because currently can't handle complex eigenvalues +# A = [-1 -2; 2 -1] +# x0 = [1, -1] +# println(solve_linear_system(A, x0, t)) + +# println() +# println(symbolic_eigen([-3 4; -2 3])) # should be [1, -1] and [1 2; 1 1] (or equivalent) +# println() +# println(symbolic_eigen([4 -3; 8 -6])) # should be [-2, 0] and [1 2; 3 4] (or equivalent) +# println() +# println(symbolic_eigen([1 -1 0; 1 2 1; -2 1 -1])) # should be [-1, 1, 2] and [-1 -1 -1; -2 0 1; 7 1 1] (or equivalent) +# println() + +println(solve_linear_system([-3 4; -2 3], [7, 2], t)) +println() +println(solve_linear_system([4 -3; 8 -6], [7, 2], t)) +println() +# println(inv(diagonalize([1 -1 0; 1 2 1; -2 1 -1])[1])) --- shows that inv isn't maintaining symbolics +println(solve_linear_system([1 -1 0; 1 2 1; -2 1 -1], [7, 2, 3], t)) +println() From e55dc3481d8f103fb9c8c2610ed0a725c24a215a Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Thu, 26 Jun 2025 03:26:52 -0400 Subject: [PATCH 02/52] added rationalization to inverted matrix in solve_linear_system --- src/diffeqs.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/diffeqs.jl b/src/diffeqs.jl index e9b962300..c0da19a05 100644 --- a/src/diffeqs.jl +++ b/src/diffeqs.jl @@ -49,7 +49,7 @@ function solve_linear_system(A::Matrix{<:Number}, x0::Vector{<:Number}, t::Num) S, D = diagonalize(A) - return simplify.(S * evo_mat(D, t) * S^-1 * x0) + return simplify.(S * evo_mat(D, t) * rationalize.(S^-1) * x0) end """ @@ -91,7 +91,7 @@ function symbolic_eigen(A::Matrix{<:Number}) values = symbolic_solve(p, λ) # solve polynomial # then, find eigenvectors - S::Matrix{Num} = Matrix(I, size(A, 1), 0) # matrix storing vertical eigenvectors + S::Matrix{Number} = Matrix(I, size(A, 1), 0) # matrix storing vertical eigenvectors for value in values eqs = (value*I - A) * v# .~ zeros(size(A, 1)) # equations to give eigenvectors @@ -103,7 +103,7 @@ function symbolic_eigen(A::Matrix{<:Number}) if sol[1] isa Dict sol = [sol[1][var] for var in v[2:end]] end - vec::Vector{Num} = prepend!(sol, [1]) # add back the 1 (representing v_1) from substitution + vec::Vector{Number} = prepend!(sol, [1]) # add back the 1 (representing v_1) from substitution S = [S vec] # add vec to matrix end From 129803b9ac4d498b03e525b7106798bd3885db6d Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Fri, 27 Jun 2025 10:15:36 -0400 Subject: [PATCH 03/52] integrated into Symbolics, restructured files --- src/Symbolics.jl | 5 +++ src/diffeqs/diffeqs.jl | 31 +++++++++++++++ src/{diffeqs.jl => diffeqs/systems.jl} | 55 ++------------------------ 3 files changed, 39 insertions(+), 52 deletions(-) create mode 100644 src/diffeqs/diffeqs.jl rename src/{diffeqs.jl => diffeqs/systems.jl} (64%) diff --git a/src/Symbolics.jl b/src/Symbolics.jl index f192eb77c..9012b7316 100644 --- a/src/Symbolics.jl +++ b/src/Symbolics.jl @@ -218,6 +218,11 @@ include("solver/main.jl") include("solver/special_cases.jl") export symbolic_solve +# Diff Eq Solver +include("diffeqs/diffeqs.jl") +include("diffeqs/systems.jl") +export firstorder_separable_ode_solve, solve_linear_system + # Sympy Functions """ diff --git a/src/diffeqs/diffeqs.jl b/src/diffeqs/diffeqs.jl new file mode 100644 index 000000000..70b6efbff --- /dev/null +++ b/src/diffeqs/diffeqs.jl @@ -0,0 +1,31 @@ +using Symbolics +import Symbolics: value, coeff, sympy_integrate + +struct LinearODE + # dⁿx/dtⁿ + pₙ(t)(dⁿ⁻¹x/dtⁿ⁻¹) + ... + p₂(t)(dx/dt) + p₁(t)x = q(t) + + x::SymbolicUtils.Symbolic # dependent variable + t::SymbolicUtils.Symbolic # independent variable + p::AbstractArray # coefficient functions of t ordered in increasing order (p₁, p₂, ...) + q # right hand side function of t, without any x + + LinearODE(x::Num, t::Num, p, q) = new(value(x), value(t), p, q) +end + + +is_homogeneous(eq::LinearODE) = isempty(Symbolics.get_variables(eq.q)) + +""" +Solve first order separable ODE + +(mostly me getting used to Symbolics, not super useful in practice) + +For example, dx/dt + p(t)x ~ 0 +""" +function firstorder_separable_ode_solve(ex, x, t) + x, t = Symbolics.value(x), Symbolics.value(t) + p = Symbolics.coeff(ex.lhs, x) # function of t + P = Symbolics.sympy_integrate(p, t) + @variables C + return simplify(C * exp(-P)) +end \ No newline at end of file diff --git a/src/diffeqs.jl b/src/diffeqs/systems.jl similarity index 64% rename from src/diffeqs.jl rename to src/diffeqs/systems.jl index c0da19a05..521cfdf06 100644 --- a/src/diffeqs.jl +++ b/src/diffeqs/systems.jl @@ -1,19 +1,4 @@ -using Groebner, Nemo - -""" -Solve first order separable ODE - -(mostly me getting used to Symbolics, not super useful in practice) - -For example, dx/dt + p(t)x ~ 0 -""" -function firstorder_separable_ode_solve(ex, x, t) - x, t = Symbolics.value(x), Symbolics.value(t) - p = Symbolics.coeff(ex.lhs, x) # function of t - P = sympy_integrate(p, t) - @variables C - return simplify(C * exp(-P)) -end +# import Symbolics: symbolic_solve """ Returns evolution matrix e^(tD) @@ -84,7 +69,7 @@ Replacement for `LinearAlgebra.eigen` function that uses symbolic functions to a """ function symbolic_eigen(A::Matrix{<:Number}) @variables λ # eigenvalue - v = Symbolics.variables(:v, 1:size(A, 1)) # vector of subscripted variables to represent eigenvector + v = variables(:v, 1:size(A, 1)) # vector of subscripted variables to represent eigenvector # find eigenvalues first p = det(λ*I - A) ~ 0 # polynomial to solve @@ -108,38 +93,4 @@ function symbolic_eigen(A::Matrix{<:Number}) end return Eigen(values, S) -end - -# tests -@variables x, t - -Dt = Differential(t) -ex = Dt(x) + 2 * t * x ~ 0 -println(firstorder_separable_ode_solve(ex, x, t)) -println() - -A = [1 0; 0 -1] -x0 = [1, -1] -println(solve_uncoupled_system(A, x0, t)) -println() - -# commented out below because currently can't handle complex eigenvalues -# A = [-1 -2; 2 -1] -# x0 = [1, -1] -# println(solve_linear_system(A, x0, t)) - -# println() -# println(symbolic_eigen([-3 4; -2 3])) # should be [1, -1] and [1 2; 1 1] (or equivalent) -# println() -# println(symbolic_eigen([4 -3; 8 -6])) # should be [-2, 0] and [1 2; 3 4] (or equivalent) -# println() -# println(symbolic_eigen([1 -1 0; 1 2 1; -2 1 -1])) # should be [-1, 1, 2] and [-1 -1 -1; -2 0 1; 7 1 1] (or equivalent) -# println() - -println(solve_linear_system([-3 4; -2 3], [7, 2], t)) -println() -println(solve_linear_system([4 -3; 8 -6], [7, 2], t)) -println() -# println(inv(diagonalize([1 -1 0; 1 2 1; -2 1 -1])[1])) --- shows that inv isn't maintaining symbolics -println(solve_linear_system([1 -1 0; 1 2 1; -2 1 -1], [7, 2, 3], t)) -println() +end \ No newline at end of file From a5b7c51f9ca2d6b0f354ecced891147b27683a5f Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Fri, 27 Jun 2025 10:20:06 -0400 Subject: [PATCH 04/52] added unit tests --- test/diffeqs.jl | 26 ++++++++++++++++++++++++++ test/runtests.jl | 1 + 2 files changed, 27 insertions(+) create mode 100644 test/diffeqs.jl diff --git a/test/diffeqs.jl b/test/diffeqs.jl new file mode 100644 index 000000000..619f883b6 --- /dev/null +++ b/test/diffeqs.jl @@ -0,0 +1,26 @@ +using Symbolics +using Symbolics: firstorder_separable_ode_solve, solve_linear_system, LinearODE, is_homogeneous +import Groebner, Nemo, SymPy +using Test + +@variables x, y, t, C +Dt = Differential(t) +@test_broken isequal(firstorder_separable_ode_solve(Dt(x) - x ~ 0, x, t), C*exp(t)) +@test isequal(firstorder_separable_ode_solve(Dt(x) + 2*t*x ~ 0, x, t), C*exp(-(t^2))) +@test isequal(firstorder_separable_ode_solve(Dt(x) + (t^2-3)*x ~ 0, x, t), C*exp(3t - (1//3)*t^3)) + +# Systems +@test isapprox(solve_linear_system([1 0; 0 -1], [1, -1], t), [exp(t), -exp(-t)]) +@test isapprox(solve_linear_system([-3 4; -2 3], [7, 2], t), [10exp(-t) - 3exp(t), 5exp(-t) - 3exp(t)]) +@test isapprox(solve_linear_system([4 -3; 8 -6], [7, 2], t), [18 - 11exp(-2t), 24 - 22exp(-2t)]) +@test_broken isapprox(solve_linear_system([-1 -2; 2 -1], [1, -1], t), [exp(-t)*(cos(2t) + sin(2t)), exp(-t)*(sin(2t) - cos(2t))]) +@test isapprox(solve_linear_system([1 -1 0; 1 2 1; -2 1 -1], [7, 2, 3], t), (5//3)*exp(-t)*[-1, -2, 7] - 14exp(t)*[-1, 0, 1] + (16//3)*exp(2t)*[-1, 1, 1]) + +@test isequal(solve_linear_system([1 0; 0 -1], [1, -1], t), [exp(t), -exp(-t)]) +@test isequal(solve_linear_system([-3 4; -2 3], [7, 2], t), [10exp(-t) - 3exp(t), 5exp(-t) - 3exp(t)]) +@test_broken isequal(solve_linear_system([4 -3; 8 -6], [7, 2], t), [18 - 11exp(-2t), 24 - 22exp(-2t)]) +@test_broken isequal(solve_linear_system([-1 -2; 2 -1], [1, -1], t), [exp(-t)*(cos(2t) + sin(2t)), exp(-t)*(sin(2t) - cos(2t))]) +@test isequal(solve_linear_system([1 -1 0; 1 2 1; -2 1 -1], [7, 2, 3], t), (5//3)*exp(-t)*[-1, -2, 7] - 14exp(t)*[-1, 0, 1] + (16//3)*exp(2t)*[-1, 1, 1]) + +# LinearODEs +@test is_homogeneous(LinearODE(x, t, [1, 1], 0)) \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 2df5bd8ff..909739d8c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -63,6 +63,7 @@ if GROUP == "All" || GROUP == "Core" @safetestset "Function inverses test" begin include("inverse.jl") end @safetestset "Taylor Series Test" begin include("taylor.jl") end @safetestset "Discontinuity registration test" begin include("discontinuities.jl") end + @safetestset "ODE solver test" begin include("diffeqs.jl") end end end From d6daa7b6e5ab9b965adec20631cfad90e0434052 Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Fri, 27 Jun 2025 11:59:43 -0400 Subject: [PATCH 05/52] added display functionality for LinearODE --- src/diffeqs/diffeqs.jl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/diffeqs/diffeqs.jl b/src/diffeqs/diffeqs.jl index 70b6efbff..732b70a32 100644 --- a/src/diffeqs/diffeqs.jl +++ b/src/diffeqs/diffeqs.jl @@ -8,10 +8,16 @@ struct LinearODE t::SymbolicUtils.Symbolic # independent variable p::AbstractArray # coefficient functions of t ordered in increasing order (p₁, p₂, ...) q # right hand side function of t, without any x + Dt::Differential + order::Int LinearODE(x::Num, t::Num, p, q) = new(value(x), value(t), p, q) end +get_expression(eq::LinearODE) = (eq.Dt^eq.order)(eq.x) + sum([(eq.p[n])*(eq.Dt^n)(eq.x) for n = 1:length(eq.p)]) ~ eq.q + +Base.print(io::IO, eq::LinearODE) = print(io, "(D$(eq.t)^$(eq.order))$(eq.x) + " * join(["($(eq.p[length(eq.p)-n]))(D$(eq.t)^$(length(eq.p)-n))$(eq.x)" for n = 0:(eq.order-2)], " + ") * " ~ $(eq.q)") +Base.show(io::IO, eq::LinearODE) = print(io, eq) is_homogeneous(eq::LinearODE) = isempty(Symbolics.get_variables(eq.q)) From 871fff4ad9d2010e80ce40174af8fd345405c7fd Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Fri, 27 Jun 2025 12:00:27 -0400 Subject: [PATCH 06/52] implemented solver for homogeneous solutions (of degree 5 or less with no repeated roots) --- src/diffeqs/diffeqs.jl | 25 ++++++++++++++++++++++++- test/diffeqs.jl | 17 +++++++++++++++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/diffeqs/diffeqs.jl b/src/diffeqs/diffeqs.jl index 732b70a32..1139f9d00 100644 --- a/src/diffeqs/diffeqs.jl +++ b/src/diffeqs/diffeqs.jl @@ -11,7 +11,8 @@ struct LinearODE Dt::Differential order::Int - LinearODE(x::Num, t::Num, p, q) = new(value(x), value(t), p, q) + LinearODE(x::Num, t::Num, p, q) = new(value(x), value(t), p, q, Differential(t), length(p)+1) + LinearODE(x, t, p, q) = new(x, t, p, q, Differential(t), length(p)+1) end get_expression(eq::LinearODE) = (eq.Dt^eq.order)(eq.x) + sum([(eq.p[n])*(eq.Dt^n)(eq.x) for n = 1:length(eq.p)]) ~ eq.q @@ -20,6 +21,28 @@ Base.print(io::IO, eq::LinearODE) = print(io, "(D$(eq.t)^$(eq.order))$(eq.x) + " Base.show(io::IO, eq::LinearODE) = print(io, eq) is_homogeneous(eq::LinearODE) = isempty(Symbolics.get_variables(eq.q)) +has_const_coeffs(eq::LinearODE) = all(isempty.(Symbolics.get_variables.(eq.p))) + +to_homogeneous(eq::LinearODE) = LinearODE(eq.x, eq.t, eq.p, 0) + +function characteristic_polynomial(eq::LinearODE, r) + poly = 0 + @assert has_const_coeffs(eq) "ODE must have constant coefficients to generate characteristic polynomial" + p = [eq.p; 1] # add implied coefficient of 1 to highest order + for i in eachindex(p) + poly += p[i] * r^(i-1) + end + + return poly +end + +function homogeneous_solve(eq::LinearODE) + @variables r + p = characteristic_polynomial(eq, r) + roots = symbolic_solve(p, r, dropmultiplicity=false) + @variables C[1:degree(p, r)] + return sum(Symbolics.scalarize(C) .* exp.(roots*eq.t)) +end """ Solve first order separable ODE diff --git a/test/diffeqs.jl b/test/diffeqs.jl index 619f883b6..c280ddbcb 100644 --- a/test/diffeqs.jl +++ b/test/diffeqs.jl @@ -1,5 +1,5 @@ using Symbolics -using Symbolics: firstorder_separable_ode_solve, solve_linear_system, LinearODE, is_homogeneous +using Symbolics: firstorder_separable_ode_solve, solve_linear_system, LinearODE, is_homogeneous, has_const_coeffs, to_homogeneous, homogeneous_solve import Groebner, Nemo, SymPy using Test @@ -13,14 +13,27 @@ Dt = Differential(t) @test isapprox(solve_linear_system([1 0; 0 -1], [1, -1], t), [exp(t), -exp(-t)]) @test isapprox(solve_linear_system([-3 4; -2 3], [7, 2], t), [10exp(-t) - 3exp(t), 5exp(-t) - 3exp(t)]) @test isapprox(solve_linear_system([4 -3; 8 -6], [7, 2], t), [18 - 11exp(-2t), 24 - 22exp(-2t)]) + @test_broken isapprox(solve_linear_system([-1 -2; 2 -1], [1, -1], t), [exp(-t)*(cos(2t) + sin(2t)), exp(-t)*(sin(2t) - cos(2t))]) + @test isapprox(solve_linear_system([1 -1 0; 1 2 1; -2 1 -1], [7, 2, 3], t), (5//3)*exp(-t)*[-1, -2, 7] - 14exp(t)*[-1, 0, 1] + (16//3)*exp(2t)*[-1, 1, 1]) @test isequal(solve_linear_system([1 0; 0 -1], [1, -1], t), [exp(t), -exp(-t)]) @test isequal(solve_linear_system([-3 4; -2 3], [7, 2], t), [10exp(-t) - 3exp(t), 5exp(-t) - 3exp(t)]) @test_broken isequal(solve_linear_system([4 -3; 8 -6], [7, 2], t), [18 - 11exp(-2t), 24 - 22exp(-2t)]) + @test_broken isequal(solve_linear_system([-1 -2; 2 -1], [1, -1], t), [exp(-t)*(cos(2t) + sin(2t)), exp(-t)*(sin(2t) - cos(2t))]) + @test isequal(solve_linear_system([1 -1 0; 1 2 1; -2 1 -1], [7, 2, 3], t), (5//3)*exp(-t)*[-1, -2, 7] - 14exp(t)*[-1, 0, 1] + (16//3)*exp(2t)*[-1, 1, 1]) # LinearODEs -@test is_homogeneous(LinearODE(x, t, [1, 1], 0)) \ No newline at end of file +@test is_homogeneous(LinearODE(x, t, [1, 1], 0)) +@test !is_homogeneous(LinearODE(x, t, [t, 1], t^2)) + +@test has_const_coeffs(LinearODE(x, t, [1, 1], 0)) +@test !has_const_coeffs(LinearODE(x, t, [t^2, 1], 0)) + +@test is_homogeneous(to_homogeneous(LinearODE(x, t, [t, 1], t^2))) +@variables C[1:3] +@test isequal(homogeneous_solve(LinearODE(x, t, [-1], 0)), C[1]*exp(t)) +@test isequal(homogeneous_solve(LinearODE(x, t, [-4, 3], 0)), C[1]*exp(-4t) + C[2]*exp(t)) \ No newline at end of file From b7a6c2e32a51abaf22f464300c25ff5ae3d50a14 Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Fri, 27 Jun 2025 16:48:15 -0400 Subject: [PATCH 07/52] added solving via integrating factor (1st order ODEs) --- src/Symbolics.jl | 2 +- src/diffeqs/diffeqs.jl | 57 +++++++++++++++++++++++++++++------------- test/diffeqs.jl | 25 +++++++++++------- 3 files changed, 56 insertions(+), 28 deletions(-) diff --git a/src/Symbolics.jl b/src/Symbolics.jl index 9012b7316..d06ca5759 100644 --- a/src/Symbolics.jl +++ b/src/Symbolics.jl @@ -221,7 +221,7 @@ export symbolic_solve # Diff Eq Solver include("diffeqs/diffeqs.jl") include("diffeqs/systems.jl") -export firstorder_separable_ode_solve, solve_linear_system +export symbolic_solve_ode, solve_linear_system # Sympy Functions diff --git a/src/diffeqs/diffeqs.jl b/src/diffeqs/diffeqs.jl index 1139f9d00..a14156fe3 100644 --- a/src/diffeqs/diffeqs.jl +++ b/src/diffeqs/diffeqs.jl @@ -10,14 +10,15 @@ struct LinearODE q # right hand side function of t, without any x Dt::Differential order::Int + C::Vector{Num} # constants - LinearODE(x::Num, t::Num, p, q) = new(value(x), value(t), p, q, Differential(t), length(p)+1) - LinearODE(x, t, p, q) = new(x, t, p, q, Differential(t), length(p)+1) + LinearODE(x::Num, t::Num, p, q) = new(value(x), value(t), p, q, Differential(t), length(p), variables(:C, 1:length(p))) + LinearODE(x, t, p, q) = new(x, t, p, q, Differential(t), length(p), variables(:C, 1:length(p))) end -get_expression(eq::LinearODE) = (eq.Dt^eq.order)(eq.x) + sum([(eq.p[n])*(eq.Dt^n)(eq.x) for n = 1:length(eq.p)]) ~ eq.q +get_expression(eq::LinearODE) = (eq.Dt^eq.order)(eq.x) + sum([(eq.p[n])*(eq.Dt^(n-1))(eq.x) for n = 1:length(eq.p)]) ~ eq.q -Base.print(io::IO, eq::LinearODE) = print(io, "(D$(eq.t)^$(eq.order))$(eq.x) + " * join(["($(eq.p[length(eq.p)-n]))(D$(eq.t)^$(length(eq.p)-n))$(eq.x)" for n = 0:(eq.order-2)], " + ") * " ~ $(eq.q)") +Base.print(io::IO, eq::LinearODE) = print(io, "(D$(eq.t)^$(eq.order))$(eq.x) + " * join(["($(eq.p[length(eq.p)-n]))(D$(eq.t)^$(length(eq.p)-n-1))$(eq.x)" for n = 0:(eq.order-1)], " + ") * " ~ $(eq.q)") Base.show(io::IO, eq::LinearODE) = print(io, eq) is_homogeneous(eq::LinearODE) = isempty(Symbolics.get_variables(eq.q)) @@ -36,25 +37,45 @@ function characteristic_polynomial(eq::LinearODE, r) return poly end -function homogeneous_solve(eq::LinearODE) +""" +Symbolically solve a linear ODE + +Cases handled: +- ☑ first order +- ☑ homogeneous with constant coefficients +- ▢ nonhomogeneous with constant coefficients +- ▢ particular solutions (variation of parameters? undetermined coefficients?) +- ▢ [Differential transform method](https://www.researchgate.net/publication/267767445_A_New_Algorithm_for_Solving_Linear_Ordinary_Differential_Equations) +""" +function symbolic_solve_ode(eq::LinearODE) + if eq.order == 1 + return integrating_factor_solve(eq) + end + + if is_homogeneous(eq) + if has_const_coeffs(eq) + return const_coeff_solve + end + end +end + +function const_coeff_solve(eq::LinearODE) @variables r p = characteristic_polynomial(eq, r) roots = symbolic_solve(p, r, dropmultiplicity=false) - @variables C[1:degree(p, r)] - return sum(Symbolics.scalarize(C) .* exp.(roots*eq.t)) + return sum(eq.C .* exp.(roots*eq.t)) end """ -Solve first order separable ODE - -(mostly me getting used to Symbolics, not super useful in practice) - -For example, dx/dt + p(t)x ~ 0 +Solve almost any first order ODE using an integrating factor """ -function firstorder_separable_ode_solve(ex, x, t) - x, t = Symbolics.value(x), Symbolics.value(t) - p = Symbolics.coeff(ex.lhs, x) # function of t - P = Symbolics.sympy_integrate(p, t) - @variables C - return simplify(C * exp(-P)) +function integrating_factor_solve(eq::LinearODE) + p = eq.p[1] # only p + v = 0 # integrating factor + if isempty(Symbolics.get_variables(p)) + v = exp(p*eq.t) + else + v = exp(sympy_integrate(p, eq.t)) + end + return Symbolics.sympy_simplify((1/v) * (sympy_integrate(eq.q*v, eq.t) + eq.C[1])) end \ No newline at end of file diff --git a/test/diffeqs.jl b/test/diffeqs.jl index c280ddbcb..ff066f36f 100644 --- a/test/diffeqs.jl +++ b/test/diffeqs.jl @@ -1,13 +1,9 @@ using Symbolics -using Symbolics: firstorder_separable_ode_solve, solve_linear_system, LinearODE, is_homogeneous, has_const_coeffs, to_homogeneous, homogeneous_solve +using Symbolics: solve_linear_system, LinearODE, is_homogeneous, has_const_coeffs, to_homogeneous, symbolic_solve_ode import Groebner, Nemo, SymPy using Test -@variables x, y, t, C -Dt = Differential(t) -@test_broken isequal(firstorder_separable_ode_solve(Dt(x) - x ~ 0, x, t), C*exp(t)) -@test isequal(firstorder_separable_ode_solve(Dt(x) + 2*t*x ~ 0, x, t), C*exp(-(t^2))) -@test isequal(firstorder_separable_ode_solve(Dt(x) + (t^2-3)*x ~ 0, x, t), C*exp(3t - (1//3)*t^3)) +@variables x, y, t # Systems @test isapprox(solve_linear_system([1 0; 0 -1], [1, -1], t), [exp(t), -exp(-t)]) @@ -34,6 +30,17 @@ Dt = Differential(t) @test !has_const_coeffs(LinearODE(x, t, [t^2, 1], 0)) @test is_homogeneous(to_homogeneous(LinearODE(x, t, [t, 1], t^2))) -@variables C[1:3] -@test isequal(homogeneous_solve(LinearODE(x, t, [-1], 0)), C[1]*exp(t)) -@test isequal(homogeneous_solve(LinearODE(x, t, [-4, 3], 0)), C[1]*exp(-4t) + C[2]*exp(t)) \ No newline at end of file + +C = Symbolics.variables(:C, 1:5) + +## constant coefficients, nth-order +@test isequal(symbolic_solve_ode(LinearODE(x, t, [-1], 0)), C[1]*exp(t)) +@test isequal(symbolic_solve_ode(LinearODE(x, t, [-4, 3], 0)), C[1]*exp(-4t) + C[2]*exp(t)) + +## first order +@test isequal(symbolic_solve_ode(LinearODE(x, t, [5/t], 7t)), Symbolics.sympy_simplify(C[1]*t^(-5) + t^2)) +@test isequal(symbolic_solve_ode(LinearODE(x, t, [cos(t)], cos(t))), 1 + C[1]*exp(-sin(t))) +@test isequal(symbolic_solve_ode(LinearODE(x, t, [-(1+t)], 1+t)), Symbolics.sympy_simplify(C[1]*exp((1//2)t^2 + t) - 1)) +# SymPy is being weird and not simplifying correctly (and some symbols are wrong, like pi and erf being syms), but these otherwise work +@test_broken isequal(symbolic_solve_ode(LinearODE(x, t, [-2t], 1)), Symbolics.sympy_simplify(exp(t^2)*sqrt(Symbolics.variable(:pi))*((@syms erf(z))[1])(t)/2 + C[1]*exp(t^2))) +@test_broken isequal(symbolic_solve_ode(LinearODE(x, t, [1], 2sin(t))), C[1]*exp(-t) + sin(t) - cos(t)) \ No newline at end of file From ee8bc8e6764626cb5ee2230b81346766f43f3085 Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Fri, 27 Jun 2025 20:31:52 -0400 Subject: [PATCH 08/52] added handling of repeated characteristic roots --- src/diffeqs/diffeqs.jl | 17 ++++++++++++++--- test/diffeqs.jl | 10 ++++++++-- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/diffeqs/diffeqs.jl b/src/diffeqs/diffeqs.jl index a14156fe3..8863278e5 100644 --- a/src/diffeqs/diffeqs.jl +++ b/src/diffeqs/diffeqs.jl @@ -54,7 +54,7 @@ function symbolic_solve_ode(eq::LinearODE) if is_homogeneous(eq) if has_const_coeffs(eq) - return const_coeff_solve + return const_coeff_solve(eq) end end end @@ -63,7 +63,18 @@ function const_coeff_solve(eq::LinearODE) @variables r p = characteristic_polynomial(eq, r) roots = symbolic_solve(p, r, dropmultiplicity=false) - return sum(eq.C .* exp.(roots*eq.t)) + + # Handle repeated roots + solutions = exp.(roots*eq.t) + for i in eachindex(solutions)[1:end-1] + j = i+1 + while j <= length(solutions) && isequal(solutions[i], solutions[j]) + solutions[j] *= eq.t^(j-i) # multiply by t for each repetition + j+=1 + end + end + + return sum(eq.C .* solutions) end """ @@ -77,5 +88,5 @@ function integrating_factor_solve(eq::LinearODE) else v = exp(sympy_integrate(p, eq.t)) end - return Symbolics.sympy_simplify((1/v) * (sympy_integrate(eq.q*v, eq.t) + eq.C[1])) + return Symbolics.sympy_simplify((1/v) * ((isequal(eq.q, 0) ? 0 : sympy_integrate(eq.q*v, eq.t)) + eq.C[1])) end \ No newline at end of file diff --git a/test/diffeqs.jl b/test/diffeqs.jl index ff066f36f..154378df8 100644 --- a/test/diffeqs.jl +++ b/test/diffeqs.jl @@ -42,5 +42,11 @@ C = Symbolics.variables(:C, 1:5) @test isequal(symbolic_solve_ode(LinearODE(x, t, [cos(t)], cos(t))), 1 + C[1]*exp(-sin(t))) @test isequal(symbolic_solve_ode(LinearODE(x, t, [-(1+t)], 1+t)), Symbolics.sympy_simplify(C[1]*exp((1//2)t^2 + t) - 1)) # SymPy is being weird and not simplifying correctly (and some symbols are wrong, like pi and erf being syms), but these otherwise work -@test_broken isequal(symbolic_solve_ode(LinearODE(x, t, [-2t], 1)), Symbolics.sympy_simplify(exp(t^2)*sqrt(Symbolics.variable(:pi))*((@syms erf(z))[1])(t)/2 + C[1]*exp(t^2))) -@test_broken isequal(symbolic_solve_ode(LinearODE(x, t, [1], 2sin(t))), C[1]*exp(-t) + sin(t) - cos(t)) \ No newline at end of file +@test_broken isequal(symbolic_solve_ode(LinearODE(x, t, [-2t], 1)), Symbolics.sympy_simplify(exp(t^2)*sqrt(Symbolics.variable(:pi))*erf(t)/2 + C[1]*exp(t^2))) +@test_broken isequal(symbolic_solve_ode(LinearODE(x, t, [1], 2sin(t))), C[1]*exp(-t) + sin(t) - cos(t)) + +## repeated characteristic roots +@test isequal(symbolic_solve_ode(LinearODE(x, t, [1, 2], 0)), C[1]*exp(-t) + C[2]*t*exp(-t)) +@test isequal(symbolic_solve_ode(LinearODE(x, t, [0, 0, 0, 4, -4], 0)), C[1] + C[2]*t + C[3]*t^2 + C[4]*exp(2t) + C[5]*t*exp(2t)) +@test isequal(symbolic_solve_ode(LinearODE(x, t, [8, 12, 6], 0)), C[1]*exp(-2t) + C[2]*t*exp(-2t) + C[3]*t^2*exp(-2t)) + From 2376fa943964e12017a52bd44f79331d7f399d3e Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Sat, 28 Jun 2025 16:29:45 -0400 Subject: [PATCH 09/52] implemented exponential response formula / resonant response formula --- src/diffeqs/diffeqs.jl | 80 +++++++++++++++++++++++++++++++++++++++++- test/diffeqs.jl | 1 + 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/src/diffeqs/diffeqs.jl b/src/diffeqs/diffeqs.jl index 8863278e5..7123d1e36 100644 --- a/src/diffeqs/diffeqs.jl +++ b/src/diffeqs/diffeqs.jl @@ -43,7 +43,8 @@ Symbolically solve a linear ODE Cases handled: - ☑ first order - ☑ homogeneous with constant coefficients -- ▢ nonhomogeneous with constant coefficients +- ◩ nonhomogeneous with constant coefficients + - ☑ ERF + RRF - ▢ particular solutions (variation of parameters? undetermined coefficients?) - ▢ [Differential transform method](https://www.researchgate.net/publication/267767445_A_New_Algorithm_for_Solving_Linear_Ordinary_Differential_Equations) """ @@ -57,6 +58,13 @@ function symbolic_solve_ode(eq::LinearODE) return const_coeff_solve(eq) end end + + if has_const_coeffs(eq) + rrf = resonant_response_formula(eq) + if rrf !== nothing + return const_coeff_solve(to_homogeneous(eq)) + rrf + end + end end function const_coeff_solve(eq::LinearODE) @@ -89,4 +97,74 @@ function integrating_factor_solve(eq::LinearODE) v = exp(sympy_integrate(p, eq.t)) end return Symbolics.sympy_simplify((1/v) * ((isequal(eq.q, 0) ? 0 : sympy_integrate(eq.q*v, eq.t)) + eq.C[1])) +end + +""" +Returns a, r from q(t)=a*e^(rt) if it is of that form. If not, returns `nothing` +""" +function get_rrf_coeff(q, t) + facs = factors(q) + + # handle complex r + # very convoluted, could probably be improved (possibly by making heavier use of @rule) + + # Description of process: + # only one factor of c*e^((a + bi)t) -> c*cos(bt)e^at + i*c*sin(bt)e^(at) + # real(factor) / imag(factor) = cos(bt)/sin(bt) - can extract imaginary part b from this + # then, divide real(factor) = c*cos(bt)e^at by cos(bt) to get c*e^at + # call self to get c and a, then add back in b + get_b = @rule cos(~b*t) / sin(~b*t) => ~b + if length(facs) == 1 && !isequal(imag(facs[1]), 0) && get_b(real(facs[1])/imag(facs[1])) !== nothing + r_im = get_b(real(facs[1])/imag(facs[1])) + real_q = real(facs[1]) / cos(r_im*t) + if isempty(Symbolics.get_variables(real_q, t)) + return real_q, r_im*im + end + a, r_re = get_rrf_coeff(real(facs[1]) / cos(r_im*t), t) + return a, r_re + r_im*im + end + + a = prod(filter(fac -> isempty(Symbolics.get_variables(fac, [t])), facs)) + + not_a = filter(fac -> !isempty(Symbolics.get_variables(fac, [t])), facs) # should just be e^(rt) + if length(not_a) != 1 + return nothing + end + + der = expand_derivatives(Differential(t)(not_a[1])) + r = simplify(der / not_a[1]) + if !isempty(Symbolics.get_variables(r, t)) + return nothing + end + + return a, r +end + +""" +Returns a particular solution to a constant coefficient ODE with q(t) = a*e^(rt) + +Exponential Response Formula: x_p(t) = a*e^(rt)/p(r) where p(r) is characteristic polynomial + +Resonant Response Formula: If r is a characteristic root, multiply by t and take the derivative of p (possibly multiple times) +""" +function resonant_response_formula(eq::LinearODE) + @assert has_const_coeffs(eq) + + # get a and r from q = a*e^(rt) + rrf_coeff = get_rrf_coeff(eq.q, eq.t) + if rrf_coeff === nothing + return nothing + end + a, r = rrf_coeff + + # figure out how many times p needs to be differentiated before denominator isn't 0 + k = 0 + @variables s + p = characteristic_polynomial(eq, s) + Ds = Differential(s) + while isequal(substitute(expand_derivatives((Ds^k)(p)), Dict(s => r)), 0) + k += 1 + end + + return (eq.q*eq.t^k) / (substitute(expand_derivatives((Ds^k)(p)), Dict(s => r))) end \ No newline at end of file diff --git a/test/diffeqs.jl b/test/diffeqs.jl index 154378df8..16bf24e0d 100644 --- a/test/diffeqs.jl +++ b/test/diffeqs.jl @@ -50,3 +50,4 @@ C = Symbolics.variables(:C, 1:5) @test isequal(symbolic_solve_ode(LinearODE(x, t, [0, 0, 0, 4, -4], 0)), C[1] + C[2]*t + C[3]*t^2 + C[4]*exp(2t) + C[5]*t*exp(2t)) @test isequal(symbolic_solve_ode(LinearODE(x, t, [8, 12, 6], 0)), C[1]*exp(-2t) + C[2]*t*exp(-2t) + C[3]*t^2*exp(-2t)) +@test isequal(symbolic_solve_ode(LinearODE(x, t, [9, -6], 4exp(3t))), C[1]*exp(3t) + C[2]*t*exp(3t) + 2(t^2)*exp(3t)) \ No newline at end of file From b4b0f1e908cca532b3afd596e9a73155eae68471 Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Mon, 30 Jun 2025 00:20:34 -0400 Subject: [PATCH 10/52] added solving particular solutions when q(t) includes sin or cos and an exponential --- src/diffeqs/diffeqs.jl | 137 +++++++++++++++++++++++++++++++---------- test/diffeqs.jl | 5 +- 2 files changed, 110 insertions(+), 32 deletions(-) diff --git a/src/diffeqs/diffeqs.jl b/src/diffeqs/diffeqs.jl index 7123d1e36..6b3a22e80 100644 --- a/src/diffeqs/diffeqs.jl +++ b/src/diffeqs/diffeqs.jl @@ -4,21 +4,35 @@ import Symbolics: value, coeff, sympy_integrate struct LinearODE # dⁿx/dtⁿ + pₙ(t)(dⁿ⁻¹x/dtⁿ⁻¹) + ... + p₂(t)(dx/dt) + p₁(t)x = q(t) - x::SymbolicUtils.Symbolic # dependent variable - t::SymbolicUtils.Symbolic # independent variable + x::Num # dependent variable + t::Num # independent variable p::AbstractArray # coefficient functions of t ordered in increasing order (p₁, p₂, ...) - q # right hand side function of t, without any x + q::Any # right hand side function of t, without any x Dt::Differential order::Int C::Vector{Num} # constants - LinearODE(x::Num, t::Num, p, q) = new(value(x), value(t), p, q, Differential(t), length(p), variables(:C, 1:length(p))) - LinearODE(x, t, p, q) = new(x, t, p, q, Differential(t), length(p), variables(:C, 1:length(p))) + function LinearODE(x::Num, t::Num, p, q) + new(value(x), value(t), p, q, Differential(t), + length(p), variables(:C, 1:length(p))) + end + function LinearODE(x, t, p, q) + new(x, t, p, q, Differential(t), length(p), variables(:C, 1:length(p))) + end end -get_expression(eq::LinearODE) = (eq.Dt^eq.order)(eq.x) + sum([(eq.p[n])*(eq.Dt^(n-1))(eq.x) for n = 1:length(eq.p)]) ~ eq.q +function get_expression(eq::LinearODE) + (eq.Dt^eq.order)(eq.x) + sum([(eq.p[n]) * (eq.Dt^(n - 1))(eq.x) for n in 1:length(eq.p)]) ~ eq.q +end -Base.print(io::IO, eq::LinearODE) = print(io, "(D$(eq.t)^$(eq.order))$(eq.x) + " * join(["($(eq.p[length(eq.p)-n]))(D$(eq.t)^$(length(eq.p)-n-1))$(eq.x)" for n = 0:(eq.order-1)], " + ") * " ~ $(eq.q)") +function Base.print(io::IO, eq::LinearODE) + print(io, + "(D$(eq.t)^$(eq.order))$(eq.x) + " * + join( + ["($(eq.p[length(eq.p)-n]))(D$(eq.t)^$(length(eq.p)-n-1))$(eq.x)" + for n in 0:(eq.order - 1)], + " + ") * " ~ $(eq.q)") +end Base.show(io::IO, eq::LinearODE) = print(io, eq) is_homogeneous(eq::LinearODE) = isempty(Symbolics.get_variables(eq.q)) @@ -31,7 +45,7 @@ function characteristic_polynomial(eq::LinearODE, r) @assert has_const_coeffs(eq) "ODE must have constant coefficients to generate characteristic polynomial" p = [eq.p; 1] # add implied coefficient of 1 to highest order for i in eachindex(p) - poly += p[i] * r^(i-1) + poly += p[i] * r^(i - 1) end return poly @@ -43,10 +57,12 @@ Symbolically solve a linear ODE Cases handled: - ☑ first order - ☑ homogeneous with constant coefficients -- ◩ nonhomogeneous with constant coefficients +- ◩ particular solutions (variation of parameters? undetermined coefficients?) - ☑ ERF + RRF -- ▢ particular solutions (variation of parameters? undetermined coefficients?) + - ☑ complex ERF + RRF to handle sin/cos - ▢ [Differential transform method](https://www.researchgate.net/publication/267767445_A_New_Algorithm_for_Solving_Linear_Ordinary_Differential_Equations) +- ▢ Laplace Transform +- ▢ Expression parsing """ function symbolic_solve_ode(eq::LinearODE) if eq.order == 1 @@ -64,21 +80,25 @@ function symbolic_solve_ode(eq::LinearODE) if rrf !== nothing return const_coeff_solve(to_homogeneous(eq)) + rrf end + rrf_trig = exp_trig_particular_solution(eq) + if rrf_trig !== nothing + return const_coeff_solve(to_homogeneous(eq)) + rrf_trig + end end end function const_coeff_solve(eq::LinearODE) @variables r p = characteristic_polynomial(eq, r) - roots = symbolic_solve(p, r, dropmultiplicity=false) + roots = symbolic_solve(p, r, dropmultiplicity = false) # Handle repeated roots - solutions = exp.(roots*eq.t) - for i in eachindex(solutions)[1:end-1] - j = i+1 + solutions = exp.(roots * eq.t) + for i in eachindex(solutions)[1:(end - 1)] + j = i + 1 while j <= length(solutions) && isequal(solutions[i], solutions[j]) - solutions[j] *= eq.t^(j-i) # multiply by t for each repetition - j+=1 + solutions[j] *= eq.t^(j - i) # multiply by t for each repetition + j += 1 end end @@ -92,11 +112,12 @@ function integrating_factor_solve(eq::LinearODE) p = eq.p[1] # only p v = 0 # integrating factor if isempty(Symbolics.get_variables(p)) - v = exp(p*eq.t) + v = exp(p * eq.t) else v = exp(sympy_integrate(p, eq.t)) end - return Symbolics.sympy_simplify((1/v) * ((isequal(eq.q, 0) ? 0 : sympy_integrate(eq.q*v, eq.t)) + eq.C[1])) + return expand(Symbolics.sympy_simplify((1 / v) * ((isequal(eq.q, 0) ? 0 : + sympy_integrate(eq.q * v, eq.t)) + eq.C[1]))) end """ @@ -104,7 +125,7 @@ Returns a, r from q(t)=a*e^(rt) if it is of that form. If not, returns `nothing` """ function get_rrf_coeff(q, t) facs = factors(q) - + # handle complex r # very convoluted, could probably be improved (possibly by making heavier use of @rule) @@ -113,19 +134,21 @@ function get_rrf_coeff(q, t) # real(factor) / imag(factor) = cos(bt)/sin(bt) - can extract imaginary part b from this # then, divide real(factor) = c*cos(bt)e^at by cos(bt) to get c*e^at # call self to get c and a, then add back in b - get_b = @rule cos(~b*t) / sin(~b*t) => ~b - if length(facs) == 1 && !isequal(imag(facs[1]), 0) && get_b(real(facs[1])/imag(facs[1])) !== nothing - r_im = get_b(real(facs[1])/imag(facs[1])) - real_q = real(facs[1]) / cos(r_im*t) - if isempty(Symbolics.get_variables(real_q, t)) - return real_q, r_im*im + get_b = Symbolics.Chain([ + (@rule cos(t) / sin(t) => 1), (@rule cos(~b * t) / sin(~b * t) => ~b)]) + if length(facs) == 1 && !isequal(imag(facs[1]), 0) && + !isequal(get_b(real(facs[1]) / imag(facs[1])), real(facs[1]) / imag(facs[1])) + r_im = get_b(real(facs[1]) / imag(facs[1])) + real_q = real(facs[1]) / cos(r_im * t) + if isempty(Symbolics.get_variables(real_q, [t])) + return real_q, r_im * im end - a, r_re = get_rrf_coeff(real(facs[1]) / cos(r_im*t), t) - return a, r_re + r_im*im + a, r_re = get_rrf_coeff(real(facs[1]) / cos(r_im * t), t) + return a, r_re + r_im * im end a = prod(filter(fac -> isempty(Symbolics.get_variables(fac, [t])), facs)) - + not_a = filter(fac -> !isempty(Symbolics.get_variables(fac, [t])), facs) # should just be e^(rt) if length(not_a) != 1 return nothing @@ -133,13 +156,64 @@ function get_rrf_coeff(q, t) der = expand_derivatives(Differential(t)(not_a[1])) r = simplify(der / not_a[1]) - if !isempty(Symbolics.get_variables(r, t)) + if !isempty(Symbolics.get_variables(r, [t])) return nothing end return a, r end +function _parse_trig(expr, t) + parse_sin = Symbolics.Chain([(@rule sin(t) => 1), (@rule sin(~x * t) => ~x)]) + parse_cos = Symbolics.Chain([(@rule cos(t) => 1), (@rule cos(~x * t) => ~x)]) + + if !isequal(parse_sin(expr), expr) + return parse_sin(expr), true + end + + if !isequal(parse_cos(expr), expr) + return parse_cos(expr), false + end + + return nothing +end + +""" +For finding particular solution when q(t) = a*e^(rt)*cos(bt) (or sin(bt)) +""" +function exp_trig_particular_solution(eq::LinearODE) + facs = factors(eq.q) + + a = prod(filter(fac -> isempty(Symbolics.get_variables(fac, [eq.t])), facs)) + + not_a = filter(fac -> !isempty(Symbolics.get_variables(fac, [eq.t])), facs) + + r = nothing + b = nothing + is_sin = false + + if length(not_a) == 1 && _parse_trig(not_a[1], eq.t) !== nothing + r = 0 + b, is_sin = _parse_trig(not_a[1], eq.t) + elseif length(not_a) != 2 + return nothing + elseif get_rrf_coeff(not_a[1], eq.t) !== nothing && _parse_trig(not_a[2], eq.t) !== nothing + r = get_rrf_coeff(not_a[1], eq.t)[2] + b, is_sin = _parse_trig(not_a[2], eq.t) + elseif get_rrf_coeff(not_a[2], eq.t) !== nothing && + _parse_trig(not_a[1], eq.t) !== nothing + r = get_rrf_coeff(not_a[2], eq.t)[2] + b, is_sin = _parse_trig(not_a[1], eq.t) + else + return nothing + end + + combined_eq = LinearODE(eq.x, eq.t, eq.p, a * exp((r + b * im)eq.t)) + rrf = resonant_response_formula(combined_eq) + + return is_sin ? imag(rrf) : real(rrf) +end + """ Returns a particular solution to a constant coefficient ODE with q(t) = a*e^(rt) @@ -156,7 +230,7 @@ function resonant_response_formula(eq::LinearODE) return nothing end a, r = rrf_coeff - + # figure out how many times p needs to be differentiated before denominator isn't 0 k = 0 @variables s @@ -166,5 +240,6 @@ function resonant_response_formula(eq::LinearODE) k += 1 end - return (eq.q*eq.t^k) / (substitute(expand_derivatives((Ds^k)(p)), Dict(s => r))) + return expand(simplify(a * exp(r * eq.t) * eq.t^k / + (substitute(expand_derivatives((Ds^k)(p)), Dict(s => r))))) end \ No newline at end of file diff --git a/test/diffeqs.jl b/test/diffeqs.jl index 16bf24e0d..fff732e8a 100644 --- a/test/diffeqs.jl +++ b/test/diffeqs.jl @@ -50,4 +50,7 @@ C = Symbolics.variables(:C, 1:5) @test isequal(symbolic_solve_ode(LinearODE(x, t, [0, 0, 0, 4, -4], 0)), C[1] + C[2]*t + C[3]*t^2 + C[4]*exp(2t) + C[5]*t*exp(2t)) @test isequal(symbolic_solve_ode(LinearODE(x, t, [8, 12, 6], 0)), C[1]*exp(-2t) + C[2]*t*exp(-2t) + C[3]*t^2*exp(-2t)) -@test isequal(symbolic_solve_ode(LinearODE(x, t, [9, -6], 4exp(3t))), C[1]*exp(3t) + C[2]*t*exp(3t) + 2(t^2)*exp(3t)) \ No newline at end of file +## resonant response formula +@test isequal(symbolic_solve_ode(LinearODE(x, t, [9, -6], 4exp(3t))), C[1]*exp(3t) + C[2]*t*exp(3t) + 2(t^2)*exp(3t)) +### trig functions +@test isequal(symbolic_solve_ode(LinearODE(x, t, [6, 5], 2exp(-t)*cos(t))), C[1]*exp(-2t) + C[2]*exp(-3t) + (1//5)*exp(-t)*cos(t)+(3//5)*exp(-t)*sin(t)) From 9292ef4dff3df49bf8ccc9c080799b371e9206b9 Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Mon, 30 Jun 2025 09:37:16 -0400 Subject: [PATCH 11/52] added handling of complex characteristic roots --- src/diffeqs/diffeqs.jl | 26 +++++++++++++++++++------- test/diffeqs.jl | 4 ++++ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/diffeqs/diffeqs.jl b/src/diffeqs/diffeqs.jl index 6b3a22e80..7630560ff 100644 --- a/src/diffeqs/diffeqs.jl +++ b/src/diffeqs/diffeqs.jl @@ -92,17 +92,28 @@ function const_coeff_solve(eq::LinearODE) p = characteristic_polynomial(eq, r) roots = symbolic_solve(p, r, dropmultiplicity = false) - # Handle repeated roots + # Handle complex + repeated roots solutions = exp.(roots * eq.t) for i in eachindex(solutions)[1:(end - 1)] j = i + 1 - while j <= length(solutions) && isequal(solutions[i], solutions[j]) + + if imag(roots[i]) != 0 && roots[i] == conj(roots[j]) + solutions[i] = exp(real(roots[i]*eq.t))*cos(imag(roots[i]*eq.t)) + solutions[j] = exp(real(roots[i]*eq.t))*sin(imag(roots[i]*eq.t)) + end + + while j <= length(solutions) && isequal(roots[i], roots[j]) solutions[j] *= eq.t^(j - i) # multiply by t for each repetition j += 1 end end - return sum(eq.C .* solutions) + solution = sum(eq.C .* solutions) + if solution isa Complex && isequal(imag(solution), 0) + solution = real(solution) + end + + return solution end """ @@ -117,7 +128,7 @@ function integrating_factor_solve(eq::LinearODE) v = exp(sympy_integrate(p, eq.t)) end return expand(Symbolics.sympy_simplify((1 / v) * ((isequal(eq.q, 0) ? 0 : - sympy_integrate(eq.q * v, eq.t)) + eq.C[1]))) + sympy_integrate(eq.q * v, eq.t)) + eq.C[1]))) end """ @@ -191,13 +202,14 @@ function exp_trig_particular_solution(eq::LinearODE) r = nothing b = nothing is_sin = false - + if length(not_a) == 1 && _parse_trig(not_a[1], eq.t) !== nothing r = 0 b, is_sin = _parse_trig(not_a[1], eq.t) elseif length(not_a) != 2 return nothing - elseif get_rrf_coeff(not_a[1], eq.t) !== nothing && _parse_trig(not_a[2], eq.t) !== nothing + elseif get_rrf_coeff(not_a[1], eq.t) !== nothing && + _parse_trig(not_a[2], eq.t) !== nothing r = get_rrf_coeff(not_a[1], eq.t)[2] b, is_sin = _parse_trig(not_a[2], eq.t) elseif get_rrf_coeff(not_a[2], eq.t) !== nothing && @@ -241,5 +253,5 @@ function resonant_response_formula(eq::LinearODE) end return expand(simplify(a * exp(r * eq.t) * eq.t^k / - (substitute(expand_derivatives((Ds^k)(p)), Dict(s => r))))) + (substitute(expand_derivatives((Ds^k)(p)), Dict(s => r))))) end \ No newline at end of file diff --git a/test/diffeqs.jl b/test/diffeqs.jl index fff732e8a..c481f9ad7 100644 --- a/test/diffeqs.jl +++ b/test/diffeqs.jl @@ -50,6 +50,10 @@ C = Symbolics.variables(:C, 1:5) @test isequal(symbolic_solve_ode(LinearODE(x, t, [0, 0, 0, 4, -4], 0)), C[1] + C[2]*t + C[3]*t^2 + C[4]*exp(2t) + C[5]*t*exp(2t)) @test isequal(symbolic_solve_ode(LinearODE(x, t, [8, 12, 6], 0)), C[1]*exp(-2t) + C[2]*t*exp(-2t) + C[3]*t^2*exp(-2t)) +## complex characteristic roots +@test isequal(symbolic_solve_ode(LinearODE(x, t, [1, 0], 0)), C[1]*cos(t) + C[2]*sin(t)) +@test isequal(symbolic_solve_ode(LinearODE(x, t, [0, 1, 0], 0)), C[1] + C[2]*cos(t) + C[3]*sin(t)) + ## resonant response formula @test isequal(symbolic_solve_ode(LinearODE(x, t, [9, -6], 4exp(3t))), C[1]*exp(3t) + C[2]*t*exp(3t) + 2(t^2)*exp(3t)) ### trig functions From 1d26ec5ef92ca7e92c1f684f655fee5727959bb1 Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Mon, 30 Jun 2025 12:10:43 -0400 Subject: [PATCH 12/52] slight refactoring and improved documentation --- src/Symbolics.jl | 2 +- src/diffeqs/diffeqs.jl | 132 ++++++++++++++++++++++++++++++----------- test/diffeqs.jl | 2 +- 3 files changed, 98 insertions(+), 38 deletions(-) diff --git a/src/Symbolics.jl b/src/Symbolics.jl index d06ca5759..ac146a7c9 100644 --- a/src/Symbolics.jl +++ b/src/Symbolics.jl @@ -221,7 +221,7 @@ export symbolic_solve # Diff Eq Solver include("diffeqs/diffeqs.jl") include("diffeqs/systems.jl") -export symbolic_solve_ode, solve_linear_system +export LinearODE, symbolic_solve_ode, solve_linear_system # Sympy Functions diff --git a/src/diffeqs/diffeqs.jl b/src/diffeqs/diffeqs.jl index 7630560ff..901bf94d0 100644 --- a/src/diffeqs/diffeqs.jl +++ b/src/diffeqs/diffeqs.jl @@ -1,45 +1,71 @@ using Symbolics import Symbolics: value, coeff, sympy_integrate +""" +Represents a linear ordinary differential equation of the form: + +dⁿx/dtⁿ + pₙ(t)(dⁿ⁻¹x/dtⁿ⁻¹) + ... + p₂(t)(dx/dt) + p₁(t)x = q(t) + +# Fields +- `x`: dependent variable +- `t`: independent variable +- `p`: coefficient functions of `t` ordered in increasing order (p₁, p₂, ...) +- `q`: right hand side function of `t`, without any `x` + +# Examples +```jldoctest +julia> using Symbolics + +julia> @variables x, t +2-element Vector{Num}: + x + t + +julia> eq = LinearODE(x, t, [1, 2, 3], 3exp(4t)) +(Dt^3)x + (3)(Dt^2)x + (2)(Dt^1)x + (1)(Dt^0)x ~ 3exp(4t) +``` +""" struct LinearODE - # dⁿx/dtⁿ + pₙ(t)(dⁿ⁻¹x/dtⁿ⁻¹) + ... + p₂(t)(dx/dt) + p₁(t)x = q(t) - - x::Num # dependent variable - t::Num # independent variable - p::AbstractArray # coefficient functions of t ordered in increasing order (p₁, p₂, ...) - q::Any # right hand side function of t, without any x - Dt::Differential - order::Int - C::Vector{Num} # constants - - function LinearODE(x::Num, t::Num, p, q) - new(value(x), value(t), p, q, Differential(t), - length(p), variables(:C, 1:length(p))) - end - function LinearODE(x, t, p, q) - new(x, t, p, q, Differential(t), length(p), variables(:C, 1:length(p))) - end + x::Num + t::Num + p::AbstractArray + q::Any + C::Vector{Num} + + LinearODE(x, t, p, q) = new(x, t, p, q, variables(:C, 1:length(p))) end +Dt(eq::LinearODE) = Differential(eq.t) +order(eq::LinearODE) = length(eq.p) + +"""Generates symbolic expression to represent `LinearODE`""" function get_expression(eq::LinearODE) - (eq.Dt^eq.order)(eq.x) + sum([(eq.p[n]) * (eq.Dt^(n - 1))(eq.x) for n in 1:length(eq.p)]) ~ eq.q + (Dt(eq)^order(eq))(eq.x) + sum([(eq.p[n]) * (Dt(eq)^(n - 1))(eq.x) for n in 1:length(eq.p)]) ~ eq.q end -function Base.print(io::IO, eq::LinearODE) - print(io, - "(D$(eq.t)^$(eq.order))$(eq.x) + " * - join( - ["($(eq.p[length(eq.p)-n]))(D$(eq.t)^$(length(eq.p)-n-1))$(eq.x)" - for n in 0:(eq.order - 1)], - " + ") * " ~ $(eq.q)") +function Base.string(eq::LinearODE) + "(D$(eq.t)^$(order(eq)))$(eq.x) + " * + join( + ["($(eq.p[length(eq.p)-n]))(D$(eq.t)^$(length(eq.p)-n-1))$(eq.x)" + for n in 0:(order(eq) - 1)], + " + ") * " ~ $(eq.q)" end + +Base.print(io::IO, eq::LinearODE) = print(io, string(eq)) Base.show(io::IO, eq::LinearODE) = print(io, eq) +"""Returns true if q(t) = 0 for linear ODE `eq`""" is_homogeneous(eq::LinearODE) = isempty(Symbolics.get_variables(eq.q)) +"""Returns true if all coefficient functions p(t) of `eq` are constant""" has_const_coeffs(eq::LinearODE) = all(isempty.(Symbolics.get_variables.(eq.p))) - +"""Returns homgeneous version of `eq` where q(t) = 0""" to_homogeneous(eq::LinearODE) = LinearODE(eq.x, eq.t, eq.p, 0) +""" +Returns the characteristic polynomial p of `eq` (must have constant coefficients) in terms of variable `r` + +p(D) = Dⁿ + aₙ₋₁Dⁿ⁻¹ + ... + a₁D + a₀I +""" function characteristic_polynomial(eq::LinearODE, r) poly = 0 @assert has_const_coeffs(eq) "ODE must have constant coefficients to generate characteristic polynomial" @@ -63,30 +89,64 @@ Cases handled: - ▢ [Differential transform method](https://www.researchgate.net/publication/267767445_A_New_Algorithm_for_Solving_Linear_Ordinary_Differential_Equations) - ▢ Laplace Transform - ▢ Expression parsing + +Uses methods: [`integrating_factor_solve`](@ref), [`find_homogeneous_solutions`](@ref), [`find_particular_solution`](@ref) + """ function symbolic_solve_ode(eq::LinearODE) - if eq.order == 1 + if order(eq) == 1 return integrating_factor_solve(eq) end + homogeneous_solutions = find_homogeneous_solutions(eq) + if is_homogeneous(eq) - if has_const_coeffs(eq) - return const_coeff_solve(eq) - end + return homogeneous_solutions + end + + return homogeneous_solutions + find_particular_solution(eq) +end + +""" +Find homogeneous solutions of linear ODE `eq` with integration constants of `eq.C` + +Currently only works for constant coefficient ODEs +""" +function find_homogeneous_solutions(eq::LinearODE) + if has_const_coeffs(eq) + return const_coeff_solve(to_homogeneous(eq)) + end +end + +""" +Find a particular solution to linear ODE `eq` + +Currently works for any linear combination of exponentials, sin, cos, or an exponential times sin or cos (e.g. e^2t * cos(-t) + e^-3t + sin(5t)) +""" +function find_particular_solution(eq::LinearODE) + # if q has multiple terms, find a particular solution for each and sum together + terms = Symbolics.terms(eq.q) + if length(terms) != 1 + return sum(find_particular_solution.(terms)) end if has_const_coeffs(eq) rrf = resonant_response_formula(eq) if rrf !== nothing - return const_coeff_solve(to_homogeneous(eq)) + rrf + return rrf end rrf_trig = exp_trig_particular_solution(eq) if rrf_trig !== nothing - return const_coeff_solve(to_homogeneous(eq)) + rrf_trig + return rrf_trig end end end +""" +Returns homogeneous solutions to linear ODE `eq` with constant coefficients + +xₕ(t) = C₁e^(r₁t) + C₂e^(r₂t) + ... + Cₙe^(rₙt) +""" function const_coeff_solve(eq::LinearODE) @variables r p = characteristic_polynomial(eq, r) @@ -96,14 +156,14 @@ function const_coeff_solve(eq::LinearODE) solutions = exp.(roots * eq.t) for i in eachindex(solutions)[1:(end - 1)] j = i + 1 - + if imag(roots[i]) != 0 && roots[i] == conj(roots[j]) - solutions[i] = exp(real(roots[i]*eq.t))*cos(imag(roots[i]*eq.t)) - solutions[j] = exp(real(roots[i]*eq.t))*sin(imag(roots[i]*eq.t)) + solutions[i] = exp(real(roots[i] * eq.t)) * cos(imag(roots[i] * eq.t)) + solutions[j] = exp(real(roots[i] * eq.t)) * sin(imag(roots[i] * eq.t)) end while j <= length(solutions) && isequal(roots[i], roots[j]) - solutions[j] *= eq.t^(j - i) # multiply by t for each repetition + solutions[j] *= eq.t # multiply by t for each repetition j += 1 end end diff --git a/test/diffeqs.jl b/test/diffeqs.jl index c481f9ad7..5c8c48189 100644 --- a/test/diffeqs.jl +++ b/test/diffeqs.jl @@ -40,7 +40,7 @@ C = Symbolics.variables(:C, 1:5) ## first order @test isequal(symbolic_solve_ode(LinearODE(x, t, [5/t], 7t)), Symbolics.sympy_simplify(C[1]*t^(-5) + t^2)) @test isequal(symbolic_solve_ode(LinearODE(x, t, [cos(t)], cos(t))), 1 + C[1]*exp(-sin(t))) -@test isequal(symbolic_solve_ode(LinearODE(x, t, [-(1+t)], 1+t)), Symbolics.sympy_simplify(C[1]*exp((1//2)t^2 + t) - 1)) +@test isequal(symbolic_solve_ode(LinearODE(x, t, [-(1+t)], 1+t)), expand(Symbolics.sympy_simplify(C[1]*exp((1//2)t^2 + t) - 1))) # SymPy is being weird and not simplifying correctly (and some symbols are wrong, like pi and erf being syms), but these otherwise work @test_broken isequal(symbolic_solve_ode(LinearODE(x, t, [-2t], 1)), Symbolics.sympy_simplify(exp(t^2)*sqrt(Symbolics.variable(:pi))*erf(t)/2 + C[1]*exp(t^2))) @test_broken isequal(symbolic_solve_ode(LinearODE(x, t, [1], 2sin(t))), C[1]*exp(-t) + sin(t) - cos(t)) From 38c59a3493ee63a3a3be2e0231acd7b0077b29a5 Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Mon, 30 Jun 2025 20:28:58 -0400 Subject: [PATCH 13/52] added method of undetermined coefficients --- src/diffeqs/diffeqs.jl | 87 ++++++++++++++++++++++++++++++++++++++++-- test/diffeqs.jl | 6 ++- 2 files changed, 89 insertions(+), 4 deletions(-) diff --git a/src/diffeqs/diffeqs.jl b/src/diffeqs/diffeqs.jl index 901bf94d0..0fae30842 100644 --- a/src/diffeqs/diffeqs.jl +++ b/src/diffeqs/diffeqs.jl @@ -127,7 +127,7 @@ function find_particular_solution(eq::LinearODE) # if q has multiple terms, find a particular solution for each and sum together terms = Symbolics.terms(eq.q) if length(terms) != 1 - return sum(find_particular_solution.(terms)) + return sum(find_particular_solution(LinearODE(eq.x, eq.t, eq.p, term)) for term in terms) end if has_const_coeffs(eq) @@ -140,6 +140,11 @@ function find_particular_solution(eq::LinearODE) return rrf_trig end end + + undetermined_coeff = method_of_undetermined_coefficients(eq) + if undetermined_coeff !== nothing + return undetermined_coeff + end end """ @@ -187,8 +192,12 @@ function integrating_factor_solve(eq::LinearODE) else v = exp(sympy_integrate(p, eq.t)) end - return expand(Symbolics.sympy_simplify((1 / v) * ((isequal(eq.q, 0) ? 0 : - sympy_integrate(eq.q * v, eq.t)) + eq.C[1]))) + solution = (1 / v) * ((isequal(eq.q, 0) ? 0 : sympy_integrate(eq.q * v, eq.t)) + eq.C[1]) + @variables Integral + if !isempty(Symbolics.get_variables(solution, Integral)) + return nothing + end + return expand(Symbolics.sympy_simplify(solution)) end """ @@ -314,4 +323,76 @@ function resonant_response_formula(eq::LinearODE) return expand(simplify(a * exp(r * eq.t) * eq.t^k / (substitute(expand_derivatives((Ds^k)(p)), Dict(s => r))))) +end + +function method_of_undetermined_coefficients(eq::LinearODE) + # constant + p = eq.p[1] + if isempty(Symbolics.get_variables(p, eq.t)) && isempty(Symbolics.get_variables(eq.q, eq.t)) + return eq.q // p + end + + # polynomial + degree = Symbolics.degree(eq.q, eq.t) # just a starting point + a = Symbolics.variables(:a, 1:degree+1) + form = sum(a[n]*eq.t^(n-1) for n = 1:degree+1) + eq_subbed = substitute(get_expression(eq), Dict(eq.x => form)) + eq_subbed = expand_derivatives(eq_subbed) + try + coeff_solution = symbolic_solve(eq_subbed, length(a) == 1 ? a[1] : a) + catch + coeff_solution = nothing + end + if degree > 0 && coeff_solution !== nothing && !isempty(coeff_solution) + return substitute(form, coeff_solution[1]) + end + + # exponential + @variables a + coeff = get_rrf_coeff(eq.q, eq.t) + if coeff !== nothing + r = coeff[2] + form = a*exp(r*eq.t) + eq_subbed = substitute(get_expression(eq), Dict(eq.x => form)) + eq_subbed = expand_derivatives(eq_subbed) + @show coeff_solution = symbolic_solve(eq_subbed, a) + + if coeff_solution !== nothing && !isempty(coeff_solution) + return substitute(form, coeff_solution[1]) + end + end + + # sin and cos + # this is a hacky way of doing things + @variables a, b + @variables cs, sn + parsed = _parse_trig(factors(eq.q)[end], eq.t) + if parsed !== nothing + ω = parsed[1] + form = a*cos(ω*eq.t) + b*sin(ω*eq.t) + eq_subbed = substitute(get_expression(eq), Dict(eq.x => form)) + eq_subbed = expand_derivatives(eq_subbed) + eq_subbed = expand(substitute(eq_subbed.lhs - eq_subbed.rhs, Dict(cos(ω*eq.t)=>cs, sin(ω*eq.t)=>sn))) + cos_eq = simplify(sum(terms_with(eq_subbed, cs))/cs) + sin_eq = simplify(sum(terms_with(eq_subbed, sn))/sn) + if !isempty(Symbolics.get_variables(cos_eq, [eq.t,sn,cs])) || !isempty(Symbolics.get_variables(sin_eq, [eq.t,sn,cs])) + coeff_solution = nothing + else + coeff_solution = symbolic_solve([cos_eq, sin_eq], [a,b]) + end + + if coeff_solution !== nothing && !isempty(coeff_solution) + return substitute(form, coeff_solution[1]) + end + end +end + +function is_solution(solution, eq) + if solution === nothing + return false + end + + expr = substitute(get_expression(eq), Dict(eq.x => solution)) + @show expr = expand(expand_derivatives(expr.lhs - expr.rhs)) + return isequal(expr, 0) end \ No newline at end of file diff --git a/test/diffeqs.jl b/test/diffeqs.jl index 5c8c48189..db52329f7 100644 --- a/test/diffeqs.jl +++ b/test/diffeqs.jl @@ -1,5 +1,5 @@ using Symbolics -using Symbolics: solve_linear_system, LinearODE, is_homogeneous, has_const_coeffs, to_homogeneous, symbolic_solve_ode +using Symbolics: solve_linear_system, LinearODE, is_homogeneous, has_const_coeffs, to_homogeneous, symbolic_solve_ode, find_particular_solution import Groebner, Nemo, SymPy using Test @@ -58,3 +58,7 @@ C = Symbolics.variables(:C, 1:5) @test isequal(symbolic_solve_ode(LinearODE(x, t, [9, -6], 4exp(3t))), C[1]*exp(3t) + C[2]*t*exp(3t) + 2(t^2)*exp(3t)) ### trig functions @test isequal(symbolic_solve_ode(LinearODE(x, t, [6, 5], 2exp(-t)*cos(t))), C[1]*exp(-2t) + C[2]*exp(-3t) + (1//5)*exp(-t)*cos(t)+(3//5)*exp(-t)*sin(t)) + +## undetermined coefficients +@test isequal(symbolic_solve_ode(LinearODE(x, t, [-3, 2], 2t - 5)), C[1]exp(t) + C[2]exp(-3t) - (2//3)t + 11//9) +@test isequal(find_particular_solution(LinearODE(x, t, [1, 0], t^2)), t^2 - 2) \ No newline at end of file From 77f3994bb572cbb040945432a3d6d3714256e16f Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Tue, 1 Jul 2025 02:19:06 -0400 Subject: [PATCH 14/52] added expression parsing --- src/diffeqs/diffeqs.jl | 54 ++++++++++++++++++++++++++++++++++++++++++ test/diffeqs.jl | 7 +++++- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/src/diffeqs/diffeqs.jl b/src/diffeqs/diffeqs.jl index 0fae30842..49c0c0e06 100644 --- a/src/diffeqs/diffeqs.jl +++ b/src/diffeqs/diffeqs.jl @@ -33,6 +33,57 @@ struct LinearODE C::Vector{Num} LinearODE(x, t, p, q) = new(x, t, p, q, variables(:C, 1:length(p))) + + function LinearODE(expr, x, t) + if expr isa Equation + epxr = expr.lhs - expr.rhs + end + + expr = expand(simplify(expr)) + terms = Symbolics.terms(epxr) + p::Vector{Num} = [] + q = 0 + order = 0 + for term in terms + if isequal(Symbolics.get_variables(term, [x]), [x]) + facs = factors(term) + deriv = filter(fac -> Symbolics.hasderiv(Symbolics.value(fac)), facs) + p_n = prod(filter(fac -> !Symbolics.hasderiv(Symbolics.value(fac)), facs)) + if isempty(deriv) + p[1] = p_n/x + continue + end + + @assert length(deriv) == 1 "Expected linear term: $term" + n = _get_der_order(deriv[1], x, t) + if n+1 > length(p) + append!(p, zeros(Int, n-length(p) + 1)) + end + p[n + 1] = p_n + order = max(order, n) + + elseif isempty(Symbolics.get_variables(term, [x])) + q -= term + else + @error "Invalid term in LinearODE: $term" + end + end + + # normalize leading coefficient to 1 + leading_coeff = p[order + 1] + p = expand.(p .// leading_coeff) + q = expand(q // leading_coeff) + + new(x, t, p[1:order], q) + end +end + +function _get_der_order(expr, x, t) + if isequal(expr, x) + return 0 + end + + return _get_der_order(substitute(expr, Dict(Differential(t)(x) => x)), x, t) + 1 end Dt(eq::LinearODE) = Differential(eq.t) @@ -53,6 +104,9 @@ end Base.print(io::IO, eq::LinearODE) = print(io, string(eq)) Base.show(io::IO, eq::LinearODE) = print(io, eq) +Base.isequal(eq1::LinearODE, eq2::LinearODE) = + isequal(eq1.x, eq2.x) && isequal(eq1.t, eq2.t) && + isequal(eq1.p, eq2.p) && isequal(eq1.q, eq2.q) """Returns true if q(t) = 0 for linear ODE `eq`""" is_homogeneous(eq::LinearODE) = isempty(Symbolics.get_variables(eq.q)) diff --git a/test/diffeqs.jl b/test/diffeqs.jl index db52329f7..0d521a70f 100644 --- a/test/diffeqs.jl +++ b/test/diffeqs.jl @@ -61,4 +61,9 @@ C = Symbolics.variables(:C, 1:5) ## undetermined coefficients @test isequal(symbolic_solve_ode(LinearODE(x, t, [-3, 2], 2t - 5)), C[1]exp(t) + C[2]exp(-3t) - (2//3)t + 11//9) -@test isequal(find_particular_solution(LinearODE(x, t, [1, 0], t^2)), t^2 - 2) \ No newline at end of file +@test isequal(find_particular_solution(LinearODE(x, t, [1, 0], t^2)), t^2 - 2) + +# Parsing +Dt = Differential(t) +@test isequal(LinearODE(x, t, [1], 0), LinearODE(Dt(x) + x ~ 0, x, t)) +@test isequal(LinearODE(x, t, [sin(t), 0, 3t^2], exp(2t) + 2cos(t)), LinearODE(6t^2*(Dt^2)(x) + 2sin(t)*x - 2exp(2t) + 2(Dt^3)(x) ~ 4cos(t), x, t)) \ No newline at end of file From c1de7c18116299e494bf1338e6729f25059be2dd Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Tue, 1 Jul 2025 12:50:20 -0400 Subject: [PATCH 15/52] updated cases handled --- src/diffeqs/diffeqs.jl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/diffeqs/diffeqs.jl b/src/diffeqs/diffeqs.jl index 49c0c0e06..a2d991dcf 100644 --- a/src/diffeqs/diffeqs.jl +++ b/src/diffeqs/diffeqs.jl @@ -137,12 +137,14 @@ Symbolically solve a linear ODE Cases handled: - ☑ first order - ☑ homogeneous with constant coefficients -- ◩ particular solutions (variation of parameters? undetermined coefficients?) +- ◩ particular solutions - ☑ ERF + RRF - ☑ complex ERF + RRF to handle sin/cos + - ☑ method of undetermined coefficients + - ▢ variation of parameters - ▢ [Differential transform method](https://www.researchgate.net/publication/267767445_A_New_Algorithm_for_Solving_Linear_Ordinary_Differential_Equations) - ▢ Laplace Transform -- ▢ Expression parsing +- ☑ Expression parsing Uses methods: [`integrating_factor_solve`](@ref), [`find_homogeneous_solutions`](@ref), [`find_particular_solution`](@ref) From 11bd108e1b6af8bbf383cb1a10c4ef4a6a0a3a92 Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Tue, 1 Jul 2025 16:34:58 -0400 Subject: [PATCH 16/52] added IVP --- src/Symbolics.jl | 2 +- src/diffeqs/diffeqs.jl | 38 ++++++++++++++++++++++++++++++++++++++ test/diffeqs.jl | 8 ++++++-- 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/Symbolics.jl b/src/Symbolics.jl index ac146a7c9..c049b395b 100644 --- a/src/Symbolics.jl +++ b/src/Symbolics.jl @@ -221,7 +221,7 @@ export symbolic_solve # Diff Eq Solver include("diffeqs/diffeqs.jl") include("diffeqs/systems.jl") -export LinearODE, symbolic_solve_ode, solve_linear_system +export LinearODE, IVP, symbolic_solve_ode, solve_linear_system, solve_IVP # Sympy Functions diff --git a/src/diffeqs/diffeqs.jl b/src/diffeqs/diffeqs.jl index a2d991dcf..dd4b26110 100644 --- a/src/diffeqs/diffeqs.jl +++ b/src/diffeqs/diffeqs.jl @@ -451,4 +451,42 @@ function is_solution(solution, eq) expr = substitute(get_expression(eq), Dict(eq.x => solution)) @show expr = expand(expand_derivatives(expr.lhs - expr.rhs)) return isequal(expr, 0) +end + +""" +Initial value problem (IVP) for a linear ODE +""" +struct IVP + eq::LinearODE + initial_conditions::Vector{Num} # values at t = 0 of nth derivative of x + + function IVP(eq::LinearODE, initial_conditions::Vector{<:Number}) + @assert length(initial_conditions) == order(eq) "# of Initial conditions must match order of ODE" + new(eq, initial_conditions) + end +end + + +function solve_IVP(ivp::IVP) + general_solution = symbolic_solve_ode(ivp.eq) + if general_solution === nothing + return nothing + end + + eqs = [] + for i in eachindex(ivp.initial_conditions) + eq::Num = expand_derivatives((Dt(ivp.eq)^(i-1))(general_solution)) - ivp.initial_conditions[i] + + eq = substitute(eq, Dict(ivp.eq.t => 0), fold=false) + + # make sure exp, sin, and cos don't evaluate to floats + exp0 = substitute(exp(ivp.eq.t), Dict(ivp.eq.t => 0), fold=false) + sin0 = substitute(sin(ivp.eq.t), Dict(ivp.eq.t => 0), fold=false) + cos0 = substitute(cos(ivp.eq.t), Dict(ivp.eq.t => 0), fold=false) + + eq = expand(simplify(substitute(eq, Dict(exp0 => 1, sin0 => 0, cos0 => 1), fold=false))) + push!(eqs, eq) + end + + return expand(simplify(substitute(general_solution, symbolic_solve(eqs, ivp.eq.C)[1]))) end \ No newline at end of file diff --git a/test/diffeqs.jl b/test/diffeqs.jl index 0d521a70f..8664f22d8 100644 --- a/test/diffeqs.jl +++ b/test/diffeqs.jl @@ -1,5 +1,5 @@ using Symbolics -using Symbolics: solve_linear_system, LinearODE, is_homogeneous, has_const_coeffs, to_homogeneous, symbolic_solve_ode, find_particular_solution +using Symbolics: solve_linear_system, LinearODE, is_homogeneous, has_const_coeffs, to_homogeneous, symbolic_solve_ode, find_particular_solution, IVP, solve_IVP import Groebner, Nemo, SymPy using Test @@ -66,4 +66,8 @@ C = Symbolics.variables(:C, 1:5) # Parsing Dt = Differential(t) @test isequal(LinearODE(x, t, [1], 0), LinearODE(Dt(x) + x ~ 0, x, t)) -@test isequal(LinearODE(x, t, [sin(t), 0, 3t^2], exp(2t) + 2cos(t)), LinearODE(6t^2*(Dt^2)(x) + 2sin(t)*x - 2exp(2t) + 2(Dt^3)(x) ~ 4cos(t), x, t)) \ No newline at end of file +@test isequal(LinearODE(x, t, [sin(t), 0, 3t^2], exp(2t) + 2cos(t)), LinearODE(6t^2*(Dt^2)(x) + 2sin(t)*x - 2exp(2t) + 2(Dt^3)(x) ~ 4cos(t), x, t)) + +# IVP +@test isequal(solve_IVP(IVP(LinearODE(x, t, [-3, 2], 0), [1, -1])), (1//2)exp(-3t) + (1//2)exp(t)) +@test isequal(solve_IVP(IVP(LinearODE(x, t, [9, -6], 4exp(3t)), [5, 6])), 5exp(3t) - 9t*exp(3t) + 2(t^2)*exp(3t)) \ No newline at end of file From 4c5400eefb6a98045083ac8b85539f25698f09bf Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Thu, 3 Jul 2025 13:15:52 -0400 Subject: [PATCH 17/52] added method to solve Clairaut's equation --- src/diffeqs/diffeqs.jl | 68 ++++++++++++++++++++++++++++++++++++++++-- test/diffeqs.jl | 7 ++++- 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/src/diffeqs/diffeqs.jl b/src/diffeqs/diffeqs.jl index dd4b26110..f40463a70 100644 --- a/src/diffeqs/diffeqs.jl +++ b/src/diffeqs/diffeqs.jl @@ -65,7 +65,8 @@ struct LinearODE elseif isempty(Symbolics.get_variables(term, [x])) q -= term else - @error "Invalid term in LinearODE: $term" + # throw assertion error for invalid term so it can be easily caught + @assert false "Invalid term in LinearODE: $term" end end @@ -135,7 +136,9 @@ end Symbolically solve a linear ODE Cases handled: -- ☑ first order +- ☑ first order linear +- ☑ Clairaut's equation +- ◩ bernoulli equations - ☑ homogeneous with constant coefficients - ◩ particular solutions - ☑ ERF + RRF @@ -163,6 +166,23 @@ function symbolic_solve_ode(eq::LinearODE) return homogeneous_solutions + find_particular_solution(eq) end +function symbolic_solve_ode(expr::Equation, x, t) + if solve_clairaut(expr, x, t) !== nothing + return solve_clairaut(expr, x, t) + end + + try + eq = LinearODE(expr, x, t) + return symbolic_solve_ode(eq) + catch e + if e isa AssertionError + return nothing + else + throw(e) + end + end +end + """ Find homogeneous solutions of linear ODE `eq` with integration constants of `eq.C` @@ -489,4 +509,46 @@ function solve_IVP(ivp::IVP) end return expand(simplify(substitute(general_solution, symbolic_solve(eqs, ivp.eq.C)[1]))) -end \ No newline at end of file +end + +""" +Solve Clairaut's equation of the form x = x'*t + f(x'). + +Returns solution of the form x = C*t + f(C) where C is a constant. +""" +function solve_clairaut(expr, x, t) + Dt = Differential(t) + rhs = 0 + if isequal(expr.rhs, x) + rhs = expr.lhs + elseif isequal(expr.lhs, x) + rhs = expr.rhs + else + return nothing + end + + terms = Symbolics.terms(rhs) + matched = false # if expr contains term Dt(x)*t + f = 0 + for term in terms + if isequal(term, Dt(x)*t) + matched = true + elseif !isempty(Symbolics.get_variables(term, [t])) + return nothing + else + f += term + end + end + + if !matched + return nothing + end + + @variables C + f = substitute(f, Dict(Dt(x) => C)) + if !isempty(Symbolics.get_variables(f, [x])) + return nothing + end + + return C*t + f +end diff --git a/test/diffeqs.jl b/test/diffeqs.jl index 8664f22d8..2eb4bdc76 100644 --- a/test/diffeqs.jl +++ b/test/diffeqs.jl @@ -70,4 +70,9 @@ Dt = Differential(t) # IVP @test isequal(solve_IVP(IVP(LinearODE(x, t, [-3, 2], 0), [1, -1])), (1//2)exp(-3t) + (1//2)exp(t)) -@test isequal(solve_IVP(IVP(LinearODE(x, t, [9, -6], 4exp(3t)), [5, 6])), 5exp(3t) - 9t*exp(3t) + 2(t^2)*exp(3t)) \ No newline at end of file +@test isequal(solve_IVP(IVP(LinearODE(x, t, [9, -6], 4exp(3t)), [5, 6])), 5exp(3t) - 9t*exp(3t) + 2(t^2)*exp(3t)) + +# Other methods +@variables C +@test isequal(symbolic_solve_ode(x ~ Dt(x)*t - ((Dt(x))^3), x, t), C*t - C^3) +@test isequal(symbolic_solve_ode(x ~ Dt(x)*t + (Dt(x))^2 - sin(Dt(x)) + 2, x, t), C*t + C^2 - sin(C) + 2) \ No newline at end of file From e1a8fb06962ed87c8d5460dc4a4c002053997378 Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Thu, 3 Jul 2025 23:48:57 -0400 Subject: [PATCH 18/52] implemented solving bernoulli equations --- src/diffeqs/diffeqs.jl | 153 +++++++++++++++++++++++++++++++++++------ test/diffeqs.jl | 13 ++-- 2 files changed, 141 insertions(+), 25 deletions(-) diff --git a/src/diffeqs/diffeqs.jl b/src/diffeqs/diffeqs.jl index f40463a70..9dd203519 100644 --- a/src/diffeqs/diffeqs.jl +++ b/src/diffeqs/diffeqs.jl @@ -46,11 +46,16 @@ struct LinearODE order = 0 for term in terms if isequal(Symbolics.get_variables(term, [x]), [x]) - facs = factors(term) + facs = _true_factors(term) deriv = filter(fac -> Symbolics.hasderiv(Symbolics.value(fac)), facs) p_n = prod(filter(fac -> !Symbolics.hasderiv(Symbolics.value(fac)), facs)) if isempty(deriv) - p[1] = p_n/x + @assert Symbolics.degree(term, x) == 1 "Expected linear term: $term" + if isempty(p) + p = [p_n/x] + else + p[1] = p_n/x + end continue end @@ -153,22 +158,31 @@ Uses methods: [`integrating_factor_solve`](@ref), [`find_homogeneous_solutions`] """ function symbolic_solve_ode(eq::LinearODE) - if order(eq) == 1 - return integrating_factor_solve(eq) - end - homogeneous_solutions = find_homogeneous_solutions(eq) - - if is_homogeneous(eq) + + if is_homogeneous(eq) && homogeneous_solutions !== nothing return homogeneous_solutions end - - return homogeneous_solutions + find_particular_solution(eq) + + particular_solution = find_particular_solution(eq) + if homogeneous_solutions !== nothing && particular_solution !== nothing + return homogeneous_solutions + particular_solution + end + + if order(eq) == 1 + return integrating_factor_solve(eq) + end end function symbolic_solve_ode(expr::Equation, x, t) - if solve_clairaut(expr, x, t) !== nothing - return solve_clairaut(expr, x, t) + clairaut = solve_clairaut(expr, x, t) + if clairaut !== nothing + return clairaut + end + + bernoulli = solve_bernoulli(expr, x, t) + if bernoulli !== nothing + return bernoulli end try @@ -176,6 +190,7 @@ function symbolic_solve_ode(expr::Equation, x, t) return symbolic_solve_ode(eq) catch e if e isa AssertionError + @warn e return nothing else throw(e) @@ -203,7 +218,11 @@ function find_particular_solution(eq::LinearODE) # if q has multiple terms, find a particular solution for each and sum together terms = Symbolics.terms(eq.q) if length(terms) != 1 - return sum(find_particular_solution(LinearODE(eq.x, eq.t, eq.p, term)) for term in terms) + solutions = find_particular_solution.(LinearODE.(Ref(eq.x), Ref(eq.t), Ref(eq.p), terms)) + if any(s -> s === nothing, solutions) + return nothing + end + return sum(solutions) end if has_const_coeffs(eq) @@ -280,7 +299,7 @@ end Returns a, r from q(t)=a*e^(rt) if it is of that form. If not, returns `nothing` """ function get_rrf_coeff(q, t) - facs = factors(q) + facs = _true_factors(q) # handle complex r # very convoluted, could probably be improved (possibly by making heavier use of @rule) @@ -338,7 +357,7 @@ end For finding particular solution when q(t) = a*e^(rt)*cos(bt) (or sin(bt)) """ function exp_trig_particular_solution(eq::LinearODE) - facs = factors(eq.q) + facs = _true_factors(eq.q) a = prod(filter(fac -> isempty(Symbolics.get_variables(fac, [eq.t])), facs)) @@ -431,7 +450,7 @@ function method_of_undetermined_coefficients(eq::LinearODE) form = a*exp(r*eq.t) eq_subbed = substitute(get_expression(eq), Dict(eq.x => form)) eq_subbed = expand_derivatives(eq_subbed) - @show coeff_solution = symbolic_solve(eq_subbed, a) + coeff_solution = symbolic_solve(eq_subbed, a) if coeff_solution !== nothing && !isempty(coeff_solution) return substitute(form, coeff_solution[1]) @@ -442,15 +461,15 @@ function method_of_undetermined_coefficients(eq::LinearODE) # this is a hacky way of doing things @variables a, b @variables cs, sn - parsed = _parse_trig(factors(eq.q)[end], eq.t) + parsed = _parse_trig(_true_factors(eq.q)[end], eq.t) if parsed !== nothing ω = parsed[1] form = a*cos(ω*eq.t) + b*sin(ω*eq.t) eq_subbed = substitute(get_expression(eq), Dict(eq.x => form)) eq_subbed = expand_derivatives(eq_subbed) eq_subbed = expand(substitute(eq_subbed.lhs - eq_subbed.rhs, Dict(cos(ω*eq.t)=>cs, sin(ω*eq.t)=>sn))) - cos_eq = simplify(sum(terms_with(eq_subbed, cs))/cs) - sin_eq = simplify(sum(terms_with(eq_subbed, sn))/sn) + cos_eq = simplify(sum(filter(term -> !isempty(Symbolics.get_variables(term, cs)), terms(eq_subbed)))/cs) + sin_eq = simplify(sum(filter(term -> !isempty(Symbolics.get_variables(term, sn)), terms(eq_subbed)))/sn) if !isempty(Symbolics.get_variables(cos_eq, [eq.t,sn,cs])) || !isempty(Symbolics.get_variables(sin_eq, [eq.t,sn,cs])) coeff_solution = nothing else @@ -469,7 +488,7 @@ function is_solution(solution, eq) end expr = substitute(get_expression(eq), Dict(eq.x => solution)) - @show expr = expand(expand_derivatives(expr.lhs - expr.rhs)) + expr = expand(expand_derivatives(expr.lhs - expr.rhs)) return isequal(expr, 0) end @@ -544,7 +563,7 @@ function solve_clairaut(expr, x, t) return nothing end - @variables C + C = Symbolics.variable(:C, 1) # constant of integration f = substitute(f, Dict(Dt(x) => C)) if !isempty(Symbolics.get_variables(f, [x])) return nothing @@ -552,3 +571,95 @@ function solve_clairaut(expr, x, t) return C*t + f end + +""" +Linearize a Bernoulli equation of the form dx/dt + p(t)x = q(t)x^n into a `LinearODE` of the form dv/dt + (1-n)p(t)v = (1-n)q(t) where v = x^(1-n) +""" +function linearize_bernoulli(expr, x, t, v) + Dt = Differential(t) + + if expr isa Equation + expr = expr.lhs - expr.rhs + end + + terms = Symbolics.terms(expr) + + p = 0 + q = 0 + n = 0 + leading_coeff = 1 + for term in terms + if Symbolics.hasderiv(Symbolics.value(term)) + facs = _true_factors(term) + leading_coeff = prod(filter(fac -> !Symbolics.hasderiv(Symbolics.value(fac)), facs)) + @assert _get_der_order(term//leading_coeff, x, t) == 1 "Expected linear term in $term" + elseif !isempty(Symbolics.get_variables(term, [x])) + facs = _true_factors(term) + x_fac = filter(fac -> !isempty(Symbolics.get_variables(fac, [x])), facs) + @assert length(x_fac) == 1 "Expected linear term in $term" + + if isequal(x_fac[1], x) + p = prod(filter(fac -> isempty(Symbolics.get_variables(fac, [x])), facs)) + else + n = degree(x_fac[1]) + q = -prod(filter(fac -> isempty(Symbolics.get_variables(fac, [x])), facs)) + end + end + end + + p //= leading_coeff + q //= leading_coeff + + return LinearODE(v, t, [p*(1-n)], q*(1-n)), n +end + +""" +Solve Bernoulli equations of the form dx/dt + p(t)x = q(t)x^n +""" +function solve_bernoulli(expr, x, t) + @variables v + eq, n = linearize_bernoulli(expr, x, t, v) + + solution = symbolic_solve_ode(eq) + if solution === nothing + return nothing + end + + return simplify(solution^(1//(1-n))) +end + +""" +Solve Bernoulli equations of the form dx/dt + p(t)x = q(t)x^n with initial condition x(0) = x0 +""" +function solve_bernoulli(expr, x, t, x0) + @variables v + eq, n = linearize_bernoulli(expr, x, t, v) + + v0 = x0^(1-n) # convert initial condition from x(0) to v(0) + + ivp = IVP(eq, [v0]) + solution = solve_IVP(ivp) + if solution === nothing + return nothing + end + + return symbolic_solve(solution ~ x^(1-n), x) +end + +# takes into account fractions +function _true_factors(expr) + facs = factors(expr) + true_facs::Vector{Number} = [] + frac_rule = @rule (~x)/(~y) => [~x, 1/~y] + for fac in facs + frac = frac_rule(fac) + if frac !== nothing && !isequal(frac[1], 1) + append!(true_facs, _true_factors(frac[1])) + append!(true_facs, _true_factors(frac[2])) + else + push!(true_facs, fac) + end + end + + return true_facs +end \ No newline at end of file diff --git a/test/diffeqs.jl b/test/diffeqs.jl index 2eb4bdc76..cd4700282 100644 --- a/test/diffeqs.jl +++ b/test/diffeqs.jl @@ -43,7 +43,7 @@ C = Symbolics.variables(:C, 1:5) @test isequal(symbolic_solve_ode(LinearODE(x, t, [-(1+t)], 1+t)), expand(Symbolics.sympy_simplify(C[1]*exp((1//2)t^2 + t) - 1))) # SymPy is being weird and not simplifying correctly (and some symbols are wrong, like pi and erf being syms), but these otherwise work @test_broken isequal(symbolic_solve_ode(LinearODE(x, t, [-2t], 1)), Symbolics.sympy_simplify(exp(t^2)*sqrt(Symbolics.variable(:pi))*erf(t)/2 + C[1]*exp(t^2))) -@test_broken isequal(symbolic_solve_ode(LinearODE(x, t, [1], 2sin(t))), C[1]*exp(-t) + sin(t) - cos(t)) +@test isequal(symbolic_solve_ode(LinearODE(x, t, [1], 2sin(t))), C[1]*exp(-t) + sin(t) - cos(t)) ## repeated characteristic roots @test isequal(symbolic_solve_ode(LinearODE(x, t, [1, 2], 0)), C[1]*exp(-t) + C[2]*t*exp(-t)) @@ -73,6 +73,11 @@ Dt = Differential(t) @test isequal(solve_IVP(IVP(LinearODE(x, t, [9, -6], 4exp(3t)), [5, 6])), 5exp(3t) - 9t*exp(3t) + 2(t^2)*exp(3t)) # Other methods -@variables C -@test isequal(symbolic_solve_ode(x ~ Dt(x)*t - ((Dt(x))^3), x, t), C*t - C^3) -@test isequal(symbolic_solve_ode(x ~ Dt(x)*t + (Dt(x))^2 - sin(Dt(x)) + 2, x, t), C*t + C^2 - sin(C) + 2) \ No newline at end of file + +## Clairaut's equation +@test isequal(symbolic_solve_ode(x ~ Dt(x)*t - ((Dt(x))^3), x, t), C[1]*t - C[1]^3) +@test isequal(symbolic_solve_ode(x ~ Dt(x)*t + (Dt(x))^2 - sin(Dt(x)) + 2, x, t), C[1]*t + C[1]^2 - sin(C[1]) + 2) + +## Bernoulli equations +@test isequal(symbolic_solve_ode(Dt(x) - 5x ~ exp(-2t)*x^(-2), x, t), (C[1]exp(15t) - (3//17)exp(-2t))^(1//3)) +@test isequal(symbolic_solve_ode(Dt(x) + (4//t)*x ~ t^3 * x^2, x, t), 1/(C[1]t^4 - t^4 * log(t))) \ No newline at end of file From 4d04477dc5ab258fd30a10b81d58398f1f235581 Mon Sep 17 00:00:00 2001 From: Tori Date: Tue, 8 Jul 2025 12:18:41 -0400 Subject: [PATCH 19/52] WIP laplace --- src/diffeqs/laplace.jl | 68 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 src/diffeqs/laplace.jl diff --git a/src/diffeqs/laplace.jl b/src/diffeqs/laplace.jl new file mode 100644 index 000000000..5536b9de8 --- /dev/null +++ b/src/diffeqs/laplace.jl @@ -0,0 +1,68 @@ +using Symbolics +import Symbolics: value, coeff, sympy_integrate + +function laplace(f, t, s) + # from https://tutorial.math.lamar.edu/Classes/DE/Laplace_Table.aspx + transform_rules = [ + @rule 1 => 1/s + @rule exp(t) => 1/(s - 1) + @rule exp(~a * t) => 1/(s - ~a) + @rule t => 1/s^2 + @rule t^~n => factorial(~n)/s^(~n + 1) + @rule sqrt(t) => sqrt(pi)/(2 * s^(3/2)) + @rule sin(t) => 1/(s^2 + 1) + @rule sin(~a * t) => ~a/(s^2 + ~a^2) + @rule cos(t) => s/(s^2 + 1) + @rule cos(~a * t) => s/(s^2 + ~a^2) + @rule t*sin(t) => 1/(s^2 + 1)^2 + @rule t*sin(~a * t) => 2*~a*s / (s^2 + ~a^2)^2 + @rule t*cos(t) => (s^2 - 1) / (s^2 + 1)^2 + @rule t*cos(~a * t) => (s^2 - ~a^2) / (s^2 + ~a^2)^2 + @rule sin(t) - t*cos(t) => 2 / (s^2 + 1)^2 + @rule sin(~a*t) - ~a*t*cos(~a*t) => 2*~a^3 / (s^2 + ~a^2)^2 + @rule sin(t) + t*cos(t) => 2s^2 / (s^2 + 1)^2 + @rule sin(~a*t) + ~a*t*cos(~a*t) => 2*~a*s^2 / (s^2 + ~a^2)^2 + @rule cos(~a*t) - ~a*t*sin(~a*t) => s*(s^2 + ~a^2) / (s^2 + ~a^2)^2 + @rule cos(~a*t) + ~a*t*sin(~a*t) => s*(s^2 + 3*~a^2) / (s^2 + ~a^2)^2 + @rule sin(~a*t + ~b) => (s*sin(~b) + ~a*cos(~b)) / (s^2 + ~a^2) + @rule cos(~a*t + ~b) => (s*cos(~b) - ~a*sin(~b)) / (s^2 + ~a^2) + @rule sinh(~a * t) => ~a/(s^2 - ~a^2) + @rule cosh(~a * t) => s/(s^2 - ~a^2) + @rule exp(~a*t) * sin(~b * t) => ~b / ((s-~a)^2 + ~b^2) + @rule exp(~a*t) * cos(~b * t) => (s-~a) / ((s-~a)^2 + ~b^2) + @rule exp(~a*t) * sinh(~b * t) => ~b / ((s-~a)^2 - ~b^2) + @rule exp(~a*t) * cosh(~b * t) => (s-~a) / ((s-~a)^2 - ~b^2) + @rule t^~n * exp(~a * t) => factorial(~n) / (s - ~a)^(~n + 1) + @rule exp(~c*t) * ~g => laplace(~g, t, s - ~c) + ] + + + + return sympy_integrate(f * exp(-s * t), (t, 0, Inf)) +end + +function inverse_laplace(F, t, s) + inverse_transform_rules = [ + @rule 1/s => 1 + @rule 1/(s + ~a) => exp(~a * t) + @rule factorial(~n)/s^(~n + 1) => t^~n + @rule sqrt(pi)/(2 * s^(3/2)) => sqrt(t) + @rule ~a/(s^2 + ~a^2) => sin(~a * t) + @rule s/(s^2 + ~a^2) => cos(~a * t) + @rule 2*~a*s / (s^2 + ~a^2)^2 => t*sin(~a * t) + @rule (s^2 - ~a^2) / (s^2 + ~a^2)^2 => t*cos(~a * t) + @rule 2*~a^3 / (s^2 + ~a^2)^2 => sin(~a*t) - ~a*t*cos(~a*t) + @rule 2*~a*s^2 / (s^2 + ~a^2)^2 => sin(~a*t) + ~a*t*cos(~a*t) + @rule s*(s^2 + ~a^2) / (s^2 + ~a^2)^2 => cos(~a*t) - ~a*t*sin(~a*t) + @rule s*(s^2 + 3*~a^2) / (s^2 + ~a^2)^2 => cos(~a*t) + ~a*t*sin(~a*t) + @rule (s*sin(~b) + ~a*cos(~b)) / (s^2 + ~a^2) => sin(~a*t + ~b) + @rule (s*cos(~b) - ~a*sin(~b)) / (s^2 + ~a^2) => cos(~a*t + ~b) + @rule ~b/(s^2 - ~b^2) => sinh(~b * t) + @rule s/(s^2 - ~b^2) => cosh(~b * t) + @rule ~b / ((s-~c)^2 + ~b^2) => exp(~c*t) * sin(~b * t) + @rule (s-~c) / ((s-~c)^2 + ~b^2) => exp(~c*t) * cos(~b * t) + @rule ~b / ((s-~c)^2 - ~b^2) => exp(~c*t) * sinh(~b * t) + @rule (s-~c) / ((s-~c)^2 - ~b^2) => exp(~c*t) * cosh(~b * t) + @rule factorial(~n) / (s - ~a)^(~n + 1) => t^~n * exp(~a * t) + ] +end \ No newline at end of file From 3c2ad4b8856c6a932a3c8585c6b11c8cc7613c83 Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Wed, 16 Jul 2025 15:25:48 -0400 Subject: [PATCH 20/52] implemented laplace transform and wip inverse transform --- src/diffeqs/laplace.jl | 209 ++++++++++++++++++++++++++++++----------- 1 file changed, 152 insertions(+), 57 deletions(-) diff --git a/src/diffeqs/laplace.jl b/src/diffeqs/laplace.jl index 5536b9de8..516c0c692 100644 --- a/src/diffeqs/laplace.jl +++ b/src/diffeqs/laplace.jl @@ -1,68 +1,163 @@ using Symbolics -import Symbolics: value, coeff, sympy_integrate +import Symbolics: get_variables, coeff, sympy_integrate -function laplace(f, t, s) +function laplace(expr, f, t, s, F) + Dt = Differential(t) + Ds = Differential(s) # from https://tutorial.math.lamar.edu/Classes/DE/Laplace_Table.aspx - transform_rules = [ + transform_rules = Symbolics.Chain([ @rule 1 => 1/s @rule exp(t) => 1/(s - 1) - @rule exp(~a * t) => 1/(s - ~a) + @rule exp(~a * t) => 1/(-~a + s) @rule t => 1/s^2 @rule t^~n => factorial(~n)/s^(~n + 1) @rule sqrt(t) => sqrt(pi)/(2 * s^(3/2)) - @rule sin(t) => 1/(s^2 + 1) - @rule sin(~a * t) => ~a/(s^2 + ~a^2) - @rule cos(t) => s/(s^2 + 1) - @rule cos(~a * t) => s/(s^2 + ~a^2) - @rule t*sin(t) => 1/(s^2 + 1)^2 - @rule t*sin(~a * t) => 2*~a*s / (s^2 + ~a^2)^2 - @rule t*cos(t) => (s^2 - 1) / (s^2 + 1)^2 - @rule t*cos(~a * t) => (s^2 - ~a^2) / (s^2 + ~a^2)^2 - @rule sin(t) - t*cos(t) => 2 / (s^2 + 1)^2 - @rule sin(~a*t) - ~a*t*cos(~a*t) => 2*~a^3 / (s^2 + ~a^2)^2 - @rule sin(t) + t*cos(t) => 2s^2 / (s^2 + 1)^2 - @rule sin(~a*t) + ~a*t*cos(~a*t) => 2*~a*s^2 / (s^2 + ~a^2)^2 - @rule cos(~a*t) - ~a*t*sin(~a*t) => s*(s^2 + ~a^2) / (s^2 + ~a^2)^2 - @rule cos(~a*t) + ~a*t*sin(~a*t) => s*(s^2 + 3*~a^2) / (s^2 + ~a^2)^2 - @rule sin(~a*t + ~b) => (s*sin(~b) + ~a*cos(~b)) / (s^2 + ~a^2) - @rule cos(~a*t + ~b) => (s*cos(~b) - ~a*sin(~b)) / (s^2 + ~a^2) - @rule sinh(~a * t) => ~a/(s^2 - ~a^2) - @rule cosh(~a * t) => s/(s^2 - ~a^2) - @rule exp(~a*t) * sin(~b * t) => ~b / ((s-~a)^2 + ~b^2) - @rule exp(~a*t) * cos(~b * t) => (s-~a) / ((s-~a)^2 + ~b^2) - @rule exp(~a*t) * sinh(~b * t) => ~b / ((s-~a)^2 - ~b^2) - @rule exp(~a*t) * cosh(~b * t) => (s-~a) / ((s-~a)^2 - ~b^2) - @rule t^~n * exp(~a * t) => factorial(~n) / (s - ~a)^(~n + 1) - @rule exp(~c*t) * ~g => laplace(~g, t, s - ~c) - ] - - - - return sympy_integrate(f * exp(-s * t), (t, 0, Inf)) + @rule sin(t) => 1/(1 + s^2) + @rule sin(~a * t) => ~a/((~a)^2 + s^2) + @rule cos(t) => s/(1 + s^2) + @rule cos(~a * t) => s/((~a)^2 + s^2) + @rule t*sin(t) => 1/(1 + s^2)^2 + @rule t*sin(~a * t) => 2*~a*s / ((~a)^2 + s^2)^2 + @rule t*cos(t) => (s^2 - 1) / (1 + s^2)^2 + @rule t*cos(~a * t) => (-(~a)^2 + s^2) / ((~a)^2 + s^2)^2 + @rule sin(t) - t*cos(t) => 2 / (1 + s^2)^2 + @rule sin(~a*t) - ~a*t*cos(~a*t) => 2*(~a)^3 / ((~a)^2 + s^2)^2 + @rule sin(t) + t*cos(t) => 2s^2 / (1 + s^2)^2 + @rule sin(~a*t) + ~a*t*cos(~a*t) => 2*~a*s^2 / ((~a)^2 + s^2)^2 + @rule cos(~a*t) - ~a*t*sin(~a*t) => s*((~a)^2 + s^2) / ((~a)^2 + s^2)^2 + @rule cos(~a*t) + ~a*t*sin(~a*t) => s*(s^2 + 3*(~a)^2) / ((~a)^2 + s^2)^2 + @rule sin(~b + ~a*t) => (s*sin(~b) + ~a*cos(~b)) / ((~a)^2 + s^2) + @rule cos(~b + ~a*t) => (s*cos(~b) - ~a*sin(~b)) / ((~a)^2 + s^2) + @rule sinh(~a * t) => ~a/(-(~a)^2 + s^2) + @rule cosh(~a * t) => s/(-(~a)^2 + s^2) + @rule exp(~a*t) * sin(~b * t) => ~b / ((~b)^2 + (-~a+s)^2) + @rule exp(~a*t) * cos(~b * t) => (-~a+s) / ((~b)^2 + (-~a+s)^2) + @rule exp(~a*t) * sinh(~b * t) => ~b / (-(~b)^2 + (-~a+s)^2) + @rule exp(~a*t) * cosh(~b * t) => (-~a+s) / (-(~b)^2 + (-~a+s)^2) + @rule t^~n * exp(~a * t) => factorial(~n) / (-~a + s)^(~n + 1) + @rule exp(~c*t) * ~g => laplace(~g, f, t, s - ~c, F) # s-shift rule + @rule t*f(t) => -Ds(F(s)) # s-derivative rule + @rule t^(~n)*f(t) => (-1)^(~n) * (Ds^~n)(F(s)) # s-derivative rule + @rule f(~a + t) => exp(~a*s)*F(s) # t-shift rule + @rule f(t) => F(s) + ]) + + transformed = transform_rules(expr) + if !isequal(transformed, expr) + return transformed + end + + # t-derivative rule + n, expr = unwrap_der(expr, Dt) + if n != 0 && isequal(expr, f(t)) + f0 = Symbolics.variables(:f0, 0:(n-1)) + transformed = s^n*F(s) + for i = 1:n + transformed -= s^(n-i)*f0[i] + end + + return transformed + end + + terms = Symbolics.terms(expr) + result = 0 + if length(terms) == 1 && length(filter(x->isempty(Symbolics.get_variables(x)), _true_factors(terms[1]))) == 0 + return Integral(t in 0..Inf)(expr*exp(-s*t)) + end + for term in terms + factors = _true_factors(term) + constant = filter(x -> isempty(Symbolics.get_variables(x)), factors) + if !isempty(constant) + result += laplace(term / constant[1], f, t, s, F) * constant[1] + else + result += laplace(term, f, t, s, F) + end + end + + return result end -function inverse_laplace(F, t, s) - inverse_transform_rules = [ +function laplace(expr::Equation, f, t, s, F) + return laplace(expr.lhs, f, t, s, F) ~ laplace(expr.rhs, f, t, s, F) +end + +function inverse_laplace(expr, F, t, s, f) + inverse_transform_rules = Symbolics.Chain([ @rule 1/s => 1 - @rule 1/(s + ~a) => exp(~a * t) - @rule factorial(~n)/s^(~n + 1) => t^~n - @rule sqrt(pi)/(2 * s^(3/2)) => sqrt(t) - @rule ~a/(s^2 + ~a^2) => sin(~a * t) - @rule s/(s^2 + ~a^2) => cos(~a * t) - @rule 2*~a*s / (s^2 + ~a^2)^2 => t*sin(~a * t) - @rule (s^2 - ~a^2) / (s^2 + ~a^2)^2 => t*cos(~a * t) - @rule 2*~a^3 / (s^2 + ~a^2)^2 => sin(~a*t) - ~a*t*cos(~a*t) - @rule 2*~a*s^2 / (s^2 + ~a^2)^2 => sin(~a*t) + ~a*t*cos(~a*t) - @rule s*(s^2 + ~a^2) / (s^2 + ~a^2)^2 => cos(~a*t) - ~a*t*sin(~a*t) - @rule s*(s^2 + 3*~a^2) / (s^2 + ~a^2)^2 => cos(~a*t) + ~a*t*sin(~a*t) - @rule (s*sin(~b) + ~a*cos(~b)) / (s^2 + ~a^2) => sin(~a*t + ~b) - @rule (s*cos(~b) - ~a*sin(~b)) / (s^2 + ~a^2) => cos(~a*t + ~b) - @rule ~b/(s^2 - ~b^2) => sinh(~b * t) - @rule s/(s^2 - ~b^2) => cosh(~b * t) - @rule ~b / ((s-~c)^2 + ~b^2) => exp(~c*t) * sin(~b * t) - @rule (s-~c) / ((s-~c)^2 + ~b^2) => exp(~c*t) * cos(~b * t) - @rule ~b / ((s-~c)^2 - ~b^2) => exp(~c*t) * sinh(~b * t) - @rule (s-~c) / ((s-~c)^2 - ~b^2) => exp(~c*t) * cosh(~b * t) - @rule factorial(~n) / (s - ~a)^(~n + 1) => t^~n * exp(~a * t) - ] -end \ No newline at end of file + @rule 1/(~a + s) => exp(~a * t) + @rule 1/s^(~n) => t^(~n-1) / factorial(~n-1) + @rule 1/(2 * s^(3/2)) => sqrt(t)/sqrt(pi) + @rule 1/((~a)^2 + s^2) => sin(~a * t)/~a + @rule s/((~a)^2 + s^2) => cos(~a * t) + @rule s / ((~a)^2 + s^2)^2 => t*sin(~a * t)/(2*~a) + @rule (-(~a)^2 + s^2) / ((~a)^2 + s^2)^2 => t*cos(~a * t) + @rule 1 / ((~a)^2 + s^2)^2 => (sin(~a*t) - ~a*t*cos(~a*t))/ (2*(~a)^3) + @rule s^2 / ((~a)^2 + s^2)^2 => (sin(~a*t) + ~a*t*cos(~a*t)) / (2*~a) + @rule s*((~a)^2 + s^2) / ((~a)^2 + s^2)^2 => cos(~a*t) - ~a*t*sin(~a*t) + @rule s*(3*(~a)^2 + s^2) / ((~a)^2 + s^2)^2 => cos(~a*t) + ~a*t*sin(~a*t) + @rule (s*sin(~b) + ~a*cos(~b)) / ((~a)^2 + s^2) => sin(~b + ~a*t) + @rule (s*cos(~b) - ~a*sin(~b)) / ((~a)^2 + s^2) => cos(~b + ~a*t) + @rule 1/(s^2 - (~b)^2) => sinh(~b * t)/~b + @rule s/(s^2 - (~b)^2) => cosh(~b * t) + @rule 1 / ((~c+s)^2 + (~b)^2) => exp(-~c*t) * sin(~b * t) / ~b + @rule (~c+s) / ((~c+s)^2 + (~b)^2) => exp(-~c*t) * cos(~b * t) + @rule 1 / ((~c+s)^2 - (~b)^2) => exp(-~c*t) * sinh(~b * t) / ~b + @rule (~c+s) / ((~c+s)^2 - (~b)^2) => exp(-~c*t) * cosh(~b * t) + @rule 1 / (~a + s)^(~n) => t^(~n-1) * exp(-~a * t) / factorial(~n-1) + ]) + + transformed = inverse_transform_rules(expr) + if !isequal(transformed, expr) + return transformed + end + + terms = Symbolics.terms(expr) + result = 0 + if length(terms) == 1 && length(_true_factors(terms[1])) == 1 + return f + end + for term in terms + factors = _true_factors(term) + constant = filter(x -> isempty(Symbolics.get_variables(x)), factors) + if !isempty(constant) + result += inverse_laplace(term / constant[1], t, s) * constant[1] + else + result += inverse_laplace(term, t, s) + end + end + + return result +end + +function inverse_laplace(expr::Equation, F, t, s, f) + return inverse_laplace(expr.lhs, F, t, s, f) ~ inverse_laplace(expr.rhs, F, t, s, f) +end + +function unwrap_der(expr, Dt) + reduce_rule = @rule Dt(~x) => ~x + + if reduce_rule(expr) === nothing + return 0, expr + end + + order, expr = unwrap_der(reduce_rule(expr), Dt) + return order + 1, expr +end + +# takes into account fractions +function _true_factors(expr) + facs = Symbolics.factors(expr) + true_facs::Vector{Union{Number, Symbolics.BasicSymbolic}} = [] + frac_rule = @rule (~x)/(~y) => [~x, 1/~y] + for fac in facs + frac = frac_rule(fac) + if frac !== nothing && !isequal(frac[1], 1) + append!(true_facs, _true_factors(frac[1])) + append!(true_facs, _true_factors(frac[2])) + else + push!(true_facs, fac) + end + end + + return true_facs +end From 5812be63b5017cc048643bb168342c1f5b05dfb5 Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Thu, 17 Jul 2025 01:09:14 -0400 Subject: [PATCH 21/52] WIP partial fraction decomposition --- src/partialfractions.jl | 63 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 src/partialfractions.jl diff --git a/src/partialfractions.jl b/src/partialfractions.jl new file mode 100644 index 000000000..2e30faf87 --- /dev/null +++ b/src/partialfractions.jl @@ -0,0 +1,63 @@ +# https://dl.acm.org/doi/pdf/10.1145/800204.806314 +function partial_frac_decompose(expr) + frac_rule = @rule ~a/~b => (~a, ~b) + A, B = frac_rule(expr) + + X = [] + Z = [] + b, Y = factor_use_nemo(B) + Y = expand_factor_list(Y) + k = length(Y) + + if k == 1 + return [A], [B], [1] + end + + # idk what this is for + # for i = 1:k-1 + # if degree(B[i]) == 0 + # push!(X, A) + # push!(Z, b) + # for j = 1:k-1 + # push!(X, 0) + # push!(Z, 1) + # end + # return X, Y, Z + # end + # end + + E = 1 # TODO + F = 1 # TODO + + G = F*E^(-1) # TODO solve EG = F for G + + w = b * G[0] + + for i = 1:k + m = degree(B[i]) + + if m == 0 + push!(X, 0) + push!(Z, 1) + continue + end + + n = i*m + + end +end + +function expand_factor_list(facs) + result = [] + expand_rule = @rule ~a^~n => fill(~a, ~n) + for fac in facs + expanded = expand_rule(fac) + if expanded === nothing + push!(result, fac) + else + append!(result, expanded) + end + end + + return result +end \ No newline at end of file From d6000f6e69e1d67bf504023a06c246edd8c58bc6 Mon Sep 17 00:00:00 2001 From: Tori Date: Thu, 17 Jul 2025 12:20:06 -0400 Subject: [PATCH 22/52] working on algorithm more (possibly complete RSQDEC, but with bugs) --- src/Symbolics.jl | 3 ++ src/partialfractions.jl | 64 +++++++++++++++++++++++++++------------- test/partialfractions.jl | 10 +++++++ 3 files changed, 57 insertions(+), 20 deletions(-) create mode 100644 test/partialfractions.jl diff --git a/src/Symbolics.jl b/src/Symbolics.jl index f192eb77c..258172302 100644 --- a/src/Symbolics.jl +++ b/src/Symbolics.jl @@ -173,6 +173,9 @@ include("operators.jl") include("limits.jl") export limit +include("partialfractions.jl") +export partial_frac_decomposition + # Hacks to make wrappers "nicer" const NumberTypes = Union{AbstractFloat,Integer,Complex{<:AbstractFloat},Complex{<:Integer}} (::Type{T})(x::SymbolicUtils.Symbolic) where {T<:NumberTypes} = throw(ArgumentError("Cannot convert Sym to $T since Sym is symbolic and $T is concrete. Use `substitute` to replace the symbolic unwraps.")) diff --git a/src/partialfractions.jl b/src/partialfractions.jl index 2e30faf87..ba375d45f 100644 --- a/src/partialfractions.jl +++ b/src/partialfractions.jl @@ -1,16 +1,16 @@ # https://dl.acm.org/doi/pdf/10.1145/800204.806314 -function partial_frac_decompose(expr) +function partial_frac_decomposition(expr, x) frac_rule = @rule ~a/~b => (~a, ~b) A, B = frac_rule(expr) X = [] Z = [] - b, Y = factor_use_nemo(B) - Y = expand_factor_list(Y) + b = gcd(coeff_vector(B, x)...) + Y = x .- symbolic_solve(B, x, dropmultiplicity=false) k = length(Y) if k == 1 - return [A], [B], [1] + return expr end # idk what this is for @@ -26,15 +26,22 @@ function partial_frac_decompose(expr) # end # end - E = 1 # TODO - F = 1 # TODO + E = zeros(degree(B), degree(B)) + F = coeff_vector(A, x) - G = F*E^(-1) # TODO solve EG = F for G + for i = 1:k + E[:, i] = coeff_vector(simplify(B/Y[i]), x, degree(B) - 1) + end + @show E + Gbar = rationalize.(inv(E), tol=1e-6)*F + G0 = Gbar[1] + G = Gbar[2:end] / G0 - w = b * G[0] + w = b * G0 + j = 1 for i = 1:k - m = degree(B[i]) + m = degree(Y[i]) if m == 0 push!(X, 0) @@ -42,22 +49,39 @@ function partial_frac_decompose(expr) continue end - n = i*m - + Ai = G[j:j+m] .* x.^[0:m] + h = gcd(w, coeff_vector(Ai, x)...) + vi = w / h + Ai /= h + + if vi < 0 + vi *= -1 + Ai *= -1 + end + + push!(X, Ai) + push!(Z, vi) + + j += m end + + return reverse(X) ./ (Y .* reverse(Z)) end -function expand_factor_list(facs) - result = [] - expand_rule = @rule ~a^~n => fill(~a, ~n) - for fac in facs - expanded = expand_rule(fac) - if expanded === nothing - push!(result, fac) +function coeff_vector(poly, x, n) + coeff_dict = polynomial_coeffs(poly, [x])[1] + vec = [] + for i = 0:n + if x^i in keys(coeff_dict) + push!(vec, coeff_dict[x^i]) else - append!(result, expanded) + push!(vec, 0) end end - return result + return vec +end + +function coeff_vector(poly, x) + return coeff_vector(poly, x, degree(poly)) end \ No newline at end of file diff --git a/test/partialfractions.jl b/test/partialfractions.jl new file mode 100644 index 000000000..81542c897 --- /dev/null +++ b/test/partialfractions.jl @@ -0,0 +1,10 @@ +using Symbolics +import Symbolics: partial_frac_decomposition + +@variables x + +# https://en.neurochispas.com/algebra/4-types-of-partial-fractions-decomposition-with-examples/ +@test_broken isequal(partial_frac_decomposition((3x-1) / (x^2 + x - 6), x), 2/(x+3) + 1/(x-2)) +@test_broken isequal(partial_frac_decomposition((9x^2 + 34x + 14) / ((x+2)*(x^2 - x - 12)), x), 3/(x+2) - 1/(x+3) + 7/(x-4)) + +@test_broken isequal(partial_frac_decomposition((2x^2 + 29x - 1) / ((2x+1)*(x - 2)^2), x), -4/(2x+1) + 3/(x-2) + 11/(x-2)^2) From 852ae5b6e48ffc983cba03021229926353382256 Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Fri, 18 Jul 2025 16:15:04 -0400 Subject: [PATCH 23/52] updated documentation and fixed test imports --- docs/src/manual/ode.md | 15 +++++- src/diffeqs/diffeqs.jl | 111 +++++++++++++++++++++++++++++++++-------- src/diffeqs/systems.jl | 21 +++++++- test/diffeqs.jl | 21 ++++---- 4 files changed, 133 insertions(+), 35 deletions(-) diff --git a/docs/src/manual/ode.md b/docs/src/manual/ode.md index 33e1ba1a1..e120f404a 100644 --- a/docs/src/manual/ode.md +++ b/docs/src/manual/ode.md @@ -24,11 +24,22 @@ The analytical solution can be investigated symbolically using `observed(sys)`. ## Symbolically Solving ODEs -Currently there is no native symbolic ODE solver. Though there are bindings to SymPy - !!! note This area is currently under heavy development. More solvers will be available in the near future. +```@docs +Symbolics.LinearODE +``` + +```@docs +Symbolics.symbolic_solve_ode +``` + +### Continuous Dynamical Systems +```@docs +Symbolics.solve_linear_system +``` + ### SymPy ```@docs diff --git a/src/diffeqs/diffeqs.jl b/src/diffeqs/diffeqs.jl index 9dd203519..f6202b53f 100644 --- a/src/diffeqs/diffeqs.jl +++ b/src/diffeqs/diffeqs.jl @@ -1,6 +1,3 @@ -using Symbolics -import Symbolics: value, coeff, sympy_integrate - """ Represents a linear ordinary differential equation of the form: @@ -138,24 +135,47 @@ function characteristic_polynomial(eq::LinearODE, r) end """ -Symbolically solve a linear ODE + symbolic_solve_ode(eq::LinearODE) +Symbolically solve a linear ordinary differential equation + +# Arguments +- eq: a `LinearODE` to solve + +# Returns +Symbolic solution to the ODE + +# Supported Methods +- first-order integrating factor +- constant coefficient homogeneous solutions (can handle repeated and complex characteristic roots) +- exponential and resonant response formula particular solutions (for any linear combination of `exp`, `sin`, `cos`, or `exp` times `sin` or `cos` (e.g. `e^2t * cos(-t) + e^-3t + sin(5t))`) +- method of undetermined coefficients particular solutions +- linear combinations of above particular solutions + +# Examples + +```jldoctest +julia> using Symbolics; import Nemo, SymPy + +julia> @variables x, t +2-element Vector{Num}: + x + t -Cases handled: -- ☑ first order linear -- ☑ Clairaut's equation -- ◩ bernoulli equations -- ☑ homogeneous with constant coefficients -- ◩ particular solutions - - ☑ ERF + RRF - - ☑ complex ERF + RRF to handle sin/cos - - ☑ method of undetermined coefficients - - ▢ variation of parameters -- ▢ [Differential transform method](https://www.researchgate.net/publication/267767445_A_New_Algorithm_for_Solving_Linear_Ordinary_Differential_Equations) -- ▢ Laplace Transform -- ☑ Expression parsing +# Integrating Factor (note that SymPy is required for integration) +julia> symbolic_solve_ode(LinearODE(x, t, [5/t], 7t)) +(C₁ + t^7) / (t^5) -Uses methods: [`integrating_factor_solve`](@ref), [`find_homogeneous_solutions`](@ref), [`find_particular_solution`](@ref) +# Constant Coefficients and RRF (note that Nemo is required to find characteristic roots) +julia> symbolic_solve_ode(LinearODE(x, t, [9, -6], 4exp(3t))) +C₁*exp(3t) + C₂*t*exp(3t) + (2//1)*(t^2)*exp(3t) +julia> symbolic_solve_ode(LinearODE(x, t, [6, 5], 2exp(-t)*cos(t))) +C₁*exp(-2t) + C₂*exp(-3t) + (1//5)*cos(t)*exp(-t) + (3//5)*exp(-t)*sin(t) + +# Method of Undetermined Coefficients +julia> symbolic_solve_ode(LinearODE(x, t, [-3, 2], 2t - 5)) +(11//9) - (2//3)*t + C₁*exp(t) + C₂*exp(-3t) +``` """ function symbolic_solve_ode(eq::LinearODE) homogeneous_solutions = find_homogeneous_solutions(eq) @@ -174,6 +194,46 @@ function symbolic_solve_ode(eq::LinearODE) end end +""" + symbolic_solve_ode(expr::Equation, x, t) +Symbolically solve an ODE + +# Arguments +- expr: a symbolic ODE +- x: dependent variable +- t: independent variable + +# Supported Methods +- all methods of solving linear ODEs mentioend for `symbolic_solve_ode(eq::LinearODE)` +- Clairaut's equation +- Bernoulli equations + +# Examples + +```jldoctest +julia> using Symbolics; import Nemo + +julia> @variables x, t +2-element Vector{Num}: + x + t + +julia> Dt = Differential(t) +Differential(t) + +# LinearODE (via constant coefficients and RRF) +julia> symbolic_solve_ode(9t*x - 6*Dt(x) ~ 4exp(3t), x, t) +C₁*exp(3t) + C₂*t*exp(3t) + (2//1)*(t^2)*exp(3t) + +# Clairaut's equation +julia> symbolic_solve_ode(x ~ Dt(x)*t - ((Dt(x))^3), x, t) +C₁*t - (C₁^3) + +# Bernoulli equations +julia> symbolic_solve_ode(Dt(x) + (4//t)*x ~ t^3 * x^2, x, t) +1 / (C₁*(t^4) - (t^4)*log(t)) +``` +""" function symbolic_solve_ode(expr::Equation, x, t) clairaut = solve_clairaut(expr, x, t) if clairaut !== nothing @@ -592,11 +652,15 @@ function linearize_bernoulli(expr, x, t, v) if Symbolics.hasderiv(Symbolics.value(term)) facs = _true_factors(term) leading_coeff = prod(filter(fac -> !Symbolics.hasderiv(Symbolics.value(fac)), facs)) - @assert _get_der_order(term//leading_coeff, x, t) == 1 "Expected linear term in $term" + if _get_der_order(term//leading_coeff, x, t) != 1 + return nothing + end elseif !isempty(Symbolics.get_variables(term, [x])) facs = _true_factors(term) x_fac = filter(fac -> !isempty(Symbolics.get_variables(fac, [x])), facs) - @assert length(x_fac) == 1 "Expected linear term in $term" + if length(x_fac) != 1 + return nothing + end if isequal(x_fac[1], x) p = prod(filter(fac -> isempty(Symbolics.get_variables(fac, [x])), facs)) @@ -618,7 +682,12 @@ Solve Bernoulli equations of the form dx/dt + p(t)x = q(t)x^n """ function solve_bernoulli(expr, x, t) @variables v - eq, n = linearize_bernoulli(expr, x, t, v) + linearized = linearize_bernoulli(expr, x, t, v) + if linearized === nothing + return nothing + end + + eq, n = linearized solution = symbolic_solve_ode(eq) if solution === nothing diff --git a/src/diffeqs/systems.jl b/src/diffeqs/systems.jl index 521cfdf06..378b3c0e6 100644 --- a/src/diffeqs/systems.jl +++ b/src/diffeqs/systems.jl @@ -6,6 +6,7 @@ Returns evolution matrix e^(tD) evo_mat(D::Matrix{<:Number}, t::Num) = diagm(exp.(t .* diag(D))) """ + solve_linear_system(A::Matrix{<:Number}, x0::Vector{<:Number}, t::Num) Solve linear continuous dynamical system of differential equations of the form Ax = x' with initial condition x0 # Arguments @@ -14,7 +15,25 @@ Solve linear continuous dynamical system of differential equations of the form A - `t`: independent variable # Returns -- vector of symbolic solutions +vector of symbolic solutions + +# Examples +!!! note uses method `symbolic_solve`, so packages `Nemo` and `Groebner` are often required +```jldoctest +julia> @variables t +1-element Vector{Num}: + t + +julia> solve_linear_system([1 0; 0 -1], [1, -1], t) # requires Nemo +2-element Vector{Num}: + exp(t) + -exp(-t) + +julia> solve_linear_system([-3 4; -2 3], [7, 2], t) # requires Groebner +2-element Vector{Num}: + (10//1)*exp(-t) - (3//1)*exp(t) + (5//1)*exp(-t) - (3//1)*exp(t) +``` """ function solve_linear_system(A::Matrix{<:Number}, x0::Vector{<:Number}, t::Num) # Check A is square diff --git a/test/diffeqs.jl b/test/diffeqs.jl index cd4700282..c37a06558 100644 --- a/test/diffeqs.jl +++ b/test/diffeqs.jl @@ -1,35 +1,34 @@ using Symbolics -using Symbolics: solve_linear_system, LinearODE, is_homogeneous, has_const_coeffs, to_homogeneous, symbolic_solve_ode, find_particular_solution, IVP, solve_IVP -import Groebner, Nemo, SymPy +using Symbolics: solve_linear_system, LinearODE, has_const_coeffs, to_homogeneous, symbolic_solve_ode, find_particular_solution, IVP, solve_IVP +using Groebner, Nemo, SymPy using Test @variables x, y, t # Systems +# ideally, `isapprox` would all be `isequal`, but there seem to be some floating point inaccuracies @test isapprox(solve_linear_system([1 0; 0 -1], [1, -1], t), [exp(t), -exp(-t)]) @test isapprox(solve_linear_system([-3 4; -2 3], [7, 2], t), [10exp(-t) - 3exp(t), 5exp(-t) - 3exp(t)]) @test isapprox(solve_linear_system([4 -3; 8 -6], [7, 2], t), [18 - 11exp(-2t), 24 - 22exp(-2t)]) -@test_broken isapprox(solve_linear_system([-1 -2; 2 -1], [1, -1], t), [exp(-t)*(cos(2t) + sin(2t)), exp(-t)*(sin(2t) - cos(2t))]) +@test_broken isapprox(solve_linear_system([-1 -2; 2 -1], [1, -1], t), [exp(-t)*(cos(2t) + sin(2t)), exp(-t)*(sin(2t) - cos(2t))]) # can't handle complex eigenvalues (though it should be able to) @test isapprox(solve_linear_system([1 -1 0; 1 2 1; -2 1 -1], [7, 2, 3], t), (5//3)*exp(-t)*[-1, -2, 7] - 14exp(t)*[-1, 0, 1] + (16//3)*exp(2t)*[-1, 1, 1]) @test isequal(solve_linear_system([1 0; 0 -1], [1, -1], t), [exp(t), -exp(-t)]) @test isequal(solve_linear_system([-3 4; -2 3], [7, 2], t), [10exp(-t) - 3exp(t), 5exp(-t) - 3exp(t)]) -@test_broken isequal(solve_linear_system([4 -3; 8 -6], [7, 2], t), [18 - 11exp(-2t), 24 - 22exp(-2t)]) - -@test_broken isequal(solve_linear_system([-1 -2; 2 -1], [1, -1], t), [exp(-t)*(cos(2t) + sin(2t)), exp(-t)*(sin(2t) - cos(2t))]) +@test isapprox(solve_linear_system([4 -3; 8 -6], [7, 2], t), [18 - 11exp(-2t), 24 - 22exp(-2t)]) @test isequal(solve_linear_system([1 -1 0; 1 2 1; -2 1 -1], [7, 2, 3], t), (5//3)*exp(-t)*[-1, -2, 7] - 14exp(t)*[-1, 0, 1] + (16//3)*exp(2t)*[-1, 1, 1]) # LinearODEs -@test is_homogeneous(LinearODE(x, t, [1, 1], 0)) -@test !is_homogeneous(LinearODE(x, t, [t, 1], t^2)) +@test Symbolics.is_homogeneous(LinearODE(x, t, [1, 1], 0)) +@test !Symbolics.is_homogeneous(LinearODE(x, t, [t, 1], t^2)) @test has_const_coeffs(LinearODE(x, t, [1, 1], 0)) @test !has_const_coeffs(LinearODE(x, t, [t^2, 1], 0)) -@test is_homogeneous(to_homogeneous(LinearODE(x, t, [t, 1], t^2))) +@test Symbolics.is_homogeneous(to_homogeneous(LinearODE(x, t, [t, 1], t^2))) C = Symbolics.variables(:C, 1:5) @@ -40,7 +39,7 @@ C = Symbolics.variables(:C, 1:5) ## first order @test isequal(symbolic_solve_ode(LinearODE(x, t, [5/t], 7t)), Symbolics.sympy_simplify(C[1]*t^(-5) + t^2)) @test isequal(symbolic_solve_ode(LinearODE(x, t, [cos(t)], cos(t))), 1 + C[1]*exp(-sin(t))) -@test isequal(symbolic_solve_ode(LinearODE(x, t, [-(1+t)], 1+t)), expand(Symbolics.sympy_simplify(C[1]*exp((1//2)t^2 + t) - 1))) +@test isequal(symbolic_solve_ode(LinearODE(x, t, [-(1+t)], 1+t)), Symbolics.expand(Symbolics.sympy_simplify(C[1]*exp((1//2)t^2 + t) - 1))) # SymPy is being weird and not simplifying correctly (and some symbols are wrong, like pi and erf being syms), but these otherwise work @test_broken isequal(symbolic_solve_ode(LinearODE(x, t, [-2t], 1)), Symbolics.sympy_simplify(exp(t^2)*sqrt(Symbolics.variable(:pi))*erf(t)/2 + C[1]*exp(t^2))) @test isequal(symbolic_solve_ode(LinearODE(x, t, [1], 2sin(t))), C[1]*exp(-t) + sin(t) - cos(t)) @@ -64,7 +63,7 @@ C = Symbolics.variables(:C, 1:5) @test isequal(find_particular_solution(LinearODE(x, t, [1, 0], t^2)), t^2 - 2) # Parsing -Dt = Differential(t) +Dt = Symbolics.Differential(t) @test isequal(LinearODE(x, t, [1], 0), LinearODE(Dt(x) + x ~ 0, x, t)) @test isequal(LinearODE(x, t, [sin(t), 0, 3t^2], exp(2t) + 2cos(t)), LinearODE(6t^2*(Dt^2)(x) + 2sin(t)*x - 2exp(2t) + 2(Dt^3)(x) ~ 4cos(t), x, t)) From 2cb343cd47447ae797e73df2cdf7063204d97369 Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Mon, 21 Jul 2025 22:28:16 -0400 Subject: [PATCH 24/52] Mostly working. Switched to cover-up method and solving a system of linear equations. --- src/partialfractions.jl | 165 ++++++++++++++++++++++++++------------- test/partialfractions.jl | 18 ++++- 2 files changed, 127 insertions(+), 56 deletions(-) diff --git a/src/partialfractions.jl b/src/partialfractions.jl index ba375d45f..6395b001b 100644 --- a/src/partialfractions.jl +++ b/src/partialfractions.jl @@ -1,73 +1,98 @@ -# https://dl.acm.org/doi/pdf/10.1145/800204.806314 +# used to represent linear or irreducible quadratic factors +struct Factor + expr + root + multiplicity + x +end + +function Factor(expr, multiplicity, x) + fac_rule = @rule x + ~r => -~r + return Factor(expr, fac_rule(expr), multiplicity, x) +end + +function Base.isequal(a::Factor, b::Factor) + return isequal(a.expr, b.expr) && a.multiplicity == b.multiplicity +end + +# https://math.mit.edu/~hrm/18.031/pf-coverup.pdf +""" + partial_frac_decomposition(expr, x) + +Performs partial fraction decomposition for expressions with linear, reapeated, or irreducible quadratic factors in the denominator. Can't handle irrational roots or non-one leading coefficients + +!!! note that irreducible quadratic and repeated linear factors require the `Groebner` package to solve a system of equations +""" function partial_frac_decomposition(expr, x) - frac_rule = @rule ~a/~b => (~a, ~b) - A, B = frac_rule(expr) + A, B = numerator(expr), denominator(expr) - X = [] - Z = [] - b = gcd(coeff_vector(B, x)...) - Y = x .- symbolic_solve(B, x, dropmultiplicity=false) - k = length(Y) + facs = factorize(B, x) + v = simplify(B / prod((f -> f.expr^f.multiplicity).(facs))) - if k == 1 + if length(facs) == 1 && only(facs).multiplicity == 1 && degree(A) <= 1 return expr end - # idk what this is for - # for i = 1:k-1 - # if degree(B[i]) == 0 - # push!(X, A) - # push!(Z, b) - # for j = 1:k-1 - # push!(X, 0) - # push!(Z, 1) - # end - # return X, Y, Z - # end - # end - - E = zeros(degree(B), degree(B)) - F = coeff_vector(A, x) + result = 0 - for i = 1:k - E[:, i] = coeff_vector(simplify(B/Y[i]), x, degree(B) - 1) - end - @show E - Gbar = rationalize.(inv(E), tol=1e-6)*F - G0 = Gbar[1] - G = Gbar[2:end] / G0 - - w = b * G0 - - j = 1 - for i = 1:k - m = degree(Y[i]) - - if m == 0 - push!(X, 0) - push!(Z, 1) - continue + c_idx = 0 + if length(facs) == 1 + fac = only(facs) + if fac.root === nothing + for i = 1:fac.multiplicity + result += (variable(:C, c_idx+=1)*x + variable(:C, c_idx+=1))/(fac.expr^i) + end + else + result += sum(variables(:C, (c_idx+1):(c_idx+=fac.multiplicity)) ./ fac.expr.^(1:fac.multiplicity)) end + else + for fac in facs + if fac.root === nothing + for i = 1:fac.multiplicity + result += (variable(:C, c_idx+=1)*x + variable(:C, c_idx+=1))/(fac.expr^i) + end + continue + end - Ai = G[j:j+m] .* x.^[0:m] - h = gcd(w, coeff_vector(Ai, x)...) - vi = w / h - Ai /= h + other_facs = filter(f -> !isequal(f, fac), facs) + + numerator = rationalize(unwrap(substitute(A / prod((f -> f.expr^f.multiplicity).(other_facs)), Dict(x => fac.root)))) + result += numerator / fac.expr^fac.multiplicity - if vi < 0 - vi *= -1 - Ai *= -1 + if fac.multiplicity > 1 + result += sum(variables(:C, (c_idx+1):(c_idx+=fac.multiplicity-1)) ./ fac.expr.^(1:fac.multiplicity-1)) + end end + end + + if isequal(get_variables(result), [x]) + return expand(result/v) + end + + lhs::Vector{Rational} = coeff_vector(numerator(expr), x) + rhs = coeff_vector(numerator(simplify(result)), x) - push!(X, Ai) - push!(Z, vi) + if length(lhs) > length(rhs) + rhs = [rhs; zeros(length(lhs)-length(rhs))] + elseif length(rhs) > length(lhs) + lhs = [lhs; zeros(length(rhs)-length(lhs))] + end - j += m + eqs = [] + for i = 1:length(lhs) + push!(eqs, lhs[i] ~ rhs[i]) end + + solution = symbolic_solve(eqs, Symbolics.variables(:C, 1:c_idx))[1] - return reverse(X) ./ (Y .* reverse(Z)) + if !(solution isa Dict) + solution = Dict(variable(:C, 1) => solution) + end + + return expand(substitute(result, solution)/v) end +# increasing from 0 to degree n function coeff_vector(poly, x, n) coeff_dict = polynomial_coeffs(poly, [x])[1] vec = [] @@ -82,6 +107,40 @@ function coeff_vector(poly, x, n) return vec end +# increasing from 0 to degree of poly function coeff_vector(poly, x) return coeff_vector(poly, x, degree(poly)) +end + +function count_multiplicities(facs) + counts = Dict() + for fac in facs + if haskey(counts, fac) + counts[fac] += 1 + else + counts[fac] = 1 + end + end + + return counts +end + +# for partial fractions, into linear and irreducible quadratic factors +function factorize(expr, x)::Set{Factor} + roots = symbolic_solve(expr, x, dropmultiplicity=false) + + counts = count_multiplicities(roots) + facs = Set() + + for root in keys(counts) + if !isequal(abs(imag(root)), 0) + fac_expr = real(expand(real((x - root)*(x - conj(root))))) + push!(facs, Factor(fac_expr, counts[root], x)) + continue + end + + push!(facs, Factor(x - root, root, counts[root], x)) + end + + return facs end \ No newline at end of file diff --git a/test/partialfractions.jl b/test/partialfractions.jl index 81542c897..7051a582d 100644 --- a/test/partialfractions.jl +++ b/test/partialfractions.jl @@ -1,10 +1,22 @@ +using Test using Symbolics +import Nemo, Groebner import Symbolics: partial_frac_decomposition @variables x # https://en.neurochispas.com/algebra/4-types-of-partial-fractions-decomposition-with-examples/ -@test_broken isequal(partial_frac_decomposition((3x-1) / (x^2 + x - 6), x), 2/(x+3) + 1/(x-2)) -@test_broken isequal(partial_frac_decomposition((9x^2 + 34x + 14) / ((x+2)*(x^2 - x - 12)), x), 3/(x+2) - 1/(x+3) + 7/(x-4)) +@test isequal(partial_frac_decomposition((3x-1) / (x^2 + x - 6), x), 2/(x+3) + 1/(x-2)) +@test isequal(partial_frac_decomposition((9x^2 + 34x + 14) / ((x+2)*(x^2 - x - 12)), x), expand(3/(x+2) + 7/(x-4) - 1/(x+3))) -@test_broken isequal(partial_frac_decomposition((2x^2 + 29x - 1) / ((2x+1)*(x - 2)^2), x), -4/(2x+1) + 3/(x-2) + 11/(x-2)^2) +# https://tutorial.math.lamar.edu/Problems/Alg/PartialFractions.aspx +# can't handle leading coefficients being not 1 in denominator +@test isequal(partial_frac_decomposition((17x-53)/(x^2 - 2x - 15), x), expand(4/(x-5) + 13/(x+3))) +@test_broken isequal(partial_frac_decomposition((34-12x)/(3x^2 - 10x - 8), x), -9(3x+2) - 1/(x-4)) +@test isequal(partial_frac_decomposition((125 + 4x - 9x^2)/((x-1)*(x+3)*(x+4)), x), expand(6/(x-1) - 8/(x+3) - 7/(x+4))) +@test isequal(partial_frac_decomposition((10x+35)/((x+4)^2), x), expand(10/(x+4) - 5/(x+4)^2)) +@test_broken isequal(partial_frac_decomposition((6x+5)/((2x-1)^2), x), 3/(2x-1) + 8/(2x-1)^2) +@test isequal(partial_frac_decomposition((7x^2-17x+38)/((x+6)*(x-1)^2), x), expand(8/(x+6) - 1/(x-1) + 4/(x-1)^2)) +@test_broken isequal(partial_frac_decomposition((4x^2 - 22x + 7)/((2x+3)*(x-2)^2), x), 4/(2x+3) - 3/(x-2)^2) +@test_broken isequal(partial_frac_decomposition((3x^2 + 7x + 28)/(x*(x^2 + x + 7)), x), expand(4/x + (3-x)/(x^2+x+7))) # irrational roots +@test isequal(partial_frac_decomposition((4x^3 + 16x + 7)/(x^2 + 4)^2, x), expand(4x/(x^2+4) + 7/(x^2+4)^2)) \ No newline at end of file From c00baedd828ddab48227bb7b9a656421e348c24f Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Wed, 23 Jul 2025 14:56:39 -0400 Subject: [PATCH 25/52] added unit tests --- src/Symbolics.jl | 3 ++- src/diffeqs/diffeqs.jl | 2 +- src/diffeqs/laplace.jl | 31 ++++++------------------------- test/laplace.jl | 19 +++++++++++++++++++ 4 files changed, 28 insertions(+), 27 deletions(-) create mode 100644 test/laplace.jl diff --git a/src/Symbolics.jl b/src/Symbolics.jl index c82c4029e..871c45f62 100644 --- a/src/Symbolics.jl +++ b/src/Symbolics.jl @@ -224,7 +224,8 @@ export symbolic_solve # Diff Eq Solver include("diffeqs/diffeqs.jl") include("diffeqs/systems.jl") -export LinearODE, IVP, symbolic_solve_ode, solve_linear_system, solve_IVP +include("diffeqs/laplace.jl") +export LinearODE, IVP, symbolic_solve_ode, solve_linear_system, solve_IVP, laplace, inverse_laplace # Sympy Functions diff --git a/src/diffeqs/diffeqs.jl b/src/diffeqs/diffeqs.jl index 9dd203519..696a1cfde 100644 --- a/src/diffeqs/diffeqs.jl +++ b/src/diffeqs/diffeqs.jl @@ -649,7 +649,7 @@ end # takes into account fractions function _true_factors(expr) facs = factors(expr) - true_facs::Vector{Number} = [] + true_facs::Vector{Union{Number, Symbolics.BasicSymbolic}} = [] frac_rule = @rule (~x)/(~y) => [~x, 1/~y] for fac in facs frac = frac_rule(fac) diff --git a/src/diffeqs/laplace.jl b/src/diffeqs/laplace.jl index 516c0c692..cc20fd595 100644 --- a/src/diffeqs/laplace.jl +++ b/src/diffeqs/laplace.jl @@ -1,5 +1,4 @@ -using Symbolics -import Symbolics: get_variables, coeff, sympy_integrate +import DomainSets.ClosedInterval function laplace(expr, f, t, s, F) Dt = Differential(t) @@ -35,7 +34,7 @@ function laplace(expr, f, t, s, F) @rule exp(~a*t) * sinh(~b * t) => ~b / (-(~b)^2 + (-~a+s)^2) @rule exp(~a*t) * cosh(~b * t) => (-~a+s) / (-(~b)^2 + (-~a+s)^2) @rule t^~n * exp(~a * t) => factorial(~n) / (-~a + s)^(~n + 1) - @rule exp(~c*t) * ~g => laplace(~g, f, t, s - ~c, F) # s-shift rule + @rule ~g * exp(~c*t) => laplace(~g, f, t, s - ~c, F) # s-shift rule @rule t*f(t) => -Ds(F(s)) # s-derivative rule @rule t^(~n)*f(t) => (-1)^(~n) * (Ds^~n)(F(s)) # s-derivative rule @rule f(~a + t) => exp(~a*s)*F(s) # t-shift rule @@ -62,7 +61,7 @@ function laplace(expr, f, t, s, F) terms = Symbolics.terms(expr) result = 0 if length(terms) == 1 && length(filter(x->isempty(Symbolics.get_variables(x)), _true_factors(terms[1]))) == 0 - return Integral(t in 0..Inf)(expr*exp(-s*t)) + return Integral(t in ClosedInterval(0, Inf))(expr*exp(-s*t)) end for term in terms factors = _true_factors(term) @@ -120,9 +119,9 @@ function inverse_laplace(expr, F, t, s, f) factors = _true_factors(term) constant = filter(x -> isempty(Symbolics.get_variables(x)), factors) if !isempty(constant) - result += inverse_laplace(term / constant[1], t, s) * constant[1] + result += inverse_laplace(term / constant[1], F, t, s, f) * constant[1] else - result += inverse_laplace(term, t, s) + result += inverse_laplace(term, F, t, s, f) end end @@ -142,22 +141,4 @@ function unwrap_der(expr, Dt) order, expr = unwrap_der(reduce_rule(expr), Dt) return order + 1, expr -end - -# takes into account fractions -function _true_factors(expr) - facs = Symbolics.factors(expr) - true_facs::Vector{Union{Number, Symbolics.BasicSymbolic}} = [] - frac_rule = @rule (~x)/(~y) => [~x, 1/~y] - for fac in facs - frac = frac_rule(fac) - if frac !== nothing && !isequal(frac[1], 1) - append!(true_facs, _true_factors(frac[1])) - append!(true_facs, _true_factors(frac[2])) - else - push!(true_facs, fac) - end - end - - return true_facs -end +end \ No newline at end of file diff --git a/test/laplace.jl b/test/laplace.jl new file mode 100644 index 000000000..a6c1732d0 --- /dev/null +++ b/test/laplace.jl @@ -0,0 +1,19 @@ +using Test +using Symbolics +using Symbolics: laplace, inverse_laplace +import Nemo, Groebner + +@variables t, s +@syms f(t) F(s) + +# https://sites.math.washington.edu/~aloveles/Math307Fall2019/m307LaplacePractice.pdf +@test isequal(laplace(exp(4t) + 5, f, t, s, F), 1/(s-4) + 5/s) +@test isequal(laplace(cos(2t) + 7sin(2t), f, t, s, F), s/(s^2 + 4) + 14/(s^2 + 4)) +@test isequal(laplace(exp(-2t)*cos(3t) + 5exp(-2t)*sin(3t), f, t, s, F), (s+2)/((s+2)^2 + 9) + 15/((s+2)^2 + 9)) +@test isequal(laplace(10 + 5t + t^2 - 4t^3, f, t, s, F), expand(10/s + 5/s^2 + 2/s^3 - 24/s^4)) +@test isequal(laplace(exp(3t)*(t^2 + 4t + 2), f, t, s, F), 2/(s-3)^3 + 4/(s-3)^2 + 2/(s-3)) +@test isequal(laplace(6exp(5t)*cos(2t) - exp(7t), f, t, s, F), 6(s-5)/((s-5)^2 + 4) + expand(-1/(s-7))) + +# https://www.math.lsu.edu/~adkins/m2065/2065s08review2a.pdf +@test isequal(inverse_laplace(7/(s+3)^3, F, t, s, f), (7//2)t^2 * exp(-3t)) +@test_broken isequal(inverse_laplace((s+2)/(s^2 - 3s - 4), F, t, s, f), (7//2)t^2 * exp(-3t)) # partial fraction decomposition \ No newline at end of file From 79a21fd9744a2de6538f956cd7b3258ff3af9c25 Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Wed, 23 Jul 2025 15:13:38 -0400 Subject: [PATCH 26/52] added checks to make sure expression is valid for decomposition --- src/partialfractions.jl | 22 +++++++++++++++++++--- test/partialfractions.jl | 7 ++++++- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/partialfractions.jl b/src/partialfractions.jl index 6395b001b..fdf33047c 100644 --- a/src/partialfractions.jl +++ b/src/partialfractions.jl @@ -25,8 +25,23 @@ Performs partial fraction decomposition for expressions with linear, reapeated, """ function partial_frac_decomposition(expr, x) A, B = numerator(expr), denominator(expr) + + # check if both numerator and denominator are polynomials + if !isequal(polynomial_coeffs(A, [x])[2], 0) || !isequal(polynomial_coeffs(B, [x])[2], 0) + return nothing + end + + if degree(A) >= degree(B) + return nothing + end - facs = factorize(B, x) + facs = 0 + try + facs = factorize(B, x) + catch AssertionError + return nothing + end + v = simplify(B / prod((f -> f.expr^f.multiplicity).(facs))) if length(facs) == 1 && only(facs).multiplicity == 1 && degree(A) <= 1 @@ -134,8 +149,9 @@ function factorize(expr, x)::Set{Factor} for root in keys(counts) if !isequal(abs(imag(root)), 0) - fac_expr = real(expand(real((x - root)*(x - conj(root))))) - push!(facs, Factor(fac_expr, counts[root], x)) + fac_expr = expand((x - root)*(x - conj(root))) + @assert isequal(imag(fac_expr), 0) "Encountered issue with complex irrational roots" + push!(facs, Factor(real(fac_expr), counts[root], x)) continue end diff --git a/test/partialfractions.jl b/test/partialfractions.jl index 7051a582d..d7a5d6ab2 100644 --- a/test/partialfractions.jl +++ b/test/partialfractions.jl @@ -19,4 +19,9 @@ import Symbolics: partial_frac_decomposition @test isequal(partial_frac_decomposition((7x^2-17x+38)/((x+6)*(x-1)^2), x), expand(8/(x+6) - 1/(x-1) + 4/(x-1)^2)) @test_broken isequal(partial_frac_decomposition((4x^2 - 22x + 7)/((2x+3)*(x-2)^2), x), 4/(2x+3) - 3/(x-2)^2) @test_broken isequal(partial_frac_decomposition((3x^2 + 7x + 28)/(x*(x^2 + x + 7)), x), expand(4/x + (3-x)/(x^2+x+7))) # irrational roots -@test isequal(partial_frac_decomposition((4x^3 + 16x + 7)/(x^2 + 4)^2, x), expand(4x/(x^2+4) + 7/(x^2+4)^2)) \ No newline at end of file +@test isequal(partial_frac_decomposition((4x^3 + 16x + 7)/(x^2 + 4)^2, x), expand(4x/(x^2+4) + 7/(x^2+4)^2)) + +# check valid expressions +@test partial_frac_decomposition(sin(x), x) === nothing +@test partial_frac_decomposition(x^2/(x-1), x) === nothing +@test partial_frac_decomposition(1/(x^2 + 2), x) === nothing # irrational roots, should eventually be fixed \ No newline at end of file From 628cbc96492ca764f935c37bd17923ae7e1a7f87 Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Wed, 23 Jul 2025 19:12:48 -0400 Subject: [PATCH 27/52] added partial fractions into laplace transform, fixed various bugs having to do with constant distribution --- src/diffeqs/laplace.jl | 43 +++++++++++++++++++++++++---------------- src/partialfractions.jl | 12 ++++++++++-- test/laplace.jl | 5 ++++- 3 files changed, 40 insertions(+), 20 deletions(-) diff --git a/src/diffeqs/laplace.jl b/src/diffeqs/laplace.jl index cc20fd595..8e7bfa74c 100644 --- a/src/diffeqs/laplace.jl +++ b/src/diffeqs/laplace.jl @@ -10,7 +10,7 @@ function laplace(expr, f, t, s, F) @rule exp(~a * t) => 1/(-~a + s) @rule t => 1/s^2 @rule t^~n => factorial(~n)/s^(~n + 1) - @rule sqrt(t) => sqrt(pi)/(2 * s^(3/2)) + @rule sqrt(t) => term(sqrt, pi)/(2 * s^(3/2)) @rule sin(t) => 1/(1 + s^2) @rule sin(~a * t) => ~a/((~a)^2 + s^2) @rule cos(t) => s/(1 + s^2) @@ -81,20 +81,26 @@ function laplace(expr::Equation, f, t, s, F) end function inverse_laplace(expr, F, t, s, f) + # check for partial fractions + partial_fractions = partial_frac_decomposition(expr, s) + if partial_fractions !== nothing && !isequal(partial_fractions, expr) + return inverse_laplace(partial_fractions, F, t, s, f) + end + inverse_transform_rules = Symbolics.Chain([ @rule 1/s => 1 - @rule 1/(~a + s) => exp(~a * t) + @rule 1/(~a + s) => exp(-~a * t) @rule 1/s^(~n) => t^(~n-1) / factorial(~n-1) - @rule 1/(2 * s^(3/2)) => sqrt(t)/sqrt(pi) - @rule 1/((~a)^2 + s^2) => sin(~a * t)/~a - @rule s/((~a)^2 + s^2) => cos(~a * t) - @rule s / ((~a)^2 + s^2)^2 => t*sin(~a * t)/(2*~a) - @rule (-(~a)^2 + s^2) / ((~a)^2 + s^2)^2 => t*cos(~a * t) - @rule 1 / ((~a)^2 + s^2)^2 => (sin(~a*t) - ~a*t*cos(~a*t))/ (2*(~a)^3) - @rule s^2 / ((~a)^2 + s^2)^2 => (sin(~a*t) + ~a*t*cos(~a*t)) / (2*~a) - @rule s*((~a)^2 + s^2) / ((~a)^2 + s^2)^2 => cos(~a*t) - ~a*t*sin(~a*t) - @rule s*(3*(~a)^2 + s^2) / ((~a)^2 + s^2)^2 => cos(~a*t) + ~a*t*sin(~a*t) - @rule (s*sin(~b) + ~a*cos(~b)) / ((~a)^2 + s^2) => sin(~b + ~a*t) + @rule 1/(2 * s^(3/2)) => sqrt(t)/term(term(sqrt, pi)) + @rule 1/(~a + s^2) => sin(postprocess_root(term(sqrt, ~a)) * t)/postprocess_root(term(sqrt, ~a)) + @rule s/(~a + s^2) => cos(postprocess_root(term(sqrt, ~a)) * t) + @rule s / (~a + s^2)^2 => t*sin(postprocess_root(term(sqrt, ~a)) * t)/(2*postprocess_root(term(sqrt, ~a))) + @rule (-~a + s^2) / (~a + s^2)^2 => t*cos(postprocess_root(term(sqrt, ~a)) * t) + @rule 1 / (~a + s^2)^2 => (sin(postprocess_root(term(sqrt, ~a))*t) - postprocess_root(term(sqrt, ~a))*t*cos(postprocess_root(term(sqrt, ~a))*t))/ (2*postprocess_root(term(sqrt, ~a))^3) + @rule s^2 / (~a + s^2)^2 => (sin(postprocess_root(term(sqrt, ~a))*t) + postprocess_root(term(sqrt, ~a))*t*cos(postprocess_root(term(sqrt, ~a))*t)) / (2*postprocess_root(term(sqrt, ~a))) + @rule s*(~a + s^2) / (~a + s^2)^2 => cos(postprocess_root(term(sqrt, ~a))*t) - postprocess_root(term(sqrt, ~a))*t*sin(postprocess_root(term(sqrt, ~a))*t) + @rule s*(3*~a + s^2) / (~a + s^2)^2 => cos(postprocess_root(term(sqrt, ~a))*t) + postprocess_root(term(sqrt, ~a))*t*sin(postprocess_root(term(sqrt, ~a))*t) + @rule (s*sin(~b) + ~a*cos(~b)) / (~a + s^2) => sin(~b + postprocess_root(term(sqrt, ~a))*t) @rule (s*cos(~b) - ~a*sin(~b)) / ((~a)^2 + s^2) => cos(~b + ~a*t) @rule 1/(s^2 - (~b)^2) => sinh(~b * t)/~b @rule s/(s^2 - (~b)^2) => cosh(~b * t) @@ -110,17 +116,20 @@ function inverse_laplace(expr, F, t, s, f) return transformed end - terms = Symbolics.terms(expr) + _terms = terms(numerator(expr)) ./ denominator(expr) + result = 0 - if length(terms) == 1 && length(_true_factors(terms[1])) == 1 - return f + if length(_terms) == 1 && length(filter(x -> isempty(get_variables(x)), _true_factors(_terms[1]))) == 0 + return nothing # no result end - for term in terms + + # apply linearity + for term in _terms factors = _true_factors(term) constant = filter(x -> isempty(Symbolics.get_variables(x)), factors) if !isempty(constant) result += inverse_laplace(term / constant[1], F, t, s, f) * constant[1] - else + else result += inverse_laplace(term, F, t, s, f) end end diff --git a/src/partialfractions.jl b/src/partialfractions.jl index fdf33047c..4fc8fa22e 100644 --- a/src/partialfractions.jl +++ b/src/partialfractions.jl @@ -81,7 +81,11 @@ function partial_frac_decomposition(expr, x) end if isequal(get_variables(result), [x]) - return expand(result/v) + distributed = 0 + for term in terms(result) + distributed += term/v + end + return distributed end lhs::Vector{Rational} = coeff_vector(numerator(expr), x) @@ -104,7 +108,11 @@ function partial_frac_decomposition(expr, x) solution = Dict(variable(:C, 1) => solution) end - return expand(substitute(result, solution)/v) + distributed = 0 + for term in terms(substitute(result, solution)) + distributed += term/v + end + return distributed end # increasing from 0 to degree n diff --git a/test/laplace.jl b/test/laplace.jl index a6c1732d0..9db2c9d4d 100644 --- a/test/laplace.jl +++ b/test/laplace.jl @@ -16,4 +16,7 @@ import Nemo, Groebner # https://www.math.lsu.edu/~adkins/m2065/2065s08review2a.pdf @test isequal(inverse_laplace(7/(s+3)^3, F, t, s, f), (7//2)t^2 * exp(-3t)) -@test_broken isequal(inverse_laplace((s+2)/(s^2 - 3s - 4), F, t, s, f), (7//2)t^2 * exp(-3t)) # partial fraction decomposition \ No newline at end of file +@test isequal(inverse_laplace((s-9)/(s^2 + 9), F, t, s, f), cos(3t) - 3sin(3t)) +# partial fraction decomposition +@test isequal(inverse_laplace((s+2)/(s^2 - 3s - 4), F, t, s, f), (6//5)*exp(4t) - (1//5)*exp(-t)) +@test isequal(inverse_laplace(1/(s^2 - 10s + 9), F, t, s, f), (1//8)*exp(9t) - (1//8)*exp(t)) \ No newline at end of file From 8164a7a6fdd88da0fe9be44ff41d5219915f78ff Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Thu, 24 Jul 2025 23:10:44 -0400 Subject: [PATCH 28/52] started implementing laplace ode solver --- src/Symbolics.jl | 2 +- src/diffeqs/laplace.jl | 35 +++++++++++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/Symbolics.jl b/src/Symbolics.jl index 871c45f62..058b3c1d4 100644 --- a/src/Symbolics.jl +++ b/src/Symbolics.jl @@ -225,7 +225,7 @@ export symbolic_solve include("diffeqs/diffeqs.jl") include("diffeqs/systems.jl") include("diffeqs/laplace.jl") -export LinearODE, IVP, symbolic_solve_ode, solve_linear_system, solve_IVP, laplace, inverse_laplace +export LinearODE, IVP, symbolic_solve_ode, solve_linear_system, solve_IVP, laplace, inverse_laplace, laplace_solve_ode # Sympy Functions diff --git a/src/diffeqs/laplace.jl b/src/diffeqs/laplace.jl index 8e7bfa74c..4303cf1cd 100644 --- a/src/diffeqs/laplace.jl +++ b/src/diffeqs/laplace.jl @@ -33,8 +33,11 @@ function laplace(expr, f, t, s, F) @rule exp(~a*t) * cos(~b * t) => (-~a+s) / ((~b)^2 + (-~a+s)^2) @rule exp(~a*t) * sinh(~b * t) => ~b / (-(~b)^2 + (-~a+s)^2) @rule exp(~a*t) * cosh(~b * t) => (-~a+s) / (-(~b)^2 + (-~a+s)^2) - @rule t^~n * exp(~a * t) => factorial(~n) / (-~a + s)^(~n + 1) - @rule ~g * exp(~c*t) => laplace(~g, f, t, s - ~c, F) # s-shift rule + @rule exp(~a * t) * t^~n => factorial(~n) / (-~a + s)^(~n + 1) + @rule exp(~a * t) * t => 1 / (-~a + s)^(2) + @rule exp(t) * t^~n => factorial(~n) / (s)^(~n + 1) + @rule exp(t) * t => 1 / (s)^(2) + @rule exp(~c*t) * ~g => laplace(~g, f, t, s - ~c, F) # s-shift rule @rule t*f(t) => -Ds(F(s)) # s-derivative rule @rule t^(~n)*f(t) => (-1)^(~n) * (Ds^~n)(F(s)) # s-derivative rule @rule f(~a + t) => exp(~a*s)*F(s) # t-shift rule @@ -82,6 +85,10 @@ end function inverse_laplace(expr, F, t, s, f) # check for partial fractions + if isequal(expr, 1/((3+s)*(s^2))) + s + partial_frac_decomposition(1/((3+s)*(s^2)), s) + end partial_fractions = partial_frac_decomposition(expr, s) if partial_fractions !== nothing && !isequal(partial_fractions, expr) return inverse_laplace(partial_fractions, F, t, s, f) @@ -120,6 +127,7 @@ function inverse_laplace(expr, F, t, s, f) result = 0 if length(_terms) == 1 && length(filter(x -> isempty(get_variables(x)), _true_factors(_terms[1]))) == 0 + println("Inverse laplace failed: $expr") return nothing # no result end @@ -150,4 +158,27 @@ function unwrap_der(expr, Dt) order, expr = unwrap_der(reduce_rule(expr), Dt) return order + 1, expr +end + +function laplace_solve_ode(eq, f, t, f0) + @variables s + @syms F(s) + transformed_eq = laplace(eq, f, t, s, F) + transformed_eq = substitute(transformed_eq, Dict(F(s) => variable(:F), [variable(:f0, i-1) => f0[i] for i=1:length(f0)]...)) + transformed_eq = transformed_eq.lhs - transformed_eq.rhs + # transformed_soln = symbolic_solve(transformed_eq, variable(:F)) + + F_terms = 0 + other_terms = [] + for term in terms(transformed_eq) + if isempty(get_variables(term, [variable(:F)])) + push!(other_terms, term) + else + F_terms += term/variable(:F) # assumes term is something times F + end + end + + transformed_soln = sum(other_terms ./ F_terms) + + return inverse_laplace(transformed_soln, F, t, s, f) end \ No newline at end of file From c699bce31d3d10e89051ab6af603fd1026605581 Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Mon, 4 Aug 2025 23:39:21 -0400 Subject: [PATCH 29/52] fix sign of terms when solving odes --- src/diffeqs/laplace.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/diffeqs/laplace.jl b/src/diffeqs/laplace.jl index 4303cf1cd..0cda972da 100644 --- a/src/diffeqs/laplace.jl +++ b/src/diffeqs/laplace.jl @@ -172,13 +172,13 @@ function laplace_solve_ode(eq, f, t, f0) other_terms = [] for term in terms(transformed_eq) if isempty(get_variables(term, [variable(:F)])) - push!(other_terms, term) + push!(other_terms, -1*term) else F_terms += term/variable(:F) # assumes term is something times F end end - transformed_soln = sum(other_terms ./ F_terms) + @show transformed_soln = sum(other_terms ./ F_terms) return inverse_laplace(transformed_soln, F, t, s, f) end \ No newline at end of file From c0589a0dc13ad2cd2fd9e15cd5d22c82de826041 Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Mon, 4 Aug 2025 23:41:14 -0400 Subject: [PATCH 30/52] minor fixes for type of _true_factors and verifying MUD solutions --- src/diffeqs/diffeqs.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/diffeqs/diffeqs.jl b/src/diffeqs/diffeqs.jl index f6202b53f..13d5b7605 100644 --- a/src/diffeqs/diffeqs.jl +++ b/src/diffeqs/diffeqs.jl @@ -498,7 +498,7 @@ function method_of_undetermined_coefficients(eq::LinearODE) catch coeff_solution = nothing end - if degree > 0 && coeff_solution !== nothing && !isempty(coeff_solution) + if degree > 0 && coeff_solution !== nothing && !isempty(coeff_solution) && evaluate(eq_subbed, coeff_solution[1]) return substitute(form, coeff_solution[1]) end @@ -730,5 +730,5 @@ function _true_factors(expr) end end - return true_facs + return convert(Vector{Num}, true_facs) end \ No newline at end of file From 8c2f212e794b5b568507b934633b3aacfefbcd4d Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Wed, 6 Aug 2025 17:14:57 -0400 Subject: [PATCH 31/52] working! added tests and fixed issues with laplace ode solver --- src/diffeqs/diffeqs.jl | 4 ++-- src/diffeqs/laplace.jl | 27 ++++++++++++++++----------- test/laplace.jl | 17 ++++++++++++++++- 3 files changed, 34 insertions(+), 14 deletions(-) diff --git a/src/diffeqs/diffeqs.jl b/src/diffeqs/diffeqs.jl index 696a1cfde..2de6a1568 100644 --- a/src/diffeqs/diffeqs.jl +++ b/src/diffeqs/diffeqs.jl @@ -649,7 +649,7 @@ end # takes into account fractions function _true_factors(expr) facs = factors(expr) - true_facs::Vector{Union{Number, Symbolics.BasicSymbolic}} = [] + true_facs::Vector{Num} = [] frac_rule = @rule (~x)/(~y) => [~x, 1/~y] for fac in facs frac = frac_rule(fac) @@ -657,7 +657,7 @@ function _true_factors(expr) append!(true_facs, _true_factors(frac[1])) append!(true_facs, _true_factors(frac[2])) else - push!(true_facs, fac) + push!(true_facs, wrap(fac)) end end diff --git a/src/diffeqs/laplace.jl b/src/diffeqs/laplace.jl index 0cda972da..5c8d0df55 100644 --- a/src/diffeqs/laplace.jl +++ b/src/diffeqs/laplace.jl @@ -1,6 +1,7 @@ import DomainSets.ClosedInterval function laplace(expr, f, t, s, F) + expr = expand(expr) Dt = Differential(t) Ds = Differential(s) # from https://tutorial.math.lamar.edu/Classes/DE/Laplace_Table.aspx @@ -33,10 +34,10 @@ function laplace(expr, f, t, s, F) @rule exp(~a*t) * cos(~b * t) => (-~a+s) / ((~b)^2 + (-~a+s)^2) @rule exp(~a*t) * sinh(~b * t) => ~b / (-(~b)^2 + (-~a+s)^2) @rule exp(~a*t) * cosh(~b * t) => (-~a+s) / (-(~b)^2 + (-~a+s)^2) - @rule exp(~a * t) * t^~n => factorial(~n) / (-~a + s)^(~n + 1) - @rule exp(~a * t) * t => 1 / (-~a + s)^(2) - @rule exp(t) * t^~n => factorial(~n) / (s)^(~n + 1) - @rule exp(t) * t => 1 / (s)^(2) + @rule t^~n * exp(~a * t) => factorial(~n) / (-~a + s)^(~n + 1) + @rule t*exp(~a * t) => 1 / (-~a + s)^(2) + @rule t^~n * exp(t) => factorial(~n) / (s)^(~n + 1) + @rule t*exp(t) => 1 / (s)^(2) @rule exp(~c*t) * ~g => laplace(~g, f, t, s - ~c, F) # s-shift rule @rule t*f(t) => -Ds(F(s)) # s-derivative rule @rule t^(~n)*f(t) => (-1)^(~n) * (Ds^~n)(F(s)) # s-derivative rule @@ -84,11 +85,11 @@ function laplace(expr::Equation, f, t, s, F) end function inverse_laplace(expr, F, t, s, f) - # check for partial fractions - if isequal(expr, 1/((3+s)*(s^2))) - s - partial_frac_decomposition(1/((3+s)*(s^2)), s) + if isequal(expr, 0) + return 0 end + + # check for partial fractions partial_fractions = partial_frac_decomposition(expr, s) if partial_fractions !== nothing && !isequal(partial_fractions, expr) return inverse_laplace(partial_fractions, F, t, s, f) @@ -165,7 +166,7 @@ function laplace_solve_ode(eq, f, t, f0) @syms F(s) transformed_eq = laplace(eq, f, t, s, F) transformed_eq = substitute(transformed_eq, Dict(F(s) => variable(:F), [variable(:f0, i-1) => f0[i] for i=1:length(f0)]...)) - transformed_eq = transformed_eq.lhs - transformed_eq.rhs + transformed_eq = expand(transformed_eq.lhs - transformed_eq.rhs) # transformed_soln = symbolic_solve(transformed_eq, variable(:F)) F_terms = 0 @@ -178,7 +179,11 @@ function laplace_solve_ode(eq, f, t, f0) end end - @show transformed_soln = sum(other_terms ./ F_terms) + if isempty(other_terms) + other_terms = 0 + end + + transformed_soln = simplify(sum(other_terms ./ F_terms)) - return inverse_laplace(transformed_soln, F, t, s, f) + return expand(inverse_laplace(transformed_soln, F, t, s, f)) end \ No newline at end of file diff --git a/test/laplace.jl b/test/laplace.jl index 9db2c9d4d..be67eb31c 100644 --- a/test/laplace.jl +++ b/test/laplace.jl @@ -19,4 +19,19 @@ import Nemo, Groebner @test isequal(inverse_laplace((s-9)/(s^2 + 9), F, t, s, f), cos(3t) - 3sin(3t)) # partial fraction decomposition @test isequal(inverse_laplace((s+2)/(s^2 - 3s - 4), F, t, s, f), (6//5)*exp(4t) - (1//5)*exp(-t)) -@test isequal(inverse_laplace(1/(s^2 - 10s + 9), F, t, s, f), (1//8)*exp(9t) - (1//8)*exp(t)) \ No newline at end of file +@test isequal(inverse_laplace(1/(s^2 - 10s + 9), F, t, s, f), (1//8)*exp(9t) - (1//8)*exp(t)) + +Dt = Differential(t) +@test isequal(laplace_solve_ode(Dt(f(t)) + 3f(t) ~ t^2*exp(-3t) + t*exp(-2t) + t, f, t, [1]), (1//3)*t^3*exp(-3t) + t*exp(-2t) + (1//3)*t + (19//9)*exp(-3t) - exp(-2t) - 1//9) +@test isequal(laplace_solve_ode((Dt^2)(f(t)) - 3Dt(f(t)) + 2f(t) ~ 4, f, t, [2, 3]), 2 - 3exp(t) + 3exp(2t)) +@test isequal(laplace_solve_ode((Dt^3)(f(t)) - Dt(f(t)) ~ 2, f, t, [4,4,4]), 5exp(t) - exp(-t) - 2t) +@test isequal(laplace_solve_ode((Dt^3)(f(t)) - Dt(f(t)) ~ 6 - 3t^2, f, t, [1, 1, 1]), exp(t) + t^3) +@test isequal(laplace_solve_ode((Dt^2)(f(t)) - f(t) ~ 2sin(t), f, t, [0, 0]), (1//2)exp(t) - (1//2)exp(-t) - sin(t)) +@test isequal(laplace_solve_ode((Dt^2)(f(t)) + 2Dt(f(t)) ~ 5f(t), f, t, [0, 0]), 0) +@test isequal(laplace_solve_ode((Dt^2)(f(t)) + f(t) ~ sin(4t), f, t, [0, 0]), (4//15)sin(t) - (1//15)sin(4t)) +@test isequal(laplace_solve_ode((Dt^2)(f(t)) + Dt(f(t)) ~ 1 + 2t, f, t, [0, 0]), 1 - exp(-t) + t^2 - t) +@test isequal(laplace_solve_ode((Dt^2)(f(t)) + 4Dt(f(t)) + 3f(t) ~ 6, f, t, [0, 0]), exp(-3t) - 3exp(-t) + 2) +@test isequal(laplace_solve_ode((Dt^2)(f(t)) - 2Dt(f(t)) ~ 3*(t + exp(2t)), f, t, [0, 0]), (3//8) - (3//4)t - (3//4)t^2 - (3//8)exp(2t) + (3//2)t*exp(2t)) +@test_broken isequal(laplace_solve_ode((Dt^2)(f(t)) - 2Dt(f(t)) ~ 20*exp(-t)*cos(t), f, t, [0, 0]), 3exp(2t) - 5 + 2exp(-t)*cos(t) - 4exp(-t)*sin(t)) # irreducible quadratic in inverse laplace +@test isequal(laplace_solve_ode((Dt^2)(f(t)) + f(t) ~ 2 + 2cos(t), f, t, [0, 0]), 2 - 2cos(t) + t*sin(t)) +@test isequal(laplace_solve_ode((Dt^2)(f(t)) - Dt(f(t)) ~ 30cos(3t), f, t, [0, 0]), 3exp(t) - 3cos(3t) - sin(3t)) \ No newline at end of file From 9203e1c20b10d446dc0ef3919492d8b14baf4f90 Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Fri, 8 Aug 2025 13:15:11 -0400 Subject: [PATCH 32/52] switched to unicode variables to avoid collisions --- src/diffeqs/diffeqs.jl | 75 +++++++++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 31 deletions(-) diff --git a/src/diffeqs/diffeqs.jl b/src/diffeqs/diffeqs.jl index 13d5b7605..e9f8152ae 100644 --- a/src/diffeqs/diffeqs.jl +++ b/src/diffeqs/diffeqs.jl @@ -308,9 +308,9 @@ Returns homogeneous solutions to linear ODE `eq` with constant coefficients xₕ(t) = C₁e^(r₁t) + C₂e^(r₂t) + ... + Cₙe^(rₙt) """ function const_coeff_solve(eq::LinearODE) - @variables r - p = characteristic_polynomial(eq, r) - roots = symbolic_solve(p, r, dropmultiplicity = false) + @variables 𝓇 + p = characteristic_polynomial(eq, 𝓇) + roots = symbolic_solve(p, 𝓇, dropmultiplicity = false) # Handle complex + repeated roots solutions = exp.(roots * eq.t) @@ -348,8 +348,8 @@ function integrating_factor_solve(eq::LinearODE) v = exp(sympy_integrate(p, eq.t)) end solution = (1 / v) * ((isequal(eq.q, 0) ? 0 : sympy_integrate(eq.q * v, eq.t)) + eq.C[1]) - @variables Integral - if !isempty(Symbolics.get_variables(solution, Integral)) + + if !isempty(Symbolics.get_variables(solution, variable(:Integral))) return nothing end return expand(Symbolics.sympy_simplify(solution)) @@ -444,8 +444,18 @@ function exp_trig_particular_solution(eq::LinearODE) return nothing end - combined_eq = LinearODE(eq.x, eq.t, eq.p, a * exp((r + b * im)eq.t)) - rrf = resonant_response_formula(combined_eq) + # do complex rrf + # figure out how many times p needs to be differentiated before denominator isn't 0 + k = 0 + @variables 𝓈 + p = characteristic_polynomial(eq, 𝓈) + Ds = Differential(𝓈) + while isequal(substitute(expand_derivatives((Ds^k)(p)), Dict(𝓈 => r+b*im)), 0) + k += 1 + end + + rrf = expand(simplify(a * exp((r + b * im) * eq.t) * eq.t^k / + (substitute(expand_derivatives((Ds^k)(p)), Dict(𝓈 => r+b*im))))) return is_sin ? imag(rrf) : real(rrf) end @@ -469,15 +479,15 @@ function resonant_response_formula(eq::LinearODE) # figure out how many times p needs to be differentiated before denominator isn't 0 k = 0 - @variables s - p = characteristic_polynomial(eq, s) - Ds = Differential(s) - while isequal(substitute(expand_derivatives((Ds^k)(p)), Dict(s => r)), 0) + @variables 𝓈 + p = characteristic_polynomial(eq, 𝓈) + Ds = Differential(𝓈) + while isequal(substitute(expand_derivatives((Ds^k)(p)), Dict(𝓈 => r)), 0) k += 1 end return expand(simplify(a * exp(r * eq.t) * eq.t^k / - (substitute(expand_derivatives((Ds^k)(p)), Dict(s => r))))) + (substitute(expand_derivatives((Ds^k)(p)), Dict(𝓈 => r))))) end function method_of_undetermined_coefficients(eq::LinearODE) @@ -489,28 +499,31 @@ function method_of_undetermined_coefficients(eq::LinearODE) # polynomial degree = Symbolics.degree(eq.q, eq.t) # just a starting point - a = Symbolics.variables(:a, 1:degree+1) - form = sum(a[n]*eq.t^(n-1) for n = 1:degree+1) + 𝒶 = Symbolics.variables(:a, 1:degree+1) + form = sum(𝒶[n]*eq.t^(n-1) for n = 1:degree+1) eq_subbed = substitute(get_expression(eq), Dict(eq.x => form)) + eq_subbed = eq_subbed.lhs - eq_subbed.rhs eq_subbed = expand_derivatives(eq_subbed) + try - coeff_solution = symbolic_solve(eq_subbed, length(a) == 1 ? a[1] : a) + coeff_solution = solve_interms_ofvar(eq_subbed, eq.t) catch coeff_solution = nothing end - if degree > 0 && coeff_solution !== nothing && !isempty(coeff_solution) && evaluate(eq_subbed, coeff_solution[1]) + + if degree > 0 && coeff_solution !== nothing && !isempty(coeff_solution) && isequal(expand(substitute(eq_subbed, coeff_solution[1])), 0) return substitute(form, coeff_solution[1]) end # exponential - @variables a + @variables 𝒶 coeff = get_rrf_coeff(eq.q, eq.t) if coeff !== nothing r = coeff[2] - form = a*exp(r*eq.t) + form = 𝒶*exp(r*eq.t) eq_subbed = substitute(get_expression(eq), Dict(eq.x => form)) eq_subbed = expand_derivatives(eq_subbed) - coeff_solution = symbolic_solve(eq_subbed, a) + coeff_solution = symbolic_solve(eq_subbed, 𝒶) if coeff_solution !== nothing && !isempty(coeff_solution) return substitute(form, coeff_solution[1]) @@ -519,21 +532,21 @@ function method_of_undetermined_coefficients(eq::LinearODE) # sin and cos # this is a hacky way of doing things - @variables a, b - @variables cs, sn + @variables 𝒶, 𝒷 + @variables 𝒸𝓈, 𝓈𝓃 parsed = _parse_trig(_true_factors(eq.q)[end], eq.t) if parsed !== nothing ω = parsed[1] - form = a*cos(ω*eq.t) + b*sin(ω*eq.t) + form = 𝒶*cos(ω*eq.t) + 𝒷*sin(ω*eq.t) eq_subbed = substitute(get_expression(eq), Dict(eq.x => form)) eq_subbed = expand_derivatives(eq_subbed) - eq_subbed = expand(substitute(eq_subbed.lhs - eq_subbed.rhs, Dict(cos(ω*eq.t)=>cs, sin(ω*eq.t)=>sn))) - cos_eq = simplify(sum(filter(term -> !isempty(Symbolics.get_variables(term, cs)), terms(eq_subbed)))/cs) - sin_eq = simplify(sum(filter(term -> !isempty(Symbolics.get_variables(term, sn)), terms(eq_subbed)))/sn) - if !isempty(Symbolics.get_variables(cos_eq, [eq.t,sn,cs])) || !isempty(Symbolics.get_variables(sin_eq, [eq.t,sn,cs])) + eq_subbed = expand(substitute(eq_subbed.lhs - eq_subbed.rhs, Dict(cos(ω*eq.t)=>𝒸𝓈, sin(ω*eq.t)=>𝓈𝓃))) + cos_eq = simplify(sum(filter(term -> !isempty(Symbolics.get_variables(term, 𝒸𝓈)), terms(eq_subbed)))/𝒸𝓈) + sin_eq = simplify(sum(filter(term -> !isempty(Symbolics.get_variables(term, 𝓈𝓃)), terms(eq_subbed)))/𝓈𝓃) + if !isempty(Symbolics.get_variables(cos_eq, [eq.t,𝓈𝓃,𝒸𝓈])) || !isempty(Symbolics.get_variables(sin_eq, [eq.t,𝓈𝓃,𝒸𝓈])) coeff_solution = nothing else - coeff_solution = symbolic_solve([cos_eq, sin_eq], [a,b]) + coeff_solution = symbolic_solve([cos_eq, sin_eq], [𝒶,𝒷]) end if coeff_solution !== nothing && !isempty(coeff_solution) @@ -681,8 +694,8 @@ end Solve Bernoulli equations of the form dx/dt + p(t)x = q(t)x^n """ function solve_bernoulli(expr, x, t) - @variables v - linearized = linearize_bernoulli(expr, x, t, v) + @variables 𝓋 + linearized = linearize_bernoulli(expr, x, t, 𝓋) if linearized === nothing return nothing end @@ -701,8 +714,8 @@ end Solve Bernoulli equations of the form dx/dt + p(t)x = q(t)x^n with initial condition x(0) = x0 """ function solve_bernoulli(expr, x, t, x0) - @variables v - eq, n = linearize_bernoulli(expr, x, t, v) + @variables 𝓋 + eq, n = linearize_bernoulli(expr, x, t, 𝓋) v0 = x0^(1-n) # convert initial condition from x(0) to v(0) From 0f000b69d8f1ec624de9c853c972215fe60d5f47 Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Mon, 11 Aug 2025 09:54:20 -0400 Subject: [PATCH 33/52] (wip) adding jacobian --- src/diffeqs/diffeqs.jl | 46 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/src/diffeqs/diffeqs.jl b/src/diffeqs/diffeqs.jl index e9f8152ae..2f8464ceb 100644 --- a/src/diffeqs/diffeqs.jl +++ b/src/diffeqs/diffeqs.jl @@ -81,11 +81,24 @@ struct LinearODE end end +function is_linear(eq::Equation, x, t) + all(isempty.(get_variables.(sparse_jacobian_ode(eq, x, t), x))) +end + +# recursively find highest derivative order in `expr` function _get_der_order(expr, x, t) - if isequal(expr, x) + if !hasderiv(unwrap(expr)) return 0 end + if length(terms(expr)) > 1 + return maximum(_get_der_order.(terms(expr), Ref(x), Ref(t))) + end + + if length(factors(expr)) > 1 + return maximum(_get_der_order.(factors(expr), Ref(x), Ref(t))) + end + return _get_der_order(substitute(expr, Dict(Differential(t)(x) => x)), x, t) + 1 end @@ -744,4 +757,35 @@ function _true_factors(expr) end return convert(Vector{Num}, true_facs) +end + +function sparse_jacobian_ode(eq, x, t) + Dt = Differential(t) + n = _get_der_order(eq, x, t) + @assert n >= 1 "ODE must have at least one derivative" + + # reduction of order + y = variables(:y, 1:n) + y_sub = Dict([[(Dt^i)(x) => y[i+1] for i=0:n-1]; (Dt^n)(x) => variable(:y, n+1)]) + eq = substitute(eq, y_sub) + + if !check_polynomial(eq, strict=false) + @warn "Equation is not in a polynomial form" + return nothing + end + + f = symbolic_linear_solve(eq, variable(:y, n+1)) + @assert f !== nothing "Failed to isolate highest order derivative term" + f = f[1] + funcs = [y[2:n]; f] + + result = sparsejacobian(funcs, y) + if all(iszero, result) + return result + end + + rev_y_sub = Dict(val => key for (key, val) in y_sub) + sparse_jacobian::SparseMatrixCSC{Num, <:Integer} = substitute.(result, Ref(rev_y_sub)) + + return sparse_jacobian end \ No newline at end of file From ee951aadcb5a3a71ecc9de6ab7ba78b0b20460ba Mon Sep 17 00:00:00 2001 From: Tori Date: Mon, 11 Aug 2025 13:11:21 -0400 Subject: [PATCH 34/52] switched parsing logic to use linear_expansion, adding reduction of order in the process --- src/diffeqs/diffeqs.jl | 125 ++++++++++++++++++----------------------- 1 file changed, 54 insertions(+), 71 deletions(-) diff --git a/src/diffeqs/diffeqs.jl b/src/diffeqs/diffeqs.jl index 2f8464ceb..603ac2c6f 100644 --- a/src/diffeqs/diffeqs.jl +++ b/src/diffeqs/diffeqs.jl @@ -33,56 +33,47 @@ struct LinearODE function LinearODE(expr, x, t) if expr isa Equation - epxr = expr.lhs - expr.rhs + expr = expr.lhs - expr.rhs end expr = expand(simplify(expr)) - terms = Symbolics.terms(epxr) - p::Vector{Num} = [] - q = 0 - order = 0 - for term in terms - if isequal(Symbolics.get_variables(term, [x]), [x]) - facs = _true_factors(term) - deriv = filter(fac -> Symbolics.hasderiv(Symbolics.value(fac)), facs) - p_n = prod(filter(fac -> !Symbolics.hasderiv(Symbolics.value(fac)), facs)) - if isempty(deriv) - @assert Symbolics.degree(term, x) == 1 "Expected linear term: $term" - if isempty(p) - p = [p_n/x] - else - p[1] = p_n/x - end - continue - end - - @assert length(deriv) == 1 "Expected linear term: $term" - n = _get_der_order(deriv[1], x, t) - if n+1 > length(p) - append!(p, zeros(Int, n-length(p) + 1)) - end - p[n + 1] = p_n - order = max(order, n) - - elseif isempty(Symbolics.get_variables(term, [x])) - q -= term - else - # throw assertion error for invalid term so it can be easily caught - @assert false "Invalid term in LinearODE: $term" - end - end - - # normalize leading coefficient to 1 - leading_coeff = p[order + 1] - p = expand.(p .// leading_coeff) - q = expand(q // leading_coeff) - new(x, t, p[1:order], q) + @assert is_linear_ode(expr, x, t) "Equation must be linear in $x and $t" + + n = _get_der_order(expr, x, t) + + ys = variables(:𝓎, 1:n) + A, b, islinear = linear_expansion(reduce_order(expr, x, t, ys), ys) + + p = expand.(simplify.(-A[end, 1:end])) + q = b[end] + + new(x, t, p, q) end end -function is_linear(eq::Equation, x, t) - all(isempty.(get_variables.(sparse_jacobian_ode(eq, x, t), x))) +function is_linear_ode(expr, x, t) + Dt = Differential(t) + ys = variables(:𝓎, 1:_get_der_order(expr, x, t)) + n = _get_der_order(expr, x, t) + @assert n >= 1 "ODE must have at least one derivative" + + y_sub = Dict([[(Dt^i)(x) => ys[i+1] for i=0:n-1]; (Dt^n)(x) => variable(:𝒴)]) + expr = substitute(expr, y_sub) + + # isolate (Dt^n)(x) + f = symbolic_linear_solve(expr, variable(:𝒴), check=false) + + # couldn't isolate + if f === nothing + return false + end + + f = f[1] + system = [ys[2:n]; f] + + A, b, islinear = linear_expansion(system, ys) + return islinear && all(isempty.(get_variables.(A, x))) end # recursively find highest derivative order in `expr` @@ -258,16 +249,9 @@ function symbolic_solve_ode(expr::Equation, x, t) return bernoulli end - try + if is_linear_ode(expr, x, t) eq = LinearODE(expr, x, t) return symbolic_solve_ode(eq) - catch e - if e isa AssertionError - @warn e - return nothing - else - throw(e) - end end end @@ -512,8 +496,8 @@ function method_of_undetermined_coefficients(eq::LinearODE) # polynomial degree = Symbolics.degree(eq.q, eq.t) # just a starting point - 𝒶 = Symbolics.variables(:a, 1:degree+1) - form = sum(𝒶[n]*eq.t^(n-1) for n = 1:degree+1) + a = Symbolics.variables(:𝒶, 1:degree+1) + form = sum(a[n]*eq.t^(n-1) for n = 1:degree+1) eq_subbed = substitute(get_expression(eq), Dict(eq.x => form)) eq_subbed = eq_subbed.lhs - eq_subbed.rhs eq_subbed = expand_derivatives(eq_subbed) @@ -759,33 +743,32 @@ function _true_factors(expr) return convert(Vector{Num}, true_facs) end -function sparse_jacobian_ode(eq, x, t) +""" + reduce_order(eq, x, t, ys) + +Reduce order of an ODE by substituting variables for derivatives to form a system of first order ODEs +""" +function reduce_order(eq, x, t, ys) Dt = Differential(t) n = _get_der_order(eq, x, t) @assert n >= 1 "ODE must have at least one derivative" # reduction of order - y = variables(:y, 1:n) - y_sub = Dict([[(Dt^i)(x) => y[i+1] for i=0:n-1]; (Dt^n)(x) => variable(:y, n+1)]) + y_sub = Dict([[(Dt^i)(x) => ys[i+1] for i=0:n-1]; (Dt^n)(x) => variable(:𝒴)]) eq = substitute(eq, y_sub) - if !check_polynomial(eq, strict=false) - @warn "Equation is not in a polynomial form" - return nothing - end - - f = symbolic_linear_solve(eq, variable(:y, n+1)) + # isolate (Dt^n)(x) + f = symbolic_linear_solve(eq, variable(:𝒴), check=false) @assert f !== nothing "Failed to isolate highest order derivative term" f = f[1] - funcs = [y[2:n]; f] + system = [ys[2:n]; f] - result = sparsejacobian(funcs, y) - if all(iszero, result) - return result - end + return system +end - rev_y_sub = Dict(val => key for (key, val) in y_sub) - sparse_jacobian::SparseMatrixCSC{Num, <:Integer} = substitute.(result, Ref(rev_y_sub)) - - return sparse_jacobian +function unreduce_order(expr, x, t, ys) + Dt = Differential(t) + rev_y_sub = Dict(ys[i] => (Dt^(i-1))(x) for i in 1:length(ys)) + + return substitute(expr, rev_y_sub) end \ No newline at end of file From a94a6dfff886745b99395cf462ccda041d5059c3 Mon Sep 17 00:00:00 2001 From: Tori Date: Mon, 11 Aug 2025 13:39:58 -0400 Subject: [PATCH 35/52] reorganized some methods into new helpers file --- src/Symbolics.jl | 1 + src/diffeqs/diffeq_helpers.jl | 97 +++++++++++++++++++++++++++++++++++ src/diffeqs/diffeqs.jl | 91 -------------------------------- 3 files changed, 98 insertions(+), 91 deletions(-) create mode 100644 src/diffeqs/diffeq_helpers.jl diff --git a/src/Symbolics.jl b/src/Symbolics.jl index c049b395b..470fda59b 100644 --- a/src/Symbolics.jl +++ b/src/Symbolics.jl @@ -221,6 +221,7 @@ export symbolic_solve # Diff Eq Solver include("diffeqs/diffeqs.jl") include("diffeqs/systems.jl") +include("diffeqs/diffeq_helpers.jl") export LinearODE, IVP, symbolic_solve_ode, solve_linear_system, solve_IVP # Sympy Functions diff --git a/src/diffeqs/diffeq_helpers.jl b/src/diffeqs/diffeq_helpers.jl new file mode 100644 index 000000000..a4cbbd470 --- /dev/null +++ b/src/diffeqs/diffeq_helpers.jl @@ -0,0 +1,97 @@ +# recursively find highest derivative order in `expr` +function _get_der_order(expr, x, t) + if !hasderiv(unwrap(expr)) + return 0 + end + + if length(terms(expr)) > 1 + return maximum(_get_der_order.(terms(expr), Ref(x), Ref(t))) + end + + if length(factors(expr)) > 1 + return maximum(_get_der_order.(factors(expr), Ref(x), Ref(t))) + end + + return _get_der_order(substitute(expr, Dict(Differential(t)(x) => x)), x, t) + 1 +end + +# takes into account fractions +function _true_factors(expr) + facs = factors(expr) + true_facs::Vector{Number} = [] + frac_rule = @rule (~x)/(~y) => [~x, 1/~y] + for fac in facs + frac = frac_rule(fac) + if frac !== nothing && !isequal(frac[1], 1) + append!(true_facs, _true_factors(frac[1])) + append!(true_facs, _true_factors(frac[2])) + else + push!(true_facs, fac) + end + end + + return convert(Vector{Num}, true_facs) +end + +""" + reduce_order(eq, x, t, ys) + +Reduce order of an ODE by substituting variables for derivatives to form a system of first order ODEs +""" +function reduce_order(eq, x, t, ys) + Dt = Differential(t) + n = _get_der_order(eq, x, t) + @assert n >= 1 "ODE must have at least one derivative" + + # reduction of order + y_sub = Dict([[(Dt^i)(x) => ys[i+1] for i=0:n-1]; (Dt^n)(x) => variable(:𝒴)]) + eq = substitute(eq, y_sub) + + # isolate (Dt^n)(x) + f = symbolic_linear_solve(eq, variable(:𝒴), check=false) + @assert f !== nothing "Failed to isolate highest order derivative term" + f = f[1] + system = [ys[2:n]; f] + + return system +end + +function unreduce_order(expr, x, t, ys) + Dt = Differential(t) + rev_y_sub = Dict(ys[i] => (Dt^(i-1))(x) for i in eachindex(ys)) + + return substitute(expr, rev_y_sub) +end + +function is_solution(solution, eq::Equation, x, t) + is_solution(solution, eq.lhs - eq.rhs, x, t) +end + +function is_solution(solution, eq::LinearODE) + is_solution(solution, get_expression(eq), eq.x, eq.t) +end + +function is_solution(solution, eq, x, t) + if solution === nothing + return false + end + + expr = substitute(eq, Dict(x => solution)) + expr = expand(expand_derivatives(expr)) + return isequal(expr, 0) +end + +function _parse_trig(expr, t) + parse_sin = Symbolics.Chain([(@rule sin(t) => 1), (@rule sin(~x * t) => ~x)]) + parse_cos = Symbolics.Chain([(@rule cos(t) => 1), (@rule cos(~x * t) => ~x)]) + + if !isequal(parse_sin(expr), expr) + return parse_sin(expr), true + end + + if !isequal(parse_cos(expr), expr) + return parse_cos(expr), false + end + + return nothing +end \ No newline at end of file diff --git a/src/diffeqs/diffeqs.jl b/src/diffeqs/diffeqs.jl index 603ac2c6f..e432be54d 100644 --- a/src/diffeqs/diffeqs.jl +++ b/src/diffeqs/diffeqs.jl @@ -76,23 +76,6 @@ function is_linear_ode(expr, x, t) return islinear && all(isempty.(get_variables.(A, x))) end -# recursively find highest derivative order in `expr` -function _get_der_order(expr, x, t) - if !hasderiv(unwrap(expr)) - return 0 - end - - if length(terms(expr)) > 1 - return maximum(_get_der_order.(terms(expr), Ref(x), Ref(t))) - end - - if length(factors(expr)) > 1 - return maximum(_get_der_order.(factors(expr), Ref(x), Ref(t))) - end - - return _get_der_order(substitute(expr, Dict(Differential(t)(x) => x)), x, t) + 1 -end - Dt(eq::LinearODE) = Differential(eq.t) order(eq::LinearODE) = length(eq.p) @@ -395,21 +378,6 @@ function get_rrf_coeff(q, t) return a, r end -function _parse_trig(expr, t) - parse_sin = Symbolics.Chain([(@rule sin(t) => 1), (@rule sin(~x * t) => ~x)]) - parse_cos = Symbolics.Chain([(@rule cos(t) => 1), (@rule cos(~x * t) => ~x)]) - - if !isequal(parse_sin(expr), expr) - return parse_sin(expr), true - end - - if !isequal(parse_cos(expr), expr) - return parse_cos(expr), false - end - - return nothing -end - """ For finding particular solution when q(t) = a*e^(rt)*cos(bt) (or sin(bt)) """ @@ -552,16 +520,6 @@ function method_of_undetermined_coefficients(eq::LinearODE) end end -function is_solution(solution, eq) - if solution === nothing - return false - end - - expr = substitute(get_expression(eq), Dict(eq.x => solution)) - expr = expand(expand_derivatives(expr.lhs - expr.rhs)) - return isequal(expr, 0) -end - """ Initial value problem (IVP) for a linear ODE """ @@ -575,7 +533,6 @@ struct IVP end end - function solve_IVP(ivp::IVP) general_solution = symbolic_solve_ode(ivp.eq) if general_solution === nothing @@ -723,52 +680,4 @@ function solve_bernoulli(expr, x, t, x0) end return symbolic_solve(solution ~ x^(1-n), x) -end - -# takes into account fractions -function _true_factors(expr) - facs = factors(expr) - true_facs::Vector{Number} = [] - frac_rule = @rule (~x)/(~y) => [~x, 1/~y] - for fac in facs - frac = frac_rule(fac) - if frac !== nothing && !isequal(frac[1], 1) - append!(true_facs, _true_factors(frac[1])) - append!(true_facs, _true_factors(frac[2])) - else - push!(true_facs, fac) - end - end - - return convert(Vector{Num}, true_facs) -end - -""" - reduce_order(eq, x, t, ys) - -Reduce order of an ODE by substituting variables for derivatives to form a system of first order ODEs -""" -function reduce_order(eq, x, t, ys) - Dt = Differential(t) - n = _get_der_order(eq, x, t) - @assert n >= 1 "ODE must have at least one derivative" - - # reduction of order - y_sub = Dict([[(Dt^i)(x) => ys[i+1] for i=0:n-1]; (Dt^n)(x) => variable(:𝒴)]) - eq = substitute(eq, y_sub) - - # isolate (Dt^n)(x) - f = symbolic_linear_solve(eq, variable(:𝒴), check=false) - @assert f !== nothing "Failed to isolate highest order derivative term" - f = f[1] - system = [ys[2:n]; f] - - return system -end - -function unreduce_order(expr, x, t, ys) - Dt = Differential(t) - rev_y_sub = Dict(ys[i] => (Dt^(i-1))(x) for i in 1:length(ys)) - - return substitute(expr, rev_y_sub) end \ No newline at end of file From d7eeccbfe97c9605735f344d35676751ebee1d42 Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Mon, 18 Aug 2025 09:30:24 -0400 Subject: [PATCH 36/52] add simplification to q when parsing --- src/diffeqs/diffeqs.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/diffeqs/diffeqs.jl b/src/diffeqs/diffeqs.jl index e432be54d..10381fa46 100644 --- a/src/diffeqs/diffeqs.jl +++ b/src/diffeqs/diffeqs.jl @@ -46,7 +46,7 @@ struct LinearODE A, b, islinear = linear_expansion(reduce_order(expr, x, t, ys), ys) p = expand.(simplify.(-A[end, 1:end])) - q = b[end] + q = expand(simplify(b[end])) new(x, t, p, q) end From 3a533865b9067201bc02be38d94ca9c7088007e8 Mon Sep 17 00:00:00 2001 From: Tori Date: Mon, 18 Aug 2025 15:12:06 -0400 Subject: [PATCH 37/52] possible fix for SymPy dependency unable to test on laptop. will test on different computer soon --- src/diffeqs/diffeqs.jl | 4 ++-- test/diffeqs.jl | 14 ++++---------- test/sympy.jl | 11 +++++++++++ 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/diffeqs/diffeqs.jl b/src/diffeqs/diffeqs.jl index 10381fa46..1e09a56ab 100644 --- a/src/diffeqs/diffeqs.jl +++ b/src/diffeqs/diffeqs.jl @@ -317,7 +317,7 @@ function const_coeff_solve(eq::LinearODE) end """ -Solve almost any first order ODE using an integrating factor +Solve almost any first order ODE using an integrating factor. Requires SymPy! """ function integrating_factor_solve(eq::LinearODE) p = eq.p[1] # only p @@ -645,7 +645,7 @@ function linearize_bernoulli(expr, x, t, v) end """ -Solve Bernoulli equations of the form dx/dt + p(t)x = q(t)x^n +Solve Bernoulli equations of the form dx/dt + p(t)x = q(t)x^n. May require SymPy to solve using integrating factor """ function solve_bernoulli(expr, x, t) @variables 𝓋 diff --git a/test/diffeqs.jl b/test/diffeqs.jl index c37a06558..0cfcab04c 100644 --- a/test/diffeqs.jl +++ b/test/diffeqs.jl @@ -1,6 +1,6 @@ using Symbolics using Symbolics: solve_linear_system, LinearODE, has_const_coeffs, to_homogeneous, symbolic_solve_ode, find_particular_solution, IVP, solve_IVP -using Groebner, Nemo, SymPy +using Groebner, Nemo using Test @variables x, y, t @@ -36,12 +36,7 @@ C = Symbolics.variables(:C, 1:5) @test isequal(symbolic_solve_ode(LinearODE(x, t, [-1], 0)), C[1]*exp(t)) @test isequal(symbolic_solve_ode(LinearODE(x, t, [-4, 3], 0)), C[1]*exp(-4t) + C[2]*exp(t)) -## first order -@test isequal(symbolic_solve_ode(LinearODE(x, t, [5/t], 7t)), Symbolics.sympy_simplify(C[1]*t^(-5) + t^2)) -@test isequal(symbolic_solve_ode(LinearODE(x, t, [cos(t)], cos(t))), 1 + C[1]*exp(-sin(t))) -@test isequal(symbolic_solve_ode(LinearODE(x, t, [-(1+t)], 1+t)), Symbolics.expand(Symbolics.sympy_simplify(C[1]*exp((1//2)t^2 + t) - 1))) -# SymPy is being weird and not simplifying correctly (and some symbols are wrong, like pi and erf being syms), but these otherwise work -@test_broken isequal(symbolic_solve_ode(LinearODE(x, t, [-2t], 1)), Symbolics.sympy_simplify(exp(t^2)*sqrt(Symbolics.variable(:pi))*erf(t)/2 + C[1]*exp(t^2))) +## first order (solving via integrating factor can be found in test/sympy.jl) @test isequal(symbolic_solve_ode(LinearODE(x, t, [1], 2sin(t))), C[1]*exp(-t) + sin(t) - cos(t)) ## repeated characteristic roots @@ -77,6 +72,5 @@ Dt = Symbolics.Differential(t) @test isequal(symbolic_solve_ode(x ~ Dt(x)*t - ((Dt(x))^3), x, t), C[1]*t - C[1]^3) @test isequal(symbolic_solve_ode(x ~ Dt(x)*t + (Dt(x))^2 - sin(Dt(x)) + 2, x, t), C[1]*t + C[1]^2 - sin(C[1]) + 2) -## Bernoulli equations -@test isequal(symbolic_solve_ode(Dt(x) - 5x ~ exp(-2t)*x^(-2), x, t), (C[1]exp(15t) - (3//17)exp(-2t))^(1//3)) -@test isequal(symbolic_solve_ode(Dt(x) + (4//t)*x ~ t^3 * x^2, x, t), 1/(C[1]t^4 - t^4 * log(t))) \ No newline at end of file +## Bernoulli equations (integrating factor solve in test/sympy.jl) +@test isequal(symbolic_solve_ode(Dt(x) - 5x ~ exp(-2t)*x^(-2), x, t), (C[1]exp(15t) - (3//17)exp(-2t))^(1//3)) \ No newline at end of file diff --git a/test/sympy.jl b/test/sympy.jl index 37e3ba0dd..55da101af 100644 --- a/test/sympy.jl +++ b/test/sympy.jl @@ -69,3 +69,14 @@ result = sympy_simplify(expr) # ode = D(f) - 2*f # sol_ode = sympy_ode_solve(ode, f, x) # @test isequal(sol_ode, Symbolics.parse("C1*exp(2*x)", Dict("f"=>f, "x"=>x))) +@variables x, t +Dt = Differential(t) + +@test isequal(symbolic_solve_ode(LinearODE(x, t, [5/t], 7t)), Symbolics.sympy_simplify(C[1]*t^(-5) + t^2)) +@test isequal(symbolic_solve_ode(LinearODE(x, t, [cos(t)], cos(t))), 1 + C[1]*exp(-sin(t))) +@test isequal(symbolic_solve_ode(LinearODE(x, t, [-(1+t)], 1+t)), Symbolics.expand(Symbolics.sympy_simplify(C[1]*exp((1//2)t^2 + t) - 1))) +# SymPy is being weird and not simplifying correctly (and some symbols are wrong, like pi and erf being syms), but these otherwise work +@test_broken isequal(symbolic_solve_ode(LinearODE(x, t, [-2t], 1)), Symbolics.sympy_simplify(exp(t^2)*sqrt(Symbolics.variable(:pi))*erf(t)/2 + C[1]*exp(t^2))) + +## Bernoulli equations +@test isequal(symbolic_solve_ode(Dt(x) + (4//t)*x ~ t^3 * x^2, x, t), 1/(C[1]t^4 - t^4 * log(t))) \ No newline at end of file From 7ba9be2ddd4786ce1170160b184394fed94d3ccc Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Tue, 19 Aug 2025 03:14:36 -0400 Subject: [PATCH 38/52] fix minor spelling errors --- src/diffeqs/diffeqs.jl | 2 +- src/diffeqs/systems.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/diffeqs/diffeqs.jl b/src/diffeqs/diffeqs.jl index 10381fa46..8e5c0b4ab 100644 --- a/src/diffeqs/diffeqs.jl +++ b/src/diffeqs/diffeqs.jl @@ -191,7 +191,7 @@ Symbolically solve an ODE - t: independent variable # Supported Methods -- all methods of solving linear ODEs mentioend for `symbolic_solve_ode(eq::LinearODE)` +- all methods of solving linear ODEs mentioned for `symbolic_solve_ode(eq::LinearODE)` - Clairaut's equation - Bernoulli equations diff --git a/src/diffeqs/systems.jl b/src/diffeqs/systems.jl index 378b3c0e6..9ab7546ff 100644 --- a/src/diffeqs/systems.jl +++ b/src/diffeqs/systems.jl @@ -11,7 +11,7 @@ Solve linear continuous dynamical system of differential equations of the form A # Arguments - `A`: matrix of coefficients -- `x0`: intial conditions vector +- `x0`: initial conditions vector - `t`: independent variable # Returns From b8704f63c812aae9a1cfe907ffe5d13d0d544049 Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Tue, 19 Aug 2025 04:15:19 -0400 Subject: [PATCH 39/52] working tests for sympy --- test/sympy.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/sympy.jl b/test/sympy.jl index 5452539f1..3da717f5f 100644 --- a/test/sympy.jl +++ b/test/sympy.jl @@ -82,7 +82,8 @@ canonical_sol_ode = Symbolics.substitute(sol_ode, Dict(const_sym => C1)) ## Native ODE solver, but using sympy_integrate @variables x, t -Dt = Differential(t) +Dt = Symbolics.Differential(t) +C = Symbolics.variables(:C, 1:5) @test isequal(symbolic_solve_ode(LinearODE(x, t, [5/t], 7t)), Symbolics.sympy_simplify(C[1]*t^(-5) + t^2)) @test isequal(symbolic_solve_ode(LinearODE(x, t, [cos(t)], cos(t))), 1 + C[1]*exp(-sin(t))) From 1498a6c41bfe584dd1e46a57124ebfd9a1bb25de Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Wed, 20 Aug 2025 01:16:46 -0400 Subject: [PATCH 40/52] fixed tests --- test/partialfractions.jl | 12 ++++++------ test/runtests.jl | 2 ++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/test/partialfractions.jl b/test/partialfractions.jl index d7a5d6ab2..bcf9dd3cd 100644 --- a/test/partialfractions.jl +++ b/test/partialfractions.jl @@ -11,15 +11,15 @@ import Symbolics: partial_frac_decomposition # https://tutorial.math.lamar.edu/Problems/Alg/PartialFractions.aspx # can't handle leading coefficients being not 1 in denominator -@test isequal(partial_frac_decomposition((17x-53)/(x^2 - 2x - 15), x), expand(4/(x-5) + 13/(x+3))) +@test isequal(partial_frac_decomposition((17x-53)/(x^2 - 2x - 15), x), 4/(x-5) + 13/(x+3)) @test_broken isequal(partial_frac_decomposition((34-12x)/(3x^2 - 10x - 8), x), -9(3x+2) - 1/(x-4)) @test isequal(partial_frac_decomposition((125 + 4x - 9x^2)/((x-1)*(x+3)*(x+4)), x), expand(6/(x-1) - 8/(x+3) - 7/(x+4))) -@test isequal(partial_frac_decomposition((10x+35)/((x+4)^2), x), expand(10/(x+4) - 5/(x+4)^2)) +@test isequal(partial_frac_decomposition((10x+35)/((x+4)^2), x), 10/(x+4) + -5/(x+4)^2) @test_broken isequal(partial_frac_decomposition((6x+5)/((2x-1)^2), x), 3/(2x-1) + 8/(2x-1)^2) -@test isequal(partial_frac_decomposition((7x^2-17x+38)/((x+6)*(x-1)^2), x), expand(8/(x+6) - 1/(x-1) + 4/(x-1)^2)) -@test_broken isequal(partial_frac_decomposition((4x^2 - 22x + 7)/((2x+3)*(x-2)^2), x), 4/(2x+3) - 3/(x-2)^2) -@test_broken isequal(partial_frac_decomposition((3x^2 + 7x + 28)/(x*(x^2 + x + 7)), x), expand(4/x + (3-x)/(x^2+x+7))) # irrational roots -@test isequal(partial_frac_decomposition((4x^3 + 16x + 7)/(x^2 + 4)^2, x), expand(4x/(x^2+4) + 7/(x^2+4)^2)) +@test isequal(partial_frac_decomposition((7x^2-17x+38)/((x+6)*(x-1)^2), x), 8/(x+6) + -1/(x-1) + 4/(x-1)^2) +@test_broken isequal(partial_frac_decomposition((4x^2 - 22x + 7)/((2x+3)*(x-2)^2), x), 4/(2x+3) + -3/(x-2)^2) +@test_broken isequal(partial_frac_decomposition((3x^2 + 7x + 28)/(x*(x^2 + x + 7)), x), 4/x + (3-x)/(x^2+x+7)) # irrational roots +@test isequal(partial_frac_decomposition((4x^3 + 16x + 7)/(x^2 + 4)^2, x), 4x/(x^2+4) + 7/(x^2+4)^2) # check valid expressions @test partial_frac_decomposition(sin(x), x) === nothing diff --git a/test/runtests.jl b/test/runtests.jl index d5725fe40..9e7271716 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -71,6 +71,8 @@ if GROUP == "All" || GROUP == "Core" @safetestset "Taylor Series Test" begin include("taylor.jl") end @safetestset "Discontinuity registration test" begin include("discontinuities.jl") end @safetestset "ODE solver test" begin include("diffeqs.jl") end + @safetestset "Laplace transform test" begin include("laplace.jl") end + @safetestset "Partial Fraction Decomposition Test" begin include("partialfractions.jl") end end end From 9eeeafc78c861a093f6c81e1ef6bd4430ad96768 Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Wed, 20 Aug 2025 03:20:20 -0400 Subject: [PATCH 41/52] better codecov and minor bug fixes --- src/diffeqs/diffeqs.jl | 34 +++++++++------------------------- test/diffeqs.jl | 25 +++++++++++++++++++++---- test/sympy.jl | 1 + 3 files changed, 31 insertions(+), 29 deletions(-) diff --git a/src/diffeqs/diffeqs.jl b/src/diffeqs/diffeqs.jl index 69b93cff2..73d1acd33 100644 --- a/src/diffeqs/diffeqs.jl +++ b/src/diffeqs/diffeqs.jl @@ -297,7 +297,7 @@ function const_coeff_solve(eq::LinearODE) for i in eachindex(solutions)[1:(end - 1)] j = i + 1 - if imag(roots[i]) != 0 && roots[i] == conj(roots[j]) + if !isequal(imag(roots[i]), 0) && isequal(roots[i], conj(roots[j])) solutions[i] = exp(real(roots[i] * eq.t)) * cos(imag(roots[i] * eq.t)) solutions[j] = exp(real(roots[i] * eq.t)) * sin(imag(roots[i] * eq.t)) end @@ -339,7 +339,7 @@ end Returns a, r from q(t)=a*e^(rt) if it is of that form. If not, returns `nothing` """ function get_rrf_coeff(q, t) - facs = _true_factors(q) + facs = factors(q) # handle complex r # very convoluted, could probably be improved (possibly by making heavier use of @rule) @@ -463,7 +463,7 @@ function method_of_undetermined_coefficients(eq::LinearODE) end # polynomial - degree = Symbolics.degree(eq.q, eq.t) # just a starting point + degree = max(Symbolics.degree(eq.q, eq.t), Symbolics.degree.(eq.p, eq.t)...) # just a starting point a = Symbolics.variables(:𝒶, 1:degree+1) form = sum(a[n]*eq.t^(n-1) for n = 1:degree+1) eq_subbed = substitute(get_expression(eq), Dict(eq.x => form)) @@ -481,14 +481,16 @@ function method_of_undetermined_coefficients(eq::LinearODE) end # exponential - @variables 𝒶 coeff = get_rrf_coeff(eq.q, eq.t) if coeff !== nothing + a_form = form # use form from polynomial case + r = coeff[2] - form = 𝒶*exp(r*eq.t) + form = a_form*exp(r*eq.t) eq_subbed = substitute(get_expression(eq), Dict(eq.x => form)) eq_subbed = expand_derivatives(eq_subbed) - coeff_solution = symbolic_solve(eq_subbed, 𝒶) + eq_subbed = simplify(expand((eq_subbed.lhs - eq_subbed.rhs) / exp(r*eq.t))) + coeff_solution = solve_interms_ofvar(eq_subbed, eq.t) if coeff_solution !== nothing && !isempty(coeff_solution) return substitute(form, coeff_solution[1]) @@ -619,7 +621,7 @@ function linearize_bernoulli(expr, x, t, v) if Symbolics.hasderiv(Symbolics.value(term)) facs = _true_factors(term) leading_coeff = prod(filter(fac -> !Symbolics.hasderiv(Symbolics.value(fac)), facs)) - if _get_der_order(term//leading_coeff, x, t) != 1 + if !isequal(term//leading_coeff, Dt(x)) return nothing end elseif !isempty(Symbolics.get_variables(term, [x])) @@ -662,22 +664,4 @@ function solve_bernoulli(expr, x, t) end return simplify(solution^(1//(1-n))) -end - -""" -Solve Bernoulli equations of the form dx/dt + p(t)x = q(t)x^n with initial condition x(0) = x0 -""" -function solve_bernoulli(expr, x, t, x0) - @variables 𝓋 - eq, n = linearize_bernoulli(expr, x, t, 𝓋) - - v0 = x0^(1-n) # convert initial condition from x(0) to v(0) - - ivp = IVP(eq, [v0]) - solution = solve_IVP(ivp) - if solution === nothing - return nothing - end - - return symbolic_solve(solution ~ x^(1-n), x) end \ No newline at end of file diff --git a/test/diffeqs.jl b/test/diffeqs.jl index 0cfcab04c..fd8a7173f 100644 --- a/test/diffeqs.jl +++ b/test/diffeqs.jl @@ -4,6 +4,7 @@ using Groebner, Nemo using Test @variables x, y, t +Dt = Symbolics.Differential(t) # Systems # ideally, `isapprox` would all be `isequal`, but there seem to be some floating point inaccuracies @@ -21,6 +22,10 @@ using Test @test isequal(solve_linear_system([1 -1 0; 1 2 1; -2 1 -1], [7, 2, 3], t), (5//3)*exp(-t)*[-1, -2, 7] - 14exp(t)*[-1, 0, 1] + (16//3)*exp(2t)*[-1, 1, 1]) +@test_throws ArgumentError solve_linear_system([1 2; 3 4], [1, 2, 3], t) # mismatch between A and x0 +@test_throws ArgumentError solve_linear_system([1 2 3; 4 5 6], [1, 2], t) # A isn't square +@test_throws ArgumentError Symbolics.solve_uncoupled_system([1 2; 3 4], [1, 2], t) # A isn't diagonal + # LinearODEs @test Symbolics.is_homogeneous(LinearODE(x, t, [1, 1], 0)) @test !Symbolics.is_homogeneous(LinearODE(x, t, [t, 1], t^2)) @@ -29,6 +34,7 @@ using Test @test !has_const_coeffs(LinearODE(x, t, [t^2, 1], 0)) @test Symbolics.is_homogeneous(to_homogeneous(LinearODE(x, t, [t, 1], t^2))) +@test !Symbolics.is_linear_ode(((Dt^2)(x))^2 ~ x^3, x, t) C = Symbolics.variables(:C, 1:5) @@ -40,7 +46,7 @@ C = Symbolics.variables(:C, 1:5) @test isequal(symbolic_solve_ode(LinearODE(x, t, [1], 2sin(t))), C[1]*exp(-t) + sin(t) - cos(t)) ## repeated characteristic roots -@test isequal(symbolic_solve_ode(LinearODE(x, t, [1, 2], 0)), C[1]*exp(-t) + C[2]*t*exp(-t)) +@test isequal(symbolic_solve_ode((Dt^2)(x) + 2(Dt^1)(x) + x ~ 0, x, t), C[1]*exp(-t) + C[2]*t*exp(-t)) @test isequal(symbolic_solve_ode(LinearODE(x, t, [0, 0, 0, 4, -4], 0)), C[1] + C[2]*t + C[3]*t^2 + C[4]*exp(2t) + C[5]*t*exp(2t)) @test isequal(symbolic_solve_ode(LinearODE(x, t, [8, 12, 6], 0)), C[1]*exp(-2t) + C[2]*t*exp(-2t) + C[3]*t^2*exp(-2t)) @@ -58,7 +64,6 @@ C = Symbolics.variables(:C, 1:5) @test isequal(find_particular_solution(LinearODE(x, t, [1, 0], t^2)), t^2 - 2) # Parsing -Dt = Symbolics.Differential(t) @test isequal(LinearODE(x, t, [1], 0), LinearODE(Dt(x) + x ~ 0, x, t)) @test isequal(LinearODE(x, t, [sin(t), 0, 3t^2], exp(2t) + 2cos(t)), LinearODE(6t^2*(Dt^2)(x) + 2sin(t)*x - 2exp(2t) + 2(Dt^3)(x) ~ 4cos(t), x, t)) @@ -70,7 +75,19 @@ Dt = Symbolics.Differential(t) ## Clairaut's equation @test isequal(symbolic_solve_ode(x ~ Dt(x)*t - ((Dt(x))^3), x, t), C[1]*t - C[1]^3) -@test isequal(symbolic_solve_ode(x ~ Dt(x)*t + (Dt(x))^2 - sin(Dt(x)) + 2, x, t), C[1]*t + C[1]^2 - sin(C[1]) + 2) +@test isequal(symbolic_solve_ode(Dt(x)*t + (Dt(x))^2 - sin(Dt(x)) + 2 ~ x, x, t), C[1]*t + C[1]^2 - sin(C[1]) + 2) +@test isnothing(symbolic_solve_ode(Dt(x) + (Dt(x))^2 ~ x, x, t)) +@test isnothing(symbolic_solve_ode(Dt(x)*t + 2t*(Dt(x))^2 ~ x, x, t)) +@test isnothing(symbolic_solve_ode(Dt(x) + x*(Dt(x))^2 ~ x, x, t)) ## Bernoulli equations (integrating factor solve in test/sympy.jl) -@test isequal(symbolic_solve_ode(Dt(x) - 5x ~ exp(-2t)*x^(-2), x, t), (C[1]exp(15t) - (3//17)exp(-2t))^(1//3)) \ No newline at end of file +@test isequal(symbolic_solve_ode(Dt(x) - 5x ~ exp(-2t)*x^(-2), x, t), (C[1]exp(15t) - (3//17)exp(-2t))^(1//3)) +@test isnothing(symbolic_solve_ode(sqrt((Dt^4)(x)) ~ log(x)^t, x, t)) + +# Helper function tests +ys = Symbolics.variables(:y, 1:2) +@test isequal(Symbolics.reduce_order((Dt^2)(x) + 3Dt(x) + 2x ~ 0, x, t, ys), [ys[2], -2ys[1] - 3ys[2]]) +@test isequal(Symbolics.unreduce_order([ys[1], ys[2]], x, t, ys), [x, Dt(x)]) + +@test Symbolics.is_solution(C[1]*exp(3t) + C[2]*t*exp(3t) + 2(t^2)*exp(3t), LinearODE(x, t, [9, -6], 4exp(3t))) +@test Symbolics.is_solution(C[1]*exp(-t) + C[2]*t*exp(-t), (Dt^2)(x) + 2(Dt^1)(x) + x ~ 0, x, t) \ No newline at end of file diff --git a/test/sympy.jl b/test/sympy.jl index 3da717f5f..3617b8520 100644 --- a/test/sympy.jl +++ b/test/sympy.jl @@ -93,6 +93,7 @@ C = Symbolics.variables(:C, 1:5) ## Bernoulli equations @test isequal(symbolic_solve_ode(Dt(x) + (4//t)*x ~ t^3 * x^2, x, t), 1/(C[1]t^4 - t^4 * log(t))) +@test isnothing(symbolic_solve_ode(Dt(x) + sqrt(t)*x ~ sin(4t)*x^3, x, t)) # Test issue #1605: Power with symbolic exponent @variables x y From e497107812cffb78f7ca21021e11f7fa7400e49e Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Thu, 21 Aug 2025 14:13:59 -0400 Subject: [PATCH 42/52] now accounts for non-one leading coefficient in denominator --- src/partialfractions.jl | 41 ++++++++++++++++++++-------------------- test/partialfractions.jl | 12 ++++++------ test/runtests.jl | 1 + 3 files changed, 28 insertions(+), 26 deletions(-) diff --git a/src/partialfractions.jl b/src/partialfractions.jl index fdf33047c..ff8bcc42f 100644 --- a/src/partialfractions.jl +++ b/src/partialfractions.jl @@ -35,36 +35,33 @@ function partial_frac_decomposition(expr, x) return nothing end - facs = 0 - try - facs = factorize(B, x) - catch AssertionError + facs = factorize(B, x) + if facs === nothing return nothing end - v = simplify(B / prod((f -> f.expr^f.multiplicity).(facs))) + leading_coeff = coeff_vector(expand(B), x)[end] #simplify(B / prod((f -> f.expr^f.multiplicity).(facs))) if length(facs) == 1 && only(facs).multiplicity == 1 && degree(A) <= 1 return expr end - result = 0 - + result = [] c_idx = 0 if length(facs) == 1 fac = only(facs) if fac.root === nothing for i = 1:fac.multiplicity - result += (variable(:C, c_idx+=1)*x + variable(:C, c_idx+=1))/(fac.expr^i) + push!(result, (variable(:C, c_idx+=1)*x + variable(:C, c_idx+=1))/(fac.expr^i)) end else - result += sum(variables(:C, (c_idx+1):(c_idx+=fac.multiplicity)) ./ fac.expr.^(1:fac.multiplicity)) + append!(result, variables(:C, (c_idx+1):(c_idx+=fac.multiplicity)) ./ fac.expr.^(1:fac.multiplicity)) end else for fac in facs if fac.root === nothing for i = 1:fac.multiplicity - result += (variable(:C, c_idx+=1)*x + variable(:C, c_idx+=1))/(fac.expr^i) + push!(result, (variable(:C, c_idx+=1)*x + variable(:C, c_idx+=1))/(fac.expr^i)) end continue end @@ -72,20 +69,22 @@ function partial_frac_decomposition(expr, x) other_facs = filter(f -> !isequal(f, fac), facs) numerator = rationalize(unwrap(substitute(A / prod((f -> f.expr^f.multiplicity).(other_facs)), Dict(x => fac.root)))) - result += numerator / fac.expr^fac.multiplicity + push!(result, numerator / fac.expr^fac.multiplicity) if fac.multiplicity > 1 - result += sum(variables(:C, (c_idx+1):(c_idx+=fac.multiplicity-1)) ./ fac.expr.^(1:fac.multiplicity-1)) + append!(result, variables(:C, (c_idx+1):(c_idx+=fac.multiplicity-1)) ./ fac.expr.^(1:fac.multiplicity-1)) end end end - - if isequal(get_variables(result), [x]) - return expand(result/v) + result + if isequal(get_variables(sum(result)), [x]) + return sum(result ./ leading_coeff) end lhs::Vector{Rational} = coeff_vector(numerator(expr), x) - rhs = coeff_vector(numerator(simplify(result)), x) + rhs = coeff_vector(expand(sum(simplify.(numerator.(result) .* ((B/leading_coeff) ./ denominator.(result))))), x) + # rhs = + coeff_vector(numerator(simplify(sum(result))), x) if length(lhs) > length(rhs) rhs = [rhs; zeros(length(lhs)-length(rhs))] @@ -97,14 +96,13 @@ function partial_frac_decomposition(expr, x) for i = 1:length(lhs) push!(eqs, lhs[i] ~ rhs[i]) end - solution = symbolic_solve(eqs, Symbolics.variables(:C, 1:c_idx))[1] if !(solution isa Dict) solution = Dict(variable(:C, 1) => solution) end - - return expand(substitute(result, solution)/v) + substitute.(result, Ref(solution)) + return sum(substitute.(result, Ref(solution)) ./ leading_coeff) end # increasing from 0 to degree n @@ -150,7 +148,10 @@ function factorize(expr, x)::Set{Factor} for root in keys(counts) if !isequal(abs(imag(root)), 0) fac_expr = expand((x - root)*(x - conj(root))) - @assert isequal(imag(fac_expr), 0) "Encountered issue with complex irrational roots" + if !isequal(imag(fac_expr), 0) + @warn "Encountered issue with complex irrational roots" + return nothing + end push!(facs, Factor(real(fac_expr), counts[root], x)) continue end diff --git a/test/partialfractions.jl b/test/partialfractions.jl index d7a5d6ab2..d7eac4b21 100644 --- a/test/partialfractions.jl +++ b/test/partialfractions.jl @@ -12,14 +12,14 @@ import Symbolics: partial_frac_decomposition # https://tutorial.math.lamar.edu/Problems/Alg/PartialFractions.aspx # can't handle leading coefficients being not 1 in denominator @test isequal(partial_frac_decomposition((17x-53)/(x^2 - 2x - 15), x), expand(4/(x-5) + 13/(x+3))) -@test_broken isequal(partial_frac_decomposition((34-12x)/(3x^2 - 10x - 8), x), -9(3x+2) - 1/(x-4)) +@test isequal(partial_frac_decomposition((34-12x)/(3x^2 - 10x - 8), x), (-3)/(2//3 + x) + -1/(-4 + x)) @test isequal(partial_frac_decomposition((125 + 4x - 9x^2)/((x-1)*(x+3)*(x+4)), x), expand(6/(x-1) - 8/(x+3) - 7/(x+4))) -@test isequal(partial_frac_decomposition((10x+35)/((x+4)^2), x), expand(10/(x+4) - 5/(x+4)^2)) -@test_broken isequal(partial_frac_decomposition((6x+5)/((2x-1)^2), x), 3/(2x-1) + 8/(2x-1)^2) -@test isequal(partial_frac_decomposition((7x^2-17x+38)/((x+6)*(x-1)^2), x), expand(8/(x+6) - 1/(x-1) + 4/(x-1)^2)) -@test_broken isequal(partial_frac_decomposition((4x^2 - 22x + 7)/((2x+3)*(x-2)^2), x), 4/(2x+3) - 3/(x-2)^2) +@test isequal(partial_frac_decomposition((10x+35)/((x+4)^2), x), 10/(x+4) + -5/(x+4)^2) +@test isequal(partial_frac_decomposition((6x+5)/((2x-1)^2), x), (3//2)/(x-1//2) + 2/(x-1//2)^2) +@test isequal(partial_frac_decomposition((7x^2-17x+38)/((x+6)*(x-1)^2), x), 8/(x+6) + -1/(x-1) + 4/(x-1)^2) +@test isequal(partial_frac_decomposition((4x^2 - 22x + 7)/((2x+3)*(x-2)^2), x), 2/(x+3//2) + -3/(x-2)^2) @test_broken isequal(partial_frac_decomposition((3x^2 + 7x + 28)/(x*(x^2 + x + 7)), x), expand(4/x + (3-x)/(x^2+x+7))) # irrational roots -@test isequal(partial_frac_decomposition((4x^3 + 16x + 7)/(x^2 + 4)^2, x), expand(4x/(x^2+4) + 7/(x^2+4)^2)) +@test isequal(partial_frac_decomposition((4x^3 + 16x + 7)/(x^2 + 4)^2, x), 4x/(x^2+4) + 7/(x^2+4)^2) # check valid expressions @test partial_frac_decomposition(sin(x), x) === nothing diff --git a/test/runtests.jl b/test/runtests.jl index 2df5bd8ff..44aa75c46 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -63,6 +63,7 @@ if GROUP == "All" || GROUP == "Core" @safetestset "Function inverses test" begin include("inverse.jl") end @safetestset "Taylor Series Test" begin include("taylor.jl") end @safetestset "Discontinuity registration test" begin include("discontinuities.jl") end + @safetestset "Partial Fractions Test" begin include("partialfractions.jl") end end end From ca218238002d867688cd56520836f8e039eec372 Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Thu, 21 Aug 2025 14:29:46 -0400 Subject: [PATCH 43/52] switched to solve_interms_ofvar instead of symbolic_solve --- src/partialfractions.jl | 53 ++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 30 deletions(-) diff --git a/src/partialfractions.jl b/src/partialfractions.jl index ff8bcc42f..d9522f8b3 100644 --- a/src/partialfractions.jl +++ b/src/partialfractions.jl @@ -40,72 +40,65 @@ function partial_frac_decomposition(expr, x) return nothing end - leading_coeff = coeff_vector(expand(B), x)[end] #simplify(B / prod((f -> f.expr^f.multiplicity).(facs))) + leading_coeff = coeff_vector(expand(B), x)[end] # of denominator + # already in partial fraction form if length(facs) == 1 && only(facs).multiplicity == 1 && degree(A) <= 1 return expr end result = [] - c_idx = 0 + c_idx = 0 # index to keep track of which C subscript to use if length(facs) == 1 fac = only(facs) - if fac.root === nothing + + if fac.root === nothing # irreducible quadratic factor for i = 1:fac.multiplicity - push!(result, (variable(:C, c_idx+=1)*x + variable(:C, c_idx+=1))/(fac.expr^i)) + push!(result, (variable(:𝒞, c_idx+=1)*x + variable(:𝒞, c_idx+=1))/(fac.expr^i)) # (Ax + B)/(x-r)^i end else - append!(result, variables(:C, (c_idx+1):(c_idx+=fac.multiplicity)) ./ fac.expr.^(1:fac.multiplicity)) + append!(result, variables(:𝒞, (c_idx+1):(c_idx+=fac.multiplicity)) ./ fac.expr.^(1:fac.multiplicity)) # C1/(x-r) + C2/(x-2)^2 ... end else for fac in facs - if fac.root === nothing + if fac.root === nothing # irreducible quadratic factor for i = 1:fac.multiplicity - push!(result, (variable(:C, c_idx+=1)*x + variable(:C, c_idx+=1))/(fac.expr^i)) + push!(result, (variable(:𝒞, c_idx+=1)*x + variable(:𝒞, c_idx+=1))/(fac.expr^i)) # (Ax + B)/(x-r)^i end continue end + # cover up method other_facs = filter(f -> !isequal(f, fac), facs) - numerator = rationalize(unwrap(substitute(A / prod((f -> f.expr^f.multiplicity).(other_facs)), Dict(x => fac.root)))) + numerator = rationalize(unwrap(substitute(A / prod((f -> f.expr^f.multiplicity).(other_facs)), Dict(x => fac.root)))) # plug in root to expression without its factor in denominator push!(result, numerator / fac.expr^fac.multiplicity) if fac.multiplicity > 1 - append!(result, variables(:C, (c_idx+1):(c_idx+=fac.multiplicity-1)) ./ fac.expr.^(1:fac.multiplicity-1)) + append!(result, variables(:𝒞, (c_idx+1):(c_idx+=fac.multiplicity-1)) ./ fac.expr.^(1:fac.multiplicity-1)) # C1/(x-r) + C2/(x-2)^2 ... end end end - result + + # no unknowns, so just return if isequal(get_variables(sum(result)), [x]) return sum(result ./ leading_coeff) end - lhs::Vector{Rational} = coeff_vector(numerator(expr), x) - rhs = coeff_vector(expand(sum(simplify.(numerator.(result) .* ((B/leading_coeff) ./ denominator.(result))))), x) - # rhs = - coeff_vector(numerator(simplify(sum(result))), x) - - if length(lhs) > length(rhs) - rhs = [rhs; zeros(length(lhs)-length(rhs))] - elseif length(rhs) > length(lhs) - lhs = [lhs; zeros(length(rhs)-length(lhs))] - end + lhs = numerator(expr) + rhs = expand(sum(simplify.(numerator.(result) .* ((B/leading_coeff) ./ denominator.(result))))) # multiply each numerator by the common denominator/its denominator, and sum to get numerator of whole expression - eqs = [] - for i = 1:length(lhs) - push!(eqs, lhs[i] ~ rhs[i]) - end - solution = symbolic_solve(eqs, Symbolics.variables(:C, 1:c_idx))[1] + solution = solve_interms_ofvar(lhs - rhs, x)[1] # solve for unknowns (C's) by looking at coefficients of the polynomial + # single unknown if !(solution isa Dict) - solution = Dict(variable(:C, 1) => solution) + solution = Dict(variable(:𝒞, 1) => solution) end - substitute.(result, Ref(solution)) - return sum(substitute.(result, Ref(solution)) ./ leading_coeff) + + return sum(substitute.(result, Ref(solution)) ./ leading_coeff) # substitute solutions back in and sum end -# increasing from 0 to degree n +# increasing from 0 to degree n. doesn't skip powers of x like polynomial_coeffs function coeff_vector(poly, x, n) coeff_dict = polynomial_coeffs(poly, [x])[1] vec = [] @@ -139,7 +132,7 @@ function count_multiplicities(facs) end # for partial fractions, into linear and irreducible quadratic factors -function factorize(expr, x)::Set{Factor} +function factorize(expr, x) roots = symbolic_solve(expr, x, dropmultiplicity=false) counts = count_multiplicities(roots) From 839bf66c617eb0152fd7cd18f7820af17ac39ac0 Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Thu, 21 Aug 2025 15:29:57 -0400 Subject: [PATCH 44/52] updated docstring --- src/partialfractions.jl | 21 ++++++++++++++++++++- test/partialfractions.jl | 1 - 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/partialfractions.jl b/src/partialfractions.jl index d9522f8b3..98c3cf3f9 100644 --- a/src/partialfractions.jl +++ b/src/partialfractions.jl @@ -19,7 +19,26 @@ end """ partial_frac_decomposition(expr, x) -Performs partial fraction decomposition for expressions with linear, reapeated, or irreducible quadratic factors in the denominator. Can't handle irrational roots or non-one leading coefficients +Performs partial fraction decomposition for expressions with linear, repeated, or irreducible quadratic factors in the denominator. Can't currently handle irrational roots. + +When leading coefficient of the denominator is not 1, it will be factored out and then put back in at the end, often leading to non-integer coefficients in the result. Will return `nothing` if the expression is not a valid polynomial fraction, or if it has irrational roots. + +# Examples + +```jldoctest +julia> @variables x +1-element Vector{Num}: + x + +julia> partial_frac_decomposition((3x-1) / (x^2 + x - 6), x) +(1//1) / (-2 + x) + (2//1) / (3 + x) + +julia> partial_frac_decomposition((4x^3 + 16x + 7)/(x^2 + 4)^2, x) # repeated irreducible quadratic factor +(4x) / (4 + x^2) + 7 / ((4 + x^2)^2) + +julia> partial_frac_decomposition((4x^2 - 22x + 7)/((2x+3)*(x-2)^2), x) # non-one leading coefficient +(-3//1) / ((-2 + x)^2) + (2//1) / ((3//2) + x) +``` !!! note that irreducible quadratic and repeated linear factors require the `Groebner` package to solve a system of equations """ diff --git a/test/partialfractions.jl b/test/partialfractions.jl index d7eac4b21..d6c75e814 100644 --- a/test/partialfractions.jl +++ b/test/partialfractions.jl @@ -10,7 +10,6 @@ import Symbolics: partial_frac_decomposition @test isequal(partial_frac_decomposition((9x^2 + 34x + 14) / ((x+2)*(x^2 - x - 12)), x), expand(3/(x+2) + 7/(x-4) - 1/(x+3))) # https://tutorial.math.lamar.edu/Problems/Alg/PartialFractions.aspx -# can't handle leading coefficients being not 1 in denominator @test isequal(partial_frac_decomposition((17x-53)/(x^2 - 2x - 15), x), expand(4/(x-5) + 13/(x+3))) @test isequal(partial_frac_decomposition((34-12x)/(3x^2 - 10x - 8), x), (-3)/(2//3 + x) + -1/(-4 + x)) @test isequal(partial_frac_decomposition((125 + 4x - 9x^2)/((x-1)*(x+3)*(x+4)), x), expand(6/(x-1) - 8/(x+3) - 7/(x+4))) From 930a0abf74bb8cfc82cedbb2853552323291d412 Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Fri, 22 Aug 2025 00:14:09 -0400 Subject: [PATCH 45/52] doc strings and code cleanup --- src/diffeqs/laplace.jl | 71 +++++++++++++++++++++++++++++------------- test/laplace.jl | 20 ++++++------ 2 files changed, 60 insertions(+), 31 deletions(-) diff --git a/src/diffeqs/laplace.jl b/src/diffeqs/laplace.jl index 6a82f6173..e24a48acb 100644 --- a/src/diffeqs/laplace.jl +++ b/src/diffeqs/laplace.jl @@ -1,6 +1,15 @@ import DomainSets.ClosedInterval -function laplace(expr, f, t, s, F) +""" + laplace(expr, f, t, F, s) + +Performs the Laplace transform of `expr` with respect to the variable `t`, where `f(t)` is a function in `expr` being transformed, and `F(s)` is the Laplace transform of `f(t)`. Returns the transformed expression in terms of `s`. + +Note that `f(t)` and `F(s)` should be defined using `@syms` + +Currently relies mostly on linearity and a rules table. When the rules table does not apply, it falls back to the integral definition of the Laplace transform. +""" +function laplace(expr, f, t, F, s) expr = expand(expr) Dt = Differential(t) Ds = Differential(s) @@ -38,7 +47,7 @@ function laplace(expr, f, t, s, F) @rule t*exp(~a * t) => 1 / (-~a + s)^(2) @rule t^~n * exp(t) => factorial(~n) / (s)^(~n + 1) @rule t*exp(t) => 1 / (s)^(2) - @rule exp(~c*t) * ~g => laplace(~g, f, t, s - ~c, F) # s-shift rule + @rule exp(~c*t) * ~g => laplace(~g, f, t, F, s - ~c) # s-shift rule @rule t*f(t) => -Ds(F(s)) # s-derivative rule @rule t^(~n)*f(t) => (-1)^(~n) * (Ds^~n)(F(s)) # s-derivative rule @rule f(~a + t) => exp(~a*s)*F(s) # t-shift rule @@ -71,20 +80,29 @@ function laplace(expr, f, t, s, F) factors = _true_factors(wrap(term)) constant = filter(x -> isempty(Symbolics.get_variables(x)), factors) if !isempty(constant) - result += laplace(term / constant[1], f, t, s, F) * constant[1] + result += laplace(term / constant[1], f, t, F, s) * constant[1] else - result += laplace(term, f, t, s, F) + result += laplace(term, f, t, F, s) end end return result end -function laplace(expr::Equation, f, t, s, F) - return laplace(expr.lhs, f, t, s, F) ~ laplace(expr.rhs, f, t, s, F) +function laplace(expr::Equation, f, t, F, s) + return laplace(expr.lhs, f, t, F, s) ~ laplace(expr.rhs, f, t, F, s) end -function inverse_laplace(expr, F, t, s, f) +""" + inverse_laplace(expr, F, s, f, t) + +Performs the inverse Laplace transform of `expr` with respect to the variable `s`, where `F(s)` is the Laplace transform of `f(t)`. Returns the transformed expression in terms of `t`. + +Note that `f(t)` and `F(s)` should be defined using `@syms`. + +Will perform partial fraction decomposition and linearity before applying the inverse Laplace transform rules. When unable to find a result, returns `nothing`. +""" +function inverse_laplace(expr, F, s, f, t) if isequal(expr, 0) return 0 end @@ -92,7 +110,7 @@ function inverse_laplace(expr, F, t, s, f) # check for partial fractions partial_fractions = partial_frac_decomposition(expr, s) if partial_fractions !== nothing && !isequal(partial_fractions, expr) - return inverse_laplace(partial_fractions, F, t, s, f) + return inverse_laplace(partial_fractions, F, s, f, t) end inverse_transform_rules = Symbolics.Chain([ @@ -128,7 +146,7 @@ function inverse_laplace(expr, F, t, s, f) result = 0 if length(_terms) == 1 && length(filter(x -> isempty(get_variables(x)), _true_factors(_terms[1]))) == 0 - println("Inverse laplace failed: $expr") + @warn "Inverse laplace failed: $expr" return nothing # no result end @@ -137,19 +155,24 @@ function inverse_laplace(expr, F, t, s, f) factors = _true_factors(term) constant = filter(x -> isempty(Symbolics.get_variables(x)), factors) if !isempty(constant) - result += inverse_laplace(term / constant[1], F, t, s, f) * constant[1] + result += inverse_laplace(term / constant[1], F, s, f, t) * constant[1] else - result += inverse_laplace(term, F, t, s, f) + result += inverse_laplace(term, F, s, f, t) end end return result end -function inverse_laplace(expr::Equation, F, t, s, f) - return inverse_laplace(expr.lhs, F, t, s, f) ~ inverse_laplace(expr.rhs, F, t, s, f) +function inverse_laplace(expr::Equation, F, s, f, t) + return inverse_laplace(expr.lhs, F, s, f, t) ~ inverse_laplace(expr.rhs, F, s, f, t) end +""" + unwrap_der(expr, Dt) + +Helper function to unwrap derivatives of `f(t)` in `expr` with respect to the differential operator `Dt = Differential(t)`. Returns a tuple `(n, base_expr)`, where `n` is the order of the derivative and `base_expr` is the expression with the derivatives removed. If `expr` does not contain `f(t)` or its derivatives, returns `(0, expr)`. +""" function unwrap_der(expr, Dt) reduce_rule = @rule Dt(~x) => ~x @@ -161,21 +184,27 @@ function unwrap_der(expr, Dt) return order + 1, expr end +""" + laplace_solve_ode(eq, f, t, f0) + +Solves the ordinary differential equation `eq` for the function `f(t)` using the Laplace transform method. + +`f0` is a vector of initial conditions evaluated at `t=0` (`[f(0), f'(0), f''(0), ...]`, must be same length as order of `eq`). +""" function laplace_solve_ode(eq, f, t, f0) - @variables s - @syms F(s) - transformed_eq = laplace(eq, f, t, s, F) - transformed_eq = substitute(transformed_eq, Dict(F(s) => variable(:F), [variable(:f0, i-1) => f0[i] for i=1:length(f0)]...)) + s = variable(:𝓈) + @syms 𝓕(s) + transformed_eq = laplace(eq, f, t, 𝓕, s) + transformed_eq = substitute(transformed_eq, Dict(𝓕(s) => variable(:𝓕), [variable(:f0, i-1) => f0[i] for i=1:length(f0)]...)) transformed_eq = expand(transformed_eq.lhs - transformed_eq.rhs) - # transformed_soln = symbolic_solve(transformed_eq, variable(:F)) F_terms = 0 other_terms = [] for term in terms(transformed_eq) - if isempty(get_variables(term, [variable(:F)])) + if isempty(get_variables(term, [variable(:𝓕)])) push!(other_terms, -1*term) else - F_terms += term/variable(:F) # assumes term is something times F + F_terms += term/variable(:𝓕) # assumes term is something times F end end @@ -185,5 +214,5 @@ function laplace_solve_ode(eq, f, t, f0) transformed_soln = simplify(sum(other_terms ./ F_terms)) - return expand(inverse_laplace(transformed_soln, F, t, s, f)) + return expand(inverse_laplace(transformed_soln, 𝓕, s, f, t)) end \ No newline at end of file diff --git a/test/laplace.jl b/test/laplace.jl index 5f24db1b3..798e45a07 100644 --- a/test/laplace.jl +++ b/test/laplace.jl @@ -7,19 +7,19 @@ import Nemo, Groebner @syms f(t)::Real F(s)::Real # https://sites.math.washington.edu/~aloveles/Math307Fall2019/m307LaplacePractice.pdf -@test isequal(laplace(exp(4t) + 5, f, t, s, F), 1/(s-4) + 5/s) -@test isequal(laplace(cos(2t) + 7sin(2t), f, t, s, F), s/(s^2 + 4) + 14/(s^2 + 4)) -@test isequal(laplace(exp(-2t)*cos(3t) + 5exp(-2t)*sin(3t), f, t, s, F), (s+2)/((s+2)^2 + 9) + 15/((s+2)^2 + 9)) -@test isequal(laplace(10 + 5t + t^2 - 4t^3, f, t, s, F), expand(10/s + 5/s^2 + 2/s^3 - 24/s^4)) -@test isequal(laplace(exp(3t)*(t^2 + 4t + 2), f, t, s, F), 2/(s-3)^3 + 4/(s-3)^2 + 2/(s-3)) -@test isequal(laplace(6exp(5t)*cos(2t) - exp(7t), f, t, s, F), 6(s-5)/((s-5)^2 + 4) + expand(-1/(s-7))) +@test isequal(laplace(exp(4t) + 5, f, t, F, s), 1/(s-4) + 5/s) +@test isequal(laplace(cos(2t) + 7sin(2t), f, t, F, s), s/(s^2 + 4) + 14/(s^2 + 4)) +@test isequal(laplace(exp(-2t)*cos(3t) + 5exp(-2t)*sin(3t), f, t, F, s), (s+2)/((s+2)^2 + 9) + 15/((s+2)^2 + 9)) +@test isequal(laplace(10 + 5t + t^2 - 4t^3, f, t, F, s), expand(10/s + 5/s^2 + 2/s^3 - 24/s^4)) +@test isequal(laplace(exp(3t)*(t^2 + 4t + 2), f, t, F, s), 2/(s-3)^3 + 4/(s-3)^2 + 2/(s-3)) +@test isequal(laplace(6exp(5t)*cos(2t) - exp(7t), f, t, F, s), 6(s-5)/((s-5)^2 + 4) + expand(-1/(s-7))) # https://www.math.lsu.edu/~adkins/m2065/2065s08review2a.pdf -@test isequal(inverse_laplace(7/(s+3)^3, F, t, s, f), (7//2)t^2 * exp(-3t)) -@test isequal(inverse_laplace((s-9)/(s^2 + 9), F, t, s, f), cos(3t) - 3sin(3t)) +@test isequal(inverse_laplace(7/(s+3)^3, F, s, f, t), (7//2)t^2 * exp(-3t)) +@test isequal(inverse_laplace((s-9)/(s^2 + 9), F, s, f, t), cos(3t) - 3sin(3t)) # partial fraction decomposition -@test isequal(inverse_laplace((s+2)/(s^2 - 3s - 4), F, t, s, f), (6//5)*exp(4t) - (1//5)*exp(-t)) -@test isequal(inverse_laplace(1/(s^2 - 10s + 9), F, t, s, f), (1//8)*exp(9t) - (1//8)*exp(t)) +@test isequal(inverse_laplace((s+2)/(s^2 - 3s - 4), F, s, f, t), (6//5)*exp(4t) - (1//5)*exp(-t)) +@test isequal(inverse_laplace(1/(s^2 - 10s + 9), F, s, f, t), (1//8)*exp(9t) - (1//8)*exp(t)) Dt = Differential(t) @test isequal(laplace_solve_ode(Dt(f(t)) + 3f(t) ~ t^2*exp(-3t) + t*exp(-2t) + t, f, t, [1]), (1//3)*t^3*exp(-3t) + t*exp(-2t) + (1//3)*t + (19//9)*exp(-3t) - exp(-2t) - 1//9) From 6bf95bbdb1f966ed32e26802c1b28823a8493f54 Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Mon, 25 Aug 2025 09:55:21 -0400 Subject: [PATCH 46/52] pull out transform rules into global scope --- src/diffeqs/laplace.jl | 141 +++++++++++++++++++++-------------------- 1 file changed, 74 insertions(+), 67 deletions(-) diff --git a/src/diffeqs/laplace.jl b/src/diffeqs/laplace.jl index e24a48acb..80866441f 100644 --- a/src/diffeqs/laplace.jl +++ b/src/diffeqs/laplace.jl @@ -1,5 +1,46 @@ import DomainSets.ClosedInterval +# from https://tutorial.math.lamar.edu/Classes/DE/Laplace_Table.aspx +transform_rules(f, t, F, s) = Symbolics.Chain([ + @rule 1 => 1/s + @rule exp(t) => 1/(s - 1) + @rule exp(~a * t) => 1/(-~a + s) + @rule t => 1/s^2 + @rule t^~n => factorial(~n)/s^(~n + 1) + @rule sqrt(t) => term(sqrt, pi)/(2 * s^(3/2)) + @rule sin(t) => 1/(1 + s^2) + @rule sin(~a * t) => ~a/((~a)^2 + s^2) + @rule cos(t) => s/(1 + s^2) + @rule cos(~a * t) => s/((~a)^2 + s^2) + @rule t*sin(t) => 1/(1 + s^2)^2 + @rule t*sin(~a * t) => 2*~a*s / ((~a)^2 + s^2)^2 + @rule t*cos(t) => (s^2 - 1) / (1 + s^2)^2 + @rule t*cos(~a * t) => (-(~a)^2 + s^2) / ((~a)^2 + s^2)^2 + @rule sin(t) - t*cos(t) => 2 / (1 + s^2)^2 + @rule sin(~a*t) - ~a*t*cos(~a*t) => 2*(~a)^3 / ((~a)^2 + s^2)^2 + @rule sin(t) + t*cos(t) => 2s^2 / (1 + s^2)^2 + @rule sin(~a*t) + ~a*t*cos(~a*t) => 2*~a*s^2 / ((~a)^2 + s^2)^2 + @rule cos(~a*t) - ~a*t*sin(~a*t) => s*((~a)^2 + s^2) / ((~a)^2 + s^2)^2 + @rule cos(~a*t) + ~a*t*sin(~a*t) => s*(s^2 + 3*(~a)^2) / ((~a)^2 + s^2)^2 + @rule sin(~b + ~a*t) => (s*sin(~b) + ~a*cos(~b)) / ((~a)^2 + s^2) + @rule cos(~b + ~a*t) => (s*cos(~b) - ~a*sin(~b)) / ((~a)^2 + s^2) + @rule sinh(~a * t) => ~a/(-(~a)^2 + s^2) + @rule cosh(~a * t) => s/(-(~a)^2 + s^2) + @rule exp(~a*t) * sin(~b * t) => ~b / ((~b)^2 + (-~a+s)^2) + @rule exp(~a*t) * cos(~b * t) => (-~a+s) / ((~b)^2 + (-~a+s)^2) + @rule exp(~a*t) * sinh(~b * t) => ~b / (-(~b)^2 + (-~a+s)^2) + @rule exp(~a*t) * cosh(~b * t) => (-~a+s) / (-(~b)^2 + (-~a+s)^2) + @rule t^~n * exp(~a * t) => factorial(~n) / (-~a + s)^(~n + 1) + @rule t*exp(~a * t) => 1 / (-~a + s)^(2) + @rule t^~n * exp(t) => factorial(~n) / (s)^(~n + 1) + @rule t*exp(t) => 1 / (s)^(2) + @rule exp(~c*t) * ~g => laplace(~g, f, t, F, s - ~c) # s-shift rule + @rule t*f(t) => -Ds(F(s)) # s-derivative rule + @rule t^(~n)*f(t) => (-1)^(~n) * (Ds^~n)(F(s)) # s-derivative rule + @rule f(~a + t) => exp(~a*s)*F(s) # t-shift rule + @rule f(t) => F(s) +]) + """ laplace(expr, f, t, F, s) @@ -13,48 +54,8 @@ function laplace(expr, f, t, F, s) expr = expand(expr) Dt = Differential(t) Ds = Differential(s) - # from https://tutorial.math.lamar.edu/Classes/DE/Laplace_Table.aspx - transform_rules = Symbolics.Chain([ - @rule 1 => 1/s - @rule exp(t) => 1/(s - 1) - @rule exp(~a * t) => 1/(-~a + s) - @rule t => 1/s^2 - @rule t^~n => factorial(~n)/s^(~n + 1) - @rule sqrt(t) => term(sqrt, pi)/(2 * s^(3/2)) - @rule sin(t) => 1/(1 + s^2) - @rule sin(~a * t) => ~a/((~a)^2 + s^2) - @rule cos(t) => s/(1 + s^2) - @rule cos(~a * t) => s/((~a)^2 + s^2) - @rule t*sin(t) => 1/(1 + s^2)^2 - @rule t*sin(~a * t) => 2*~a*s / ((~a)^2 + s^2)^2 - @rule t*cos(t) => (s^2 - 1) / (1 + s^2)^2 - @rule t*cos(~a * t) => (-(~a)^2 + s^2) / ((~a)^2 + s^2)^2 - @rule sin(t) - t*cos(t) => 2 / (1 + s^2)^2 - @rule sin(~a*t) - ~a*t*cos(~a*t) => 2*(~a)^3 / ((~a)^2 + s^2)^2 - @rule sin(t) + t*cos(t) => 2s^2 / (1 + s^2)^2 - @rule sin(~a*t) + ~a*t*cos(~a*t) => 2*~a*s^2 / ((~a)^2 + s^2)^2 - @rule cos(~a*t) - ~a*t*sin(~a*t) => s*((~a)^2 + s^2) / ((~a)^2 + s^2)^2 - @rule cos(~a*t) + ~a*t*sin(~a*t) => s*(s^2 + 3*(~a)^2) / ((~a)^2 + s^2)^2 - @rule sin(~b + ~a*t) => (s*sin(~b) + ~a*cos(~b)) / ((~a)^2 + s^2) - @rule cos(~b + ~a*t) => (s*cos(~b) - ~a*sin(~b)) / ((~a)^2 + s^2) - @rule sinh(~a * t) => ~a/(-(~a)^2 + s^2) - @rule cosh(~a * t) => s/(-(~a)^2 + s^2) - @rule exp(~a*t) * sin(~b * t) => ~b / ((~b)^2 + (-~a+s)^2) - @rule exp(~a*t) * cos(~b * t) => (-~a+s) / ((~b)^2 + (-~a+s)^2) - @rule exp(~a*t) * sinh(~b * t) => ~b / (-(~b)^2 + (-~a+s)^2) - @rule exp(~a*t) * cosh(~b * t) => (-~a+s) / (-(~b)^2 + (-~a+s)^2) - @rule t^~n * exp(~a * t) => factorial(~n) / (-~a + s)^(~n + 1) - @rule t*exp(~a * t) => 1 / (-~a + s)^(2) - @rule t^~n * exp(t) => factorial(~n) / (s)^(~n + 1) - @rule t*exp(t) => 1 / (s)^(2) - @rule exp(~c*t) * ~g => laplace(~g, f, t, F, s - ~c) # s-shift rule - @rule t*f(t) => -Ds(F(s)) # s-derivative rule - @rule t^(~n)*f(t) => (-1)^(~n) * (Ds^~n)(F(s)) # s-derivative rule - @rule f(~a + t) => exp(~a*s)*F(s) # t-shift rule - @rule f(t) => F(s) - ]) - - transformed = transform_rules(expr) + + transformed = transform_rules(f, t, F, s)(expr) if !isequal(transformed, expr) return transformed end @@ -93,6 +94,36 @@ function laplace(expr::Equation, f, t, F, s) return laplace(expr.lhs, f, t, F, s) ~ laplace(expr.rhs, f, t, F, s) end +# postprocess_root prevents automatic evaluation of sqrt to its floating point value +function _sqrt(x) + return postprocess_root(term(sqrt, x)) +end + +# F and f aren't used here, but are here for future-proofing +inverse_transform_rules(F, s, f, t) = Symbolics.Chain([ + @rule 1/s => 1 + @rule 1/(~a + s) => exp(-~a * t) + @rule 1/s^(~n) => t^(~n-1) / factorial(~n-1) + @rule 1/(2 * s^(3/2)) => sqrt(t)/term(term(sqrt, pi)) + @rule 1/(~a + s^2) => sin(_sqrt(~a) * t)/_sqrt(~a) + @rule s/(~a + s^2) => cos(_sqrt(~a) * t) + @rule s / (~a + s^2)^2 => t*sin(_sqrt(~a) * t)/(2*_sqrt(~a)) + @rule (-~a + s^2) / (~a + s^2)^2 => t*cos(_sqrt(~a) * t) + @rule 1 / (~a + s^2)^2 => (sin(_sqrt(~a)*t) - _sqrt(~a)*t*cos(_sqrt(~a)*t))/ (2*_sqrt(~a)^3) + @rule s^2 / (~a + s^2)^2 => (sin(_sqrt(~a)*t) + _sqrt(~a)*t*cos(_sqrt(~a)*t)) / (2*_sqrt(~a)) + @rule s*(~a + s^2) / (~a + s^2)^2 => cos(_sqrt(~a)*t) - _sqrt(~a)*t*sin(_sqrt(~a)*t) + @rule s*(3*~a + s^2) / (~a + s^2)^2 => cos(_sqrt(~a)*t) + _sqrt(~a)*t*sin(_sqrt(~a)*t) + @rule (s*sin(~b) + ~a*cos(~b)) / (~a + s^2) => sin(~b + _sqrt(~a)*t) + @rule (s*cos(~b) - ~a*sin(~b)) / ((~a)^2 + s^2) => cos(~b + ~a*t) + @rule 1/(s^2 - (~b)^2) => sinh(~b * t)/~b + @rule s/(s^2 - (~b)^2) => cosh(~b * t) + @rule 1 / ((~c+s)^2 + (~b)^2) => exp(-~c*t) * sin(~b * t) / ~b + @rule (~c+s) / ((~c+s)^2 + (~b)^2) => exp(-~c*t) * cos(~b * t) + @rule 1 / ((~c+s)^2 - (~b)^2) => exp(-~c*t) * sinh(~b * t) / ~b + @rule (~c+s) / ((~c+s)^2 - (~b)^2) => exp(-~c*t) * cosh(~b * t) + @rule 1 / (~a + s)^(~n) => t^(~n-1) * exp(-~a * t) / factorial(~n-1) +]) + """ inverse_laplace(expr, F, s, f, t) @@ -113,31 +144,7 @@ function inverse_laplace(expr, F, s, f, t) return inverse_laplace(partial_fractions, F, s, f, t) end - inverse_transform_rules = Symbolics.Chain([ - @rule 1/s => 1 - @rule 1/(~a + s) => exp(-~a * t) - @rule 1/s^(~n) => t^(~n-1) / factorial(~n-1) - @rule 1/(2 * s^(3/2)) => sqrt(t)/term(term(sqrt, pi)) - @rule 1/(~a + s^2) => sin(postprocess_root(term(sqrt, ~a)) * t)/postprocess_root(term(sqrt, ~a)) - @rule s/(~a + s^2) => cos(postprocess_root(term(sqrt, ~a)) * t) - @rule s / (~a + s^2)^2 => t*sin(postprocess_root(term(sqrt, ~a)) * t)/(2*postprocess_root(term(sqrt, ~a))) - @rule (-~a + s^2) / (~a + s^2)^2 => t*cos(postprocess_root(term(sqrt, ~a)) * t) - @rule 1 / (~a + s^2)^2 => (sin(postprocess_root(term(sqrt, ~a))*t) - postprocess_root(term(sqrt, ~a))*t*cos(postprocess_root(term(sqrt, ~a))*t))/ (2*postprocess_root(term(sqrt, ~a))^3) - @rule s^2 / (~a + s^2)^2 => (sin(postprocess_root(term(sqrt, ~a))*t) + postprocess_root(term(sqrt, ~a))*t*cos(postprocess_root(term(sqrt, ~a))*t)) / (2*postprocess_root(term(sqrt, ~a))) - @rule s*(~a + s^2) / (~a + s^2)^2 => cos(postprocess_root(term(sqrt, ~a))*t) - postprocess_root(term(sqrt, ~a))*t*sin(postprocess_root(term(sqrt, ~a))*t) - @rule s*(3*~a + s^2) / (~a + s^2)^2 => cos(postprocess_root(term(sqrt, ~a))*t) + postprocess_root(term(sqrt, ~a))*t*sin(postprocess_root(term(sqrt, ~a))*t) - @rule (s*sin(~b) + ~a*cos(~b)) / (~a + s^2) => sin(~b + postprocess_root(term(sqrt, ~a))*t) - @rule (s*cos(~b) - ~a*sin(~b)) / ((~a)^2 + s^2) => cos(~b + ~a*t) - @rule 1/(s^2 - (~b)^2) => sinh(~b * t)/~b - @rule s/(s^2 - (~b)^2) => cosh(~b * t) - @rule 1 / ((~c+s)^2 + (~b)^2) => exp(-~c*t) * sin(~b * t) / ~b - @rule (~c+s) / ((~c+s)^2 + (~b)^2) => exp(-~c*t) * cos(~b * t) - @rule 1 / ((~c+s)^2 - (~b)^2) => exp(-~c*t) * sinh(~b * t) / ~b - @rule (~c+s) / ((~c+s)^2 - (~b)^2) => exp(-~c*t) * cosh(~b * t) - @rule 1 / (~a + s)^(~n) => t^(~n-1) * exp(-~a * t) / factorial(~n-1) - ]) - - transformed = inverse_transform_rules(expr) + transformed = inverse_transform_rules(F, s, f, t)(expr) if !isequal(transformed, expr) return transformed end From 39449c1d3f630cc669beda01845056ca7674be61 Mon Sep 17 00:00:00 2001 From: LordOfFrogs Date: Mon, 25 Aug 2025 10:01:09 -0400 Subject: [PATCH 47/52] moved unwrap_der into diffeq_helpers.jl --- src/diffeqs/diffeq_helpers.jl | 16 ++++++++++++++++ src/diffeqs/laplace.jl | 36 ++++++++++------------------------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/diffeqs/diffeq_helpers.jl b/src/diffeqs/diffeq_helpers.jl index dbe8c5bcc..811513bc2 100644 --- a/src/diffeqs/diffeq_helpers.jl +++ b/src/diffeqs/diffeq_helpers.jl @@ -15,6 +15,22 @@ function _get_der_order(expr, x, t) return _get_der_order(substitute(expr, Dict(Differential(t)(x) => x)), x, t) + 1 end +""" + unwrap_der(expr, Dt) + +Helper function to unwrap derivatives of `f(t)` in `expr` with respect to the differential operator `Dt = Differential(t)`. Returns a tuple `(n, base_expr)`, where `n` is the order of the derivative and `base_expr` is the expression with the derivatives removed. If `expr` does not contain `f(t)` or its derivatives, returns `(0, expr)`. +""" +function unwrap_der(expr, Dt) + reduce_rule = @rule Dt(~x) => ~x + + if reduce_rule(expr) === nothing + return 0, expr + end + + order, expr = unwrap_der(reduce_rule(expr), Dt) + return order + 1, expr +end + # takes into account fractions function _true_factors(expr) facs = factors(expr) diff --git a/src/diffeqs/laplace.jl b/src/diffeqs/laplace.jl index 80866441f..4e708d605 100644 --- a/src/diffeqs/laplace.jl +++ b/src/diffeqs/laplace.jl @@ -95,7 +95,7 @@ function laplace(expr::Equation, f, t, F, s) end # postprocess_root prevents automatic evaluation of sqrt to its floating point value -function _sqrt(x) +function processed_sqrt(x) return postprocess_root(term(sqrt, x)) end @@ -105,15 +105,15 @@ inverse_transform_rules(F, s, f, t) = Symbolics.Chain([ @rule 1/(~a + s) => exp(-~a * t) @rule 1/s^(~n) => t^(~n-1) / factorial(~n-1) @rule 1/(2 * s^(3/2)) => sqrt(t)/term(term(sqrt, pi)) - @rule 1/(~a + s^2) => sin(_sqrt(~a) * t)/_sqrt(~a) - @rule s/(~a + s^2) => cos(_sqrt(~a) * t) - @rule s / (~a + s^2)^2 => t*sin(_sqrt(~a) * t)/(2*_sqrt(~a)) - @rule (-~a + s^2) / (~a + s^2)^2 => t*cos(_sqrt(~a) * t) - @rule 1 / (~a + s^2)^2 => (sin(_sqrt(~a)*t) - _sqrt(~a)*t*cos(_sqrt(~a)*t))/ (2*_sqrt(~a)^3) - @rule s^2 / (~a + s^2)^2 => (sin(_sqrt(~a)*t) + _sqrt(~a)*t*cos(_sqrt(~a)*t)) / (2*_sqrt(~a)) - @rule s*(~a + s^2) / (~a + s^2)^2 => cos(_sqrt(~a)*t) - _sqrt(~a)*t*sin(_sqrt(~a)*t) - @rule s*(3*~a + s^2) / (~a + s^2)^2 => cos(_sqrt(~a)*t) + _sqrt(~a)*t*sin(_sqrt(~a)*t) - @rule (s*sin(~b) + ~a*cos(~b)) / (~a + s^2) => sin(~b + _sqrt(~a)*t) + @rule 1/(~a + s^2) => sin(processed_sqrt(~a) * t)/processed_sqrt(~a) + @rule s/(~a + s^2) => cos(processed_sqrt(~a) * t) + @rule s / (~a + s^2)^2 => t*sin(processed_sqrt(~a) * t)/(2*processed_sqrt(~a)) + @rule (-~a + s^2) / (~a + s^2)^2 => t*cos(processed_sqrt(~a) * t) + @rule 1 / (~a + s^2)^2 => (sin(processed_sqrt(~a)*t) - processed_sqrt(~a)*t*cos(processed_sqrt(~a)*t))/ (2*processed_sqrt(~a)^3) + @rule s^2 / (~a + s^2)^2 => (sin(processed_sqrt(~a)*t) + processed_sqrt(~a)*t*cos(processed_sqrt(~a)*t)) / (2*processed_sqrt(~a)) + @rule s*(~a + s^2) / (~a + s^2)^2 => cos(processed_sqrt(~a)*t) - processed_sqrt(~a)*t*sin(processed_sqrt(~a)*t) + @rule s*(3*~a + s^2) / (~a + s^2)^2 => cos(processed_sqrt(~a)*t) + processed_sqrt(~a)*t*sin(processed_sqrt(~a)*t) + @rule (s*sin(~b) + ~a*cos(~b)) / (~a + s^2) => sin(~b + processed_sqrt(~a)*t) @rule (s*cos(~b) - ~a*sin(~b)) / ((~a)^2 + s^2) => cos(~b + ~a*t) @rule 1/(s^2 - (~b)^2) => sinh(~b * t)/~b @rule s/(s^2 - (~b)^2) => cosh(~b * t) @@ -175,22 +175,6 @@ function inverse_laplace(expr::Equation, F, s, f, t) return inverse_laplace(expr.lhs, F, s, f, t) ~ inverse_laplace(expr.rhs, F, s, f, t) end -""" - unwrap_der(expr, Dt) - -Helper function to unwrap derivatives of `f(t)` in `expr` with respect to the differential operator `Dt = Differential(t)`. Returns a tuple `(n, base_expr)`, where `n` is the order of the derivative and `base_expr` is the expression with the derivatives removed. If `expr` does not contain `f(t)` or its derivatives, returns `(0, expr)`. -""" -function unwrap_der(expr, Dt) - reduce_rule = @rule Dt(~x) => ~x - - if reduce_rule(expr) === nothing - return 0, expr - end - - order, expr = unwrap_der(reduce_rule(expr), Dt) - return order + 1, expr -end - """ laplace_solve_ode(eq, f, t, f0) From e3d0811c18d0c00031dfe835ac152014e1b9d76c Mon Sep 17 00:00:00 2001 From: Tori Date: Mon, 25 Aug 2025 12:45:47 -0400 Subject: [PATCH 48/52] switch to using fast_substitute when possible --- src/diffeqs/diffeq_helpers.jl | 6 +++--- src/diffeqs/diffeqs.jl | 28 ++++++++++++++-------------- src/diffeqs/laplace.jl | 2 +- src/diffeqs/systems.jl | 2 +- src/partialfractions.jl | 4 ++-- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/diffeqs/diffeq_helpers.jl b/src/diffeqs/diffeq_helpers.jl index 811513bc2..5d175609a 100644 --- a/src/diffeqs/diffeq_helpers.jl +++ b/src/diffeqs/diffeq_helpers.jl @@ -12,7 +12,7 @@ function _get_der_order(expr, x, t) return maximum(_get_der_order.(factors(expr), Ref(x), Ref(t))) end - return _get_der_order(substitute(expr, Dict(Differential(t)(x) => x)), x, t) + 1 + return _get_der_order(fast_substitute(expr, Dict(Differential(t)(x) => x)), x, t) + 1 end """ @@ -61,7 +61,7 @@ function reduce_order(eq, x, t, ys) # reduction of order y_sub = Dict([[(Dt^i)(x) => ys[i+1] for i=0:n-1]; (Dt^n)(x) => variable(:𝒴)]) - eq = substitute(eq, y_sub) + eq = fast_substitute(eq, y_sub) # isolate (Dt^n)(x) f = symbolic_linear_solve(eq, variable(:𝒴), check=false) @@ -76,7 +76,7 @@ function unreduce_order(expr, x, t, ys) Dt = Differential(t) rev_y_sub = Dict(ys[i] => (Dt^(i-1))(x) for i in eachindex(ys)) - return substitute(expr, rev_y_sub) + return fast_substitute(expr, rev_y_sub) end function is_solution(solution, eq::Equation, x, t) diff --git a/src/diffeqs/diffeqs.jl b/src/diffeqs/diffeqs.jl index 73d1acd33..4c0674ad8 100644 --- a/src/diffeqs/diffeqs.jl +++ b/src/diffeqs/diffeqs.jl @@ -59,7 +59,7 @@ function is_linear_ode(expr, x, t) @assert n >= 1 "ODE must have at least one derivative" y_sub = Dict([[(Dt^i)(x) => ys[i+1] for i=0:n-1]; (Dt^n)(x) => variable(:𝒴)]) - expr = substitute(expr, y_sub) + expr = fast_substitute(expr, y_sub) # isolate (Dt^n)(x) f = symbolic_linear_solve(expr, variable(:𝒴), check=false) @@ -415,12 +415,12 @@ function exp_trig_particular_solution(eq::LinearODE) @variables 𝓈 p = characteristic_polynomial(eq, 𝓈) Ds = Differential(𝓈) - while isequal(substitute(expand_derivatives((Ds^k)(p)), Dict(𝓈 => r+b*im)), 0) + while isequal(fast_substitute(expand_derivatives((Ds^k)(p)), Dict(𝓈 => r+b*im)), 0) k += 1 end rrf = expand(simplify(a * exp((r + b * im) * eq.t) * eq.t^k / - (substitute(expand_derivatives((Ds^k)(p)), Dict(𝓈 => r+b*im))))) + (fast_substitute(expand_derivatives((Ds^k)(p)), Dict(𝓈 => r+b*im))))) return is_sin ? imag(rrf) : real(rrf) end @@ -447,12 +447,12 @@ function resonant_response_formula(eq::LinearODE) @variables 𝓈 p = characteristic_polynomial(eq, 𝓈) Ds = Differential(𝓈) - while isequal(substitute(expand_derivatives((Ds^k)(p)), Dict(𝓈 => r)), 0) + while isequal(fast_substitute(expand_derivatives((Ds^k)(p)), Dict(𝓈 => r)), 0) k += 1 end return expand(simplify(a * exp(r * eq.t) * eq.t^k / - (substitute(expand_derivatives((Ds^k)(p)), Dict(𝓈 => r))))) + (fast_substitute(expand_derivatives((Ds^k)(p)), Dict(𝓈 => r))))) end function method_of_undetermined_coefficients(eq::LinearODE) @@ -476,8 +476,8 @@ function method_of_undetermined_coefficients(eq::LinearODE) coeff_solution = nothing end - if degree > 0 && coeff_solution !== nothing && !isempty(coeff_solution) && isequal(expand(substitute(eq_subbed, coeff_solution[1])), 0) - return substitute(form, coeff_solution[1]) + if degree > 0 && coeff_solution !== nothing && !isempty(coeff_solution) && isequal(expand(fast_substitute(eq_subbed, coeff_solution[1])), 0) + return fast_substitute(form, coeff_solution[1]) end # exponential @@ -487,13 +487,13 @@ function method_of_undetermined_coefficients(eq::LinearODE) r = coeff[2] form = a_form*exp(r*eq.t) - eq_subbed = substitute(get_expression(eq), Dict(eq.x => form)) + eq_subbed = fast_substitute(get_expression(eq), Dict(eq.x => form)) eq_subbed = expand_derivatives(eq_subbed) eq_subbed = simplify(expand((eq_subbed.lhs - eq_subbed.rhs) / exp(r*eq.t))) coeff_solution = solve_interms_ofvar(eq_subbed, eq.t) if coeff_solution !== nothing && !isempty(coeff_solution) - return substitute(form, coeff_solution[1]) + return fast_substitute(form, coeff_solution[1]) end end @@ -505,9 +505,9 @@ function method_of_undetermined_coefficients(eq::LinearODE) if parsed !== nothing ω = parsed[1] form = 𝒶*cos(ω*eq.t) + 𝒷*sin(ω*eq.t) - eq_subbed = substitute(get_expression(eq), Dict(eq.x => form)) + eq_subbed = fast_substitute(get_expression(eq), Dict(eq.x => form)) eq_subbed = expand_derivatives(eq_subbed) - eq_subbed = expand(substitute(eq_subbed.lhs - eq_subbed.rhs, Dict(cos(ω*eq.t)=>𝒸𝓈, sin(ω*eq.t)=>𝓈𝓃))) + eq_subbed = expand(fast_substitute(eq_subbed.lhs - eq_subbed.rhs, Dict(cos(ω*eq.t)=>𝒸𝓈, sin(ω*eq.t)=>𝓈𝓃))) cos_eq = simplify(sum(filter(term -> !isempty(Symbolics.get_variables(term, 𝒸𝓈)), terms(eq_subbed)))/𝒸𝓈) sin_eq = simplify(sum(filter(term -> !isempty(Symbolics.get_variables(term, 𝓈𝓃)), terms(eq_subbed)))/𝓈𝓃) if !isempty(Symbolics.get_variables(cos_eq, [eq.t,𝓈𝓃,𝒸𝓈])) || !isempty(Symbolics.get_variables(sin_eq, [eq.t,𝓈𝓃,𝒸𝓈])) @@ -517,7 +517,7 @@ function method_of_undetermined_coefficients(eq::LinearODE) end if coeff_solution !== nothing && !isempty(coeff_solution) - return substitute(form, coeff_solution[1]) + return fast_substitute(form, coeff_solution[1]) end end end @@ -556,7 +556,7 @@ function solve_IVP(ivp::IVP) push!(eqs, eq) end - return expand(simplify(substitute(general_solution, symbolic_solve(eqs, ivp.eq.C)[1]))) + return expand(simplify(fast_substitute(general_solution, symbolic_solve(eqs, ivp.eq.C)[1]))) end """ @@ -593,7 +593,7 @@ function solve_clairaut(expr, x, t) end C = Symbolics.variable(:C, 1) # constant of integration - f = substitute(f, Dict(Dt(x) => C)) + f = fast_substitute(f, Dict(Dt(x) => C)) if !isempty(Symbolics.get_variables(f, [x])) return nothing end diff --git a/src/diffeqs/laplace.jl b/src/diffeqs/laplace.jl index 4e708d605..beb4dbfdf 100644 --- a/src/diffeqs/laplace.jl +++ b/src/diffeqs/laplace.jl @@ -186,7 +186,7 @@ function laplace_solve_ode(eq, f, t, f0) s = variable(:𝓈) @syms 𝓕(s) transformed_eq = laplace(eq, f, t, 𝓕, s) - transformed_eq = substitute(transformed_eq, Dict(𝓕(s) => variable(:𝓕), [variable(:f0, i-1) => f0[i] for i=1:length(f0)]...)) + transformed_eq = fast_substitute(transformed_eq, Dict(𝓕(s) => variable(:𝓕), [variable(:f0, i-1) => f0[i] for i=1:length(f0)]...)) transformed_eq = expand(transformed_eq.lhs - transformed_eq.rhs) F_terms = 0 diff --git a/src/diffeqs/systems.jl b/src/diffeqs/systems.jl index 9ab7546ff..df0efe281 100644 --- a/src/diffeqs/systems.jl +++ b/src/diffeqs/systems.jl @@ -99,7 +99,7 @@ function symbolic_eigen(A::Matrix{<:Number}) for value in values eqs = (value*I - A) * v# .~ zeros(size(A, 1)) # equations to give eigenvectors - eqs = substitute(eqs, Dict(v[1] => 1)) # set first element to 1 to constrain solution space + eqs = fast_substitute(eqs, Dict(v[1] => 1)) # set first element to 1 to constrain solution space sol = symbolic_solve(eqs[1:end-1], v[2:end]) # solve all but one equation (because of constraining solutions above) diff --git a/src/partialfractions.jl b/src/partialfractions.jl index 98c3cf3f9..0b56fb099 100644 --- a/src/partialfractions.jl +++ b/src/partialfractions.jl @@ -90,7 +90,7 @@ function partial_frac_decomposition(expr, x) # cover up method other_facs = filter(f -> !isequal(f, fac), facs) - numerator = rationalize(unwrap(substitute(A / prod((f -> f.expr^f.multiplicity).(other_facs)), Dict(x => fac.root)))) # plug in root to expression without its factor in denominator + numerator = rationalize(unwrap(fast_substitute(A / prod((f -> f.expr^f.multiplicity).(other_facs)), Dict(x => fac.root)))) # plug in root to expression without its factor in denominator push!(result, numerator / fac.expr^fac.multiplicity) if fac.multiplicity > 1 @@ -114,7 +114,7 @@ function partial_frac_decomposition(expr, x) solution = Dict(variable(:𝒞, 1) => solution) end - return sum(substitute.(result, Ref(solution)) ./ leading_coeff) # substitute solutions back in and sum + return sum(fast_substitute.(result, Ref(solution)) ./ leading_coeff) # fast_substitute solutions back in and sum end # increasing from 0 to degree n. doesn't skip powers of x like polynomial_coeffs From dde1cdb40613f8084a49a2a23293dc31be63f053 Mon Sep 17 00:00:00 2001 From: Tori Date: Mon, 25 Aug 2025 12:50:15 -0400 Subject: [PATCH 49/52] minor bug fix in transform_rules Reference to Ds replaced with Differential(s), as Ds was local variable in laplace --- src/diffeqs/laplace.jl | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/diffeqs/laplace.jl b/src/diffeqs/laplace.jl index beb4dbfdf..4e01b791a 100644 --- a/src/diffeqs/laplace.jl +++ b/src/diffeqs/laplace.jl @@ -35,8 +35,8 @@ transform_rules(f, t, F, s) = Symbolics.Chain([ @rule t^~n * exp(t) => factorial(~n) / (s)^(~n + 1) @rule t*exp(t) => 1 / (s)^(2) @rule exp(~c*t) * ~g => laplace(~g, f, t, F, s - ~c) # s-shift rule - @rule t*f(t) => -Ds(F(s)) # s-derivative rule - @rule t^(~n)*f(t) => (-1)^(~n) * (Ds^~n)(F(s)) # s-derivative rule + @rule t*f(t) => -Differential(s)(F(s)) # s-derivative rule + @rule t^(~n)*f(t) => (-1)^(~n) * (Differential(s)^~n)(F(s)) # s-derivative rule @rule f(~a + t) => exp(~a*s)*F(s) # t-shift rule @rule f(t) => F(s) ]) @@ -53,7 +53,6 @@ Currently relies mostly on linearity and a rules table. When the rules table doe function laplace(expr, f, t, F, s) expr = expand(expr) Dt = Differential(t) - Ds = Differential(s) transformed = transform_rules(f, t, F, s)(expr) if !isequal(transformed, expr) From 11f8f29f52423719f6c36904103a309a5c52bb56 Mon Sep 17 00:00:00 2001 From: Tori Date: Mon, 25 Aug 2025 13:42:11 -0400 Subject: [PATCH 50/52] more detailed comments --- src/diffeqs/laplace.jl | 23 +++++++++++++++++++++-- src/partialfractions.jl | 2 +- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/diffeqs/laplace.jl b/src/diffeqs/laplace.jl index 4e01b791a..4c93361ae 100644 --- a/src/diffeqs/laplace.jl +++ b/src/diffeqs/laplace.jl @@ -52,14 +52,21 @@ Currently relies mostly on linearity and a rules table. When the rules table doe """ function laplace(expr, f, t, F, s) expr = expand(expr) + + if isequal(expr, 0) + return 0 + end + Dt = Differential(t) transformed = transform_rules(f, t, F, s)(expr) + + # Check if transformation was successful if !isequal(transformed, expr) return transformed end - # t-derivative rule + # t-derivative rule ((Dt^n)(f(t)) -> s^n*F(s) - s^(n-1)*f(0) - s^(n-2)*f'(0) - ... - f^(n-1)(0)) n, expr = unwrap_der(expr, Dt) if n != 0 && isequal(expr, f(t)) f0 = Symbolics.variables(:f0, 0:(n-1)) @@ -73,9 +80,13 @@ function laplace(expr, f, t, F, s) terms = Symbolics.terms(expr) result = 0 + + # unable to apply linearity, so return based on definition if length(terms) == 1 && length(filter(x->isempty(Symbolics.get_variables(x)), _true_factors(terms[1]))) == 0 return Integral(t in ClosedInterval(0, Inf))(expr*exp(-s*t)) end + + # apply linearity by splitting into terms and factoring out constants for term in terms factors = _true_factors(wrap(term)) constant = filter(x -> isempty(Symbolics.get_variables(x)), factors) @@ -144,6 +155,8 @@ function inverse_laplace(expr, F, s, f, t) end transformed = inverse_transform_rules(F, s, f, t)(expr) + + # Check if transformation was successful if !isequal(transformed, expr) return transformed end @@ -156,7 +169,7 @@ function inverse_laplace(expr, F, s, f, t) return nothing # no result end - # apply linearity + # apply linearity by splitting into terms and factoring out constants for term in _terms factors = _true_factors(term) constant = filter(x -> isempty(Symbolics.get_variables(x)), factors) @@ -184,10 +197,14 @@ Solves the ordinary differential equation `eq` for the function `f(t)` using the function laplace_solve_ode(eq, f, t, f0) s = variable(:𝓈) @syms 𝓕(s) + + # transform equation transformed_eq = laplace(eq, f, t, 𝓕, s) + # substitute in initial conditions transformed_eq = fast_substitute(transformed_eq, Dict(𝓕(s) => variable(:𝓕), [variable(:f0, i-1) => f0[i] for i=1:length(f0)]...)) transformed_eq = expand(transformed_eq.lhs - transformed_eq.rhs) + # solve for/isolate F(s) F_terms = 0 other_terms = [] for term in terms(transformed_eq) @@ -202,7 +219,9 @@ function laplace_solve_ode(eq, f, t, f0) other_terms = 0 end + # (a + b + ...)*F(s) = (c + d + ...) -> F(s) = (c + d + ...) / (a + b + ...) transformed_soln = simplify(sum(other_terms ./ F_terms)) + # perform inverse laplace transform to get f(t) return expand(inverse_laplace(transformed_soln, 𝓕, s, f, t)) end \ No newline at end of file diff --git a/src/partialfractions.jl b/src/partialfractions.jl index 0b56fb099..31ba00171 100644 --- a/src/partialfractions.jl +++ b/src/partialfractions.jl @@ -161,7 +161,7 @@ function factorize(expr, x) if !isequal(abs(imag(root)), 0) fac_expr = expand((x - root)*(x - conj(root))) if !isequal(imag(fac_expr), 0) - @warn "Encountered issue with complex irrational roots" + @warn "Encountered issue with complex irrational roots. Returning nothing." return nothing end push!(facs, Factor(real(fac_expr), counts[root], x)) From 6317eb3d8f7331845c53ab7c6eedc5e4779f9cbd Mon Sep 17 00:00:00 2001 From: Tori Date: Mon, 25 Aug 2025 14:06:41 -0400 Subject: [PATCH 51/52] added laplace examples to docstrings and added to ode.md docs page --- docs/src/manual/ode.md | 10 ++++++ src/diffeqs/laplace.jl | 73 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/docs/src/manual/ode.md b/docs/src/manual/ode.md index e120f404a..3a6f6b03b 100644 --- a/docs/src/manual/ode.md +++ b/docs/src/manual/ode.md @@ -40,6 +40,16 @@ Symbolics.symbolic_solve_ode Symbolics.solve_linear_system ``` +### Laplace Transform + +The Laplace transform can be used to solve ODEs by transforming the whole equation, solving algebraically, then applying the inverse transform. The Laplace transform and inverse transform functionality is currently based on a rule table and applying linearity, so this method is limited in what expressions are able to be transformed and inverse transformed. + +```@docs +Symbolics.laplace +Symbolics.inverse_laplace +Symbolics.laplace_solve_ode +``` + ### SymPy ```@docs diff --git a/src/diffeqs/laplace.jl b/src/diffeqs/laplace.jl index 4c93361ae..a158ab920 100644 --- a/src/diffeqs/laplace.jl +++ b/src/diffeqs/laplace.jl @@ -35,8 +35,8 @@ transform_rules(f, t, F, s) = Symbolics.Chain([ @rule t^~n * exp(t) => factorial(~n) / (s)^(~n + 1) @rule t*exp(t) => 1 / (s)^(2) @rule exp(~c*t) * ~g => laplace(~g, f, t, F, s - ~c) # s-shift rule - @rule t*f(t) => -Differential(s)(F(s)) # s-derivative rule - @rule t^(~n)*f(t) => (-1)^(~n) * (Differential(s)^~n)(F(s)) # s-derivative rule + @rule f(t)*t => -Differential(s)(F(s)) # s-derivative rule + @rule f(t)*t^(~n) => (-1)^(~n) * (Differential(s)^~n)(F(s)) # s-derivative rule @rule f(~a + t) => exp(~a*s)*F(s) # t-shift rule @rule f(t) => F(s) ]) @@ -49,6 +49,36 @@ Performs the Laplace transform of `expr` with respect to the variable `t`, where Note that `f(t)` and `F(s)` should be defined using `@syms` Currently relies mostly on linearity and a rules table. When the rules table does not apply, it falls back to the integral definition of the Laplace transform. + +# Examples + +```jldoctest +julia> @variables t, s +2-element Vector{Num}: + t + s + +julia> @syms f(t)::Real F(s)::Real +(f, F) + +julia> laplace(exp(4t) + 5, f, t, F, s) +5 / s + 1 / (-4 + s) + +julia> laplace(10 + 4t - t^2, f, t, F, s) +10 / s + 4 / (s^2) + -2 / (s^3) + +julia> laplace(exp(-2t)*cos(3t) + 5exp(-2t)*sin(3t), f, t, F, s) +(2 + s) / (9 + (2 + s)^2) + 15 / (9 + (2 + s)^2) + +julia> laplace(t^2 * f(t), f, t, F, s) # s-derivative rule +Differential(s)(Differential(s)(F(s))) + +julia> laplace(5f(t-4), f, t, F, s) # t-shift rule +5F(s)*exp(-4s) + +julia> laplace(log(t), f, t, F, s) # fallback to definition +Integral(t, 0.0 .. Inf)(exp(-s*t)*log(t)) +``` """ function laplace(expr, f, t, F, s) expr = expand(expr) @@ -142,6 +172,27 @@ Performs the inverse Laplace transform of `expr` with respect to the variable `s Note that `f(t)` and `F(s)` should be defined using `@syms`. Will perform partial fraction decomposition and linearity before applying the inverse Laplace transform rules. When unable to find a result, returns `nothing`. + +# Examples + +```jldoctest +julia> @variables t, s +2-element Vector{Num}: + t + s + +julia> @syms f(t)::Real F(s)::Real +(f, F) + +julia> inverse_laplace(7/(s+3)^3, F, s, f, t) +(7//2)*(t^2)*exp(-3t) + +julia> inverse_laplace((s+2)/(s^2 - 3s - 4), F, s, f, t) # using partial fraction decomposition +-(1//5)*exp(-t) + (6//5)*exp(4t) + +julia> inverse_laplace(1/s^4, F, s, f, t) +(1//6)*(t^3) +``` """ function inverse_laplace(expr, F, s, f, t) if isequal(expr, 0) @@ -193,6 +244,24 @@ end Solves the ordinary differential equation `eq` for the function `f(t)` using the Laplace transform method. `f0` is a vector of initial conditions evaluated at `t=0` (`[f(0), f'(0), f''(0), ...]`, must be same length as order of `eq`). + +# Examples + +```jldoctest +@variables t, s +@syms f(t)::Real F(s)::Real + +Dt = Differential(t) + +julia> laplace_solve_ode(Dt(f(t)) + 3f(t) ~ t^2*exp(-3t) + t*exp(-2t) + t, f, t, [1]) +-(1//9) + (1//3)*t + (19//9)*exp(-3t) - exp(-2t) + t*exp(-2t) + (1//3)*(t^3)*exp(-3t) + +julia> laplace_solve_ode((Dt^2)(f(t)) + f(t) ~ 2 + 2cos(t), f, t, [0, 0]) +(2//1) - (2//1)*cos(t) + t*sin(t) + +julia> laplace_solve_ode((Dt^3)(f(t)) - Dt(f(t)) ~ 6 - 3t^2, f, t, [1, 1, 1]) +exp(t) + t^3 +``` """ function laplace_solve_ode(eq, f, t, f0) s = variable(:𝓈) From 389b32803b710c5c47493f7a3b610b01fd39543d Mon Sep 17 00:00:00 2001 From: Tori Date: Mon, 25 Aug 2025 15:09:54 -0400 Subject: [PATCH 52/52] created partial fractions docs --- docs/make.jl | 1 + docs/src/manual/partial_fractions.md | 9 +++++++++ 2 files changed, 10 insertions(+) create mode 100644 docs/src/manual/partial_fractions.md diff --git a/docs/make.jl b/docs/make.jl index caf010d0b..58ea83467 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -53,6 +53,7 @@ makedocs( "Algebra" => [ "manual/solver.md", "manual/groebner.md", + "manual/partial_fractions.md", ], "Calculus" => [ diff --git a/docs/src/manual/partial_fractions.md b/docs/src/manual/partial_fractions.md new file mode 100644 index 000000000..a7a95581b --- /dev/null +++ b/docs/src/manual/partial_fractions.md @@ -0,0 +1,9 @@ +# Partial Fraction Decomposition + +Partial fraction decomposition is performed using the cover-up method. This involves "covering up" a factor in the denominator and substituting the root into the remaining expression. When the denominator can be completely factored into non-repeated linear factors, this produces the desired result. When there are repeated or irreducible quadratic factors, it produces terms with unknown coefficients in the numerator that is solved as a system of equations. + +It is often used when solving integrals or performing an inverse Laplace transform (see [`inverse_laplace`](@ref)). + +```docs +Symbolics.partial_frac_decomposition +``` \ No newline at end of file