diff --git a/Project.toml b/Project.toml index 4474f89e70..c4ad027348 100644 --- a/Project.toml +++ b/Project.toml @@ -135,6 +135,7 @@ ModelingToolkitStandardLibrary = "2.20" Moshi = "0.3" NaNMath = "0.3, 1" NonlinearSolve = "4.3" +ODEInterfaceDiffEq = "3.13.4" OffsetArrays = "1" OrderedCollections = "1" OrdinaryDiffEq = "6.82.0" @@ -184,6 +185,7 @@ LinearSolve = "7ed4a6bd-45f5-4d41-b270-4a48e9bafcae" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" ModelingToolkitStandardLibrary = "16a59e39-deab-5bd0-87e4-056b12336739" NonlinearSolve = "8913a72c-1f9b-4ce2-8d82-65094dcecaec" +ODEInterfaceDiffEq = "09606e27-ecf5-54fc-bb29-004bd9f985bf" Optimization = "7f7a1694-90dd-40f0-9382-eb1efda571ba" OptimizationBase = "bca83a33-5cc9-4baa-983d-23429ab6bcbb" OptimizationMOI = "fd9f6733-72f4-499f-8506-86b2bdd0dea1" @@ -206,4 +208,4 @@ Sundials = "c3572dad-4567-51f8-b174-8c6c989267f4" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["AmplNLWriter", "BenchmarkTools", "BoundaryValueDiffEqMIRK", "BoundaryValueDiffEqAscher", "ControlSystemsBase", "DataInterpolations", "DelayDiffEq", "NonlinearSolve", "ForwardDiff", "Ipopt", "Ipopt_jll", "ModelingToolkitStandardLibrary", "Optimization", "OptimizationOptimJL", "OptimizationMOI", "OrdinaryDiffEq", "OrdinaryDiffEqCore", "OrdinaryDiffEqDefault", "REPL", "Random", "ReferenceTests", "SafeTestsets", "StableRNGs", "Statistics", "SteadyStateDiffEq", "Test", "StochasticDiffEq", "Sundials", "StochasticDelayDiffEq", "Pkg", "JET", "OrdinaryDiffEqNonlinearSolve", "Logging", "OptimizationBase", "LinearSolve"] +test = ["AmplNLWriter", "BenchmarkTools", "BoundaryValueDiffEqMIRK", "BoundaryValueDiffEqAscher", "ControlSystemsBase", "DataInterpolations", "DelayDiffEq", "NonlinearSolve", "ForwardDiff", "Ipopt", "Ipopt_jll", "ModelingToolkitStandardLibrary", "Optimization", "OptimizationOptimJL", "OptimizationMOI", "ODEInterfaceDiffEq", "OrdinaryDiffEq", "OrdinaryDiffEqCore", "OrdinaryDiffEqDefault", "REPL", "Random", "ReferenceTests", "SafeTestsets", "StableRNGs", "Statistics", "SteadyStateDiffEq", "Test", "StochasticDiffEq", "Sundials", "StochasticDelayDiffEq", "Pkg", "JET", "OrdinaryDiffEqNonlinearSolve", "Logging", "OptimizationBase", "LinearSolve"] diff --git a/docs/src/API/model_building.md b/docs/src/API/model_building.md index 64ea81786f..7a8389fd2f 100644 --- a/docs/src/API/model_building.md +++ b/docs/src/API/model_building.md @@ -219,6 +219,8 @@ symbolic analysis. ```@docs liouville_transform +fractional_to_ordinary +linear_fractional_to_ordinary change_of_variables stochastic_integral_transform Girsanov_transform diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 8b3c4084c7..4f29c5f428 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -269,7 +269,8 @@ export isinput, isoutput, getbounds, hasbounds, getguess, hasguess, isdisturbanc subset_tunables export liouville_transform, change_independent_variable, substitute_component, add_accumulations, noise_to_brownians, Girsanov_transform, change_of_variables, - respecialize + fractional_to_ordinary, linear_fractional_to_ordinary +export respecialize export PDESystem export Differential, expand_derivatives, @derivatives export Equation, ConstrainedEquation diff --git a/src/systems/diffeqs/basic_transformations.jl b/src/systems/diffeqs/basic_transformations.jl index 16b06562a1..f823260e2a 100644 --- a/src/systems/diffeqs/basic_transformations.jl +++ b/src/systems/diffeqs/basic_transformations.jl @@ -185,6 +185,242 @@ function change_of_variables( return new_sys end +""" +Generates the system of ODEs to find solution to FDEs. + +Example: + +```julia +@independent_variables t +@variables x(t) +D = Differential(t) +tspan = (0., 1.) + +α = 0.5 +eqs = (9*gamma(1 + α)/4) - (3*t^(4 - α/2)*gamma(5 + α/2)/gamma(5 - α/2)) +eqs += (gamma(9)*t^(8 - α)/gamma(9 - α)) + (3/2*t^(α/2)-t^4)^3 - x^(3/2) +sys = fractional_to_ordinary(eqs, x, α, 10^-7, 1) + +prob = ODEProblem(sys, [], tspan) +sol = solve(prob, radau5(), abstol = 1e-10, reltol = 1e-10) +``` +""" +function fractional_to_ordinary( + eqs, variables, alphas, epsilon, T; + initials = 0, additional_eqs = [], iv = only(@independent_variables t), matrix=false +) + D = Differential(iv) + i = 0 + all_eqs = Equation[] + all_def = Pair[] + + function fto_helper(sub_eq, sub_var, α; initial=0) + alpha_0 = α + + if (α > 1) + coeff = 1/(α - 1) + m = 2 + while (α - m > 0) + coeff /= α - m + m += 1 + end + alpha_0 = α - m + 1 + end + + δ = (gamma(alpha_0+1) * epsilon)^(1/alpha_0) + a = pi/2*(1-(1-alpha_0)/((2-alpha_0) * log(epsilon^-1))) + h = 2*pi*a / log(1 + (2/epsilon * (cos(a))^(alpha_0 - 1))) + + x_sub = (gamma(2-alpha_0) * epsilon)^(1/(1-alpha_0)) + x_sup = -log(gamma(1-alpha_0) * epsilon) + M = floor(Int, log(x_sub / T) / h) + N = ceil(Int, log(x_sup / δ) / h) + + function c_i(index) + h * sin(pi * alpha_0) / pi * exp((1-alpha_0)*h*index) + end + + function γ_i(index) + exp(h * index) + end + + new_eqs = Equation[] + def = Pair[] + + if matrix + new_z = Symbol(:ʐ, :_, i) + i += 1 + γs = diagm([γ_i(index) for index in M:N-1]) + cs = [c_i(index) for index in M:N-1] + + if (α < 1) + new_z = only(@variables $new_z(iv)[1:N-M]) + new_eq = D(new_z) ~ -γs*new_z .+ sub_eq + rhs = dot(cs, new_z) + initial + push!(def, new_z=>zeros(N-M)) + else + new_z = only(@variables $new_z(iv)[1:N-M, 1:m]) + new_eq = D(new_z) ~ -γs*new_z + hcat(fill(sub_eq, N-M, 1), collect(new_z[:, 1:m-1]*diagm(1:m-1))) + rhs = coeff*sum(cs[i]*new_z[i, m] for i in 1:N-M) + for (index, value) in enumerate(initial) + rhs += value * iv^(index - 1) / gamma(index) + end + push!(def, new_z=>zeros(N-M, m)) + end + push!(new_eqs, new_eq) + else + if (α < 1) + rhs = initial + for index in range(M, N-1; step=1) + new_z = Symbol(:ʐ, :_, i) + i += 1 + new_z = ModelingToolkit.unwrap(only(@variables $new_z(iv))) + new_eq = D(new_z) ~ sub_eq - γ_i(index)*new_z + push!(new_eqs, new_eq) + push!(def, new_z=>0) + rhs += c_i(index)*new_z + end + else + rhs = 0 + for (index, value) in enumerate(initial) + rhs += value * iv^(index - 1) / gamma(index) + end + for index in range(M, N-1; step=1) + new_z = Symbol(:ʐ, :_, i) + i += 1 + γ = γ_i(index) + base = sub_eq + for k in range(1, m; step=1) + new_z = Symbol(:ʐ, :_, index-M, :_, k) + new_z = ModelingToolkit.unwrap(only(@variables $new_z(iv))) + new_eq = D(new_z) ~ base - γ*new_z + base = k * new_z + push!(new_eqs, new_eq) + push!(def, new_z=>0) + end + rhs += coeff*c_i(index)*new_z + end + end + end + push!(new_eqs, sub_var ~ rhs) + return (new_eqs, def) + end + + for (eq, cur_var, alpha, init) in zip(eqs, variables, alphas, initials) + (new_eqs, def) = fto_helper(eq, cur_var, alpha; initial=init) + append!(all_eqs, new_eqs) + append!(all_def, def) + end + append!(all_eqs, additional_eqs) + @named sys = System(all_eqs, iv; defaults=all_def) + return mtkcompile(sys) +end + +""" +Generates the system of ODEs to find solution to FDEs. + +Example: + +```julia +@independent_variables t +@variables x_0(t) +D = Differential(t) +tspan = (0., 5000.) + +function expect(t) + return sqrt(2) * sin(t + pi/4) +end + +sys = linear_fractional_to_ordinary([3, 2.5, 2, 1, .5, 0], [1, 1, 1, 4, 1, 4], 6*cos(t), 10^-5, 5000; initials=[1, 1, -1]) +prob = ODEProblem(sys, [], tspan) +sol = solve(prob, radau5(), abstol = 1e-5, reltol = 1e-5) +``` +""" +function linear_fractional_to_ordinary( + degrees, coeffs, rhs, epsilon, T; + initials = 0, symbol = :x, iv = only(@independent_variables t), matrix=false +) + previous = Symbol(symbol, :_, 0) + previous = ModelingToolkit.unwrap(only(@variables $previous(iv))) + @variables x_0(iv) + D = Differential(iv) + i = 0 + all_eqs = Equation[] + all_def = Pair[] + + function fto_helper(sub_eq, α) + δ = (gamma(α+1) * epsilon)^(1/α) + a = pi/2*(1-(1-α)/((2-α) * log(epsilon^-1))) + h = 2*pi*a / log(1 + (2/epsilon * (cos(a))^(α - 1))) + + x_sub = (gamma(2-α) * epsilon)^(1/(1-α)) + x_sup = -log(gamma(1-α) * epsilon) + M = floor(Int, log(x_sub / T) / h) + N = ceil(Int, log(x_sup / δ) / h) + + function c_i(index) + h * sin(pi * α) / pi * exp((1-α)*h*index) + end + + function γ_i(index) + exp(h * index) + end + + new_eqs = Equation[] + def = Pair[] + if matrix + new_z = Symbol(:ʐ, :_, i) + i += 1 + γs = diagm([γ_i(index) for index in M:N-1]) + cs = [c_i(index) for index in M:N-1] + + new_z = only(@variables $new_z(iv)[1:N-M]) + new_eq = D(new_z) ~ -γs*new_z .+ sub_eq + sum = dot(cs, new_z) + push!(def, new_z=>zeros(N-M)) + push!(new_eqs, new_eq) + else + sum = 0 + for index in range(M, N-1; step=1) + new_z = Symbol(:ʐ, :_, i) + i += 1 + new_z = ModelingToolkit.unwrap(only(@variables $new_z(iv))) + new_eq = D(new_z) ~ sub_eq - γ_i(index)*new_z + push!(new_eqs, new_eq) + push!(def, new_z=>0) + sum += c_i(index)*new_z + end + end + return (new_eqs, def, sum) + end + + for i in range(1, ceil(Int, degrees[1]); step=1) + new_x = Symbol(symbol, :_, i) + new_x = ModelingToolkit.unwrap(only(@variables $new_x(iv))) + push!(all_eqs, D(previous) ~ new_x) + push!(all_def, previous => initials[i]) + previous = new_x + end + + new_rhs = -rhs + for (degree, coeff) in zip(degrees, coeffs) + rounded = ceil(Int, degree) + new_x = Symbol(symbol, :_, rounded) + new_x = ModelingToolkit.unwrap(only(@variables $new_x(iv))) + if isinteger(degree) + new_rhs += coeff * new_x + else + (new_eqs, def, sum) = fto_helper(new_x, rounded - degree) + append!(all_eqs, new_eqs) + append!(all_def, def) + new_rhs += coeff * sum + end + end + push!(all_eqs, 0 ~ new_rhs) + @named sys = System(all_eqs, iv; defaults=all_def) + return mtkcompile(sys) +end + """ change_independent_variable( sys::System, iv, eqs = []; diff --git a/test/fractional_to_ordinary.jl b/test/fractional_to_ordinary.jl new file mode 100644 index 0000000000..7cd8b26bdf --- /dev/null +++ b/test/fractional_to_ordinary.jl @@ -0,0 +1,86 @@ +using ModelingToolkit, OrdinaryDiffEq, ODEInterfaceDiffEq, SpecialFunctions, LinearAlgebra +using Test + +# Testing for α < 1 +# Uses example 1 from Section 7 of https://arxiv.org/pdf/2506.04188 +@independent_variables t +@variables x(t) +D = Differential(t) +tspan = (0., 1.) +timepoint = [0., 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.] + +function expect(t, α) + return (3/2*t^(α/2) - t^4)^2 +end + +α = 0.5 +eqs = (9*gamma(1 + α)/4) - (3*t^(4 - α/2)*gamma(5 + α/2)/gamma(5 - α/2)) +eqs += (gamma(9)*t^(8 - α)/gamma(9 - α)) + (3/2*t^(α/2)-t^4)^3 - x^(3/2) +sys = fractional_to_ordinary(eqs, x, α, 10^-7, 1) + +prob = ODEProblem(sys, [], tspan) +sol = solve(prob, radau5(), saveat=timepoint, abstol = 1e-10, reltol = 1e-10) + +for time in 0:0.1:1 + @test isapprox(expect(time, α), sol(time, idxs=x), atol=1e-7) + time += 0.1 +end + +α = 0.3 +eqs = (9*gamma(1 + α)/4) - (3*t^(4 - α/2)*gamma(5 + α/2)/gamma(5 - α/2)) +eqs += (gamma(9)*t^(8 - α)/gamma(9 - α)) + (3/2*t^(α/2)-t^4)^3 - x^(3/2) +sys = fractional_to_ordinary(eqs, x, α, 10^-7, 1; matrix=true) + +prob = ODEProblem(sys, [], tspan) +sol = solve(prob, radau5(), saveat=timepoint, abstol = 1e-10, reltol = 1e-10) + +for time in 0:0.1:1 + @test isapprox(expect(time, α), sol(time, idxs=x), atol=1e-7) +end + +α = 0.9 +eqs = (9*gamma(1 + α)/4) - (3*t^(4 - α/2)*gamma(5 + α/2)/gamma(5 - α/2)) +eqs += (gamma(9)*t^(8 - α)/gamma(9 - α)) + (3/2*t^(α/2)-t^4)^3 - x^(3/2) +sys = fractional_to_ordinary(eqs, x, α, 10^-7, 1) + +prob = ODEProblem(sys, [], tspan) +sol = solve(prob, radau5(), saveat=timepoint, abstol = 1e-10, reltol = 1e-10) + +for time in 0:0.1:1 + @test isapprox(expect(time, α), sol(time, idxs=x), atol=1e-7) +end + +# Testing for example 2 of Section 7 +@independent_variables t +@variables x(t) y(t) +D = Differential(t) +tspan = (0., 220.) + +sys = fractional_to_ordinary([1 - 4*x + x^2 * y, 3*x - x^2 * y], [x, y], [1.3, 0.8], 10^-8, 220; initials=[[1.2, 1], 2.8]; matrix=true) +prob = ODEProblem(sys, [], tspan) +sol = solve(prob, radau5(), abstol = 1e-8, reltol = 1e-8) + +@test isapprox(1.0097684171, sol(220, idxs=x), atol=1e-5) +@test isapprox(2.1581264031, sol(220, idxs=y), atol=1e-5) + +#Testing for example 3 of Section 7 +@independent_variables t +@variables x_0(t) +D = Differential(t) +tspan = (0., 5000.) + +function expect(t) + return sqrt(2) * sin(t + pi/4) +end + +sys = linear_fractional_to_ordinary([3, 2.5, 2, 1, .5, 0], [1, 1, 1, 4, 1, 4], 6*cos(t), 10^-5, 5000; initials=[1, 1, -1]) +prob = ODEProblem(sys, [], tspan) +sol = solve(prob, radau5(), abstol = 1e-5, reltol = 1e-5) + +@test isapprox(expect(5000), sol(5000, idxs=x_0), atol=1e-5) + +msys = linear_fractional_to_ordinary([3, 2.5, 2, 1, .5, 0], [1, 1, 1, 4, 1, 4], 6*cos(t), 10^-5, 5000; initials=[1, 1, -1], matrix=true) +mprob = ODEProblem(sys, [], tspan) +msol = solve(prob, radau5(), abstol = 1e-5, reltol = 1e-5) + +@test isapprox(expect(5000), msol(5000, idxs=x_0), atol=1e-5) diff --git a/test/runtests.jl b/test/runtests.jl index 47230c9539..522470b896 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -100,9 +100,10 @@ end @safetestset "Subsystem replacement" include("substitute_component.jl") @safetestset "Linearization Tests" include("linearize.jl") @safetestset "LinearProblem Tests" include("linearproblem.jl") + @safetestset "Fractional Differential Equations Tests" include("fractional_to_ordinary.jl") end end - + if GROUP == "All" || GROUP == "SymbolicIndexingInterface" @safetestset "SymbolicIndexingInterface test" include("symbolic_indexing_interface.jl") @safetestset "SciML Problem Input Test" include("sciml_problem_inputs.jl")