diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 28aba75f..9cf8dcd2 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -22,10 +22,14 @@ jobs: - DataDrivenSR - DataDrivenSparse - DataDrivenLux + - nopre version: - '1' - 'lts' - 'pre' + exclude: + - group: nopre + version: 'pre' steps: - uses: actions/checkout@v6 - uses: julia-actions/setup-julia@v2 diff --git a/.typos.toml b/.typos.toml index f9a460f2..0e1b25dc 100644 --- a/.typos.toml +++ b/.typos.toml @@ -1,4 +1,7 @@ [default.extend-words] +# Pre-existing typos in public API (breaking to fix) +expresssion = "expresssion" + # Julia-specific functions indexin = "indexin" findfirst = "findfirst" diff --git a/docs/src/libs/datadrivendmd/example_02.jl b/docs/src/libs/datadrivendmd/example_02.jl index 5a727814..91750b81 100644 --- a/docs/src/libs/datadrivendmd/example_02.jl +++ b/docs/src/libs/datadrivendmd/example_02.jl @@ -20,7 +20,7 @@ sys = ODEProblem(f, u0, tspan) sol = solve(sys, Tsit5(), saveat = 0.05); # We could use the `DESolution` to define our problem, but here we want to use the data for didactic purposes. -# For a [`ContinuousDataDrivenProblem`](@ref DataDrivenProblem), we need either the state trajectory and the timepoints or the state trajectory and its derivate. +# For a [`ContinuousDataDrivenProblem`](@ref DataDrivenProblem), we need either the state trajectory and the timepoints or the state trajectory and its derivative. X = Array(sol) t = sol.t diff --git a/src/problem/type.jl b/src/problem/type.jl index 6fc86af8..7bba3844 100644 --- a/src/problem/type.jl +++ b/src/problem/type.jl @@ -114,6 +114,15 @@ end Base.eltype(::AbstractDataDrivenProblem{T}) where {T} = T +# Internal constructor that is type-stable once cType and probType are known +# This function barrier ensures the inner construction is fully specialized +function _construct_datadrivenproblem( + ::Val{cType}, ::Val{probType}, dType::Type{T}, X, t, DX, Y, U, p, name + ) where {cType, probType, T} + promoted = _promote(X, t, DX, Y, U, p) + return DataDrivenProblem{T, cType, probType}(promoted..., name) +end + function DataDrivenProblem( probType, X, t, DX, Y, U, p; name = gensym(:DDProblem), kwargs... @@ -131,7 +140,10 @@ function DataDrivenProblem( end end - return DataDrivenProblem{dType, cType, probType}(_promote(X, t, DX, Y, U, p)..., name) + # Use function barrier with Val types for type stability + return _construct_datadrivenproblem( + Val(cType), Val(probType), dType, X, t, DX, Y, U, p, name + ) end function remake_problem( diff --git a/test/basis/basis.jl b/test/basis/basis.jl index d706e2e9..b3555c0c 100644 --- a/test/basis/basis.jl +++ b/test/basis/basis.jl @@ -126,8 +126,9 @@ end @test size(basis_2) == (5,) # Note: Order may differ due to internal Symbolics representation # The linear_independent basis extracts terms which may be reordered - @test basis_2([1.0; 2.0; π], [0.0; 1.0]) ≈ [1.0; -1.0; π; 2.0; 1.0] - @test basis([1.0; 2.0; π], [0.0; 1.0]) ≈ [1.0; 2.0; π; -1.0; 5 * π + 2.0; 1.0] + # Use sorted comparison to handle non-deterministic ordering + @test sort(basis_2([1.0; 2.0; π], [0.0; 1.0])) ≈ sort([1.0; -1.0; π; 2.0; 1.0]) + @test sort(basis([1.0; 2.0; π], [0.0; 1.0])) ≈ sort([1.0; 2.0; π; -1.0; 5 * π + 2.0; 1.0]) @test size(basis) == size(basis_2) .+ (1,) push!(basis_2, sin(u[2])) diff --git a/test/nopre/Project.toml b/test/nopre/Project.toml new file mode 100644 index 00000000..3916ccc9 --- /dev/null +++ b/test/nopre/Project.toml @@ -0,0 +1,7 @@ +[deps] +DataDrivenDiffEq = "2445eb08-9709-466a-b3fc-47e12bd697a2" +JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[compat] +JET = "0.9, 0.10, 0.11" diff --git a/test/nopre/jet_tests.jl b/test/nopre/jet_tests.jl new file mode 100644 index 00000000..c95c1029 --- /dev/null +++ b/test/nopre/jet_tests.jl @@ -0,0 +1,69 @@ +using DataDrivenDiffEq +using JET +using Test + +# Note: JET analysis on symbolic packages can be slow and may report many +# false positives from the underlying symbolic infrastructure (Symbolics.jl, +# ModelingToolkit.jl). This test file focuses on core DataDrivenDiffEq +# functionality with concrete types to catch actual type stability issues. +# +# We use @test_opt with target_modules to only check DataDrivenDiffEq code, +# and we use broken=true for tests that detect expected polymorphic behavior +# (like problem type dispatch) rather than actual bugs. + +@testset "JET Static Analysis" begin + @testset "Basis generator type stability" begin + # Test basis generators with concrete types + # These should be fully type-stable as they don't involve symbolic computation + @test_opt target_modules = (DataDrivenDiffEq,) DataDrivenDiffEq.polynomial_basis( + 2, 3 + ) + @test_opt target_modules = (DataDrivenDiffEq,) DataDrivenDiffEq.monomial_basis(2, 3) + @test_opt target_modules = (DataDrivenDiffEq,) DataDrivenDiffEq.chebyshev_basis(2, 3) + @test_opt target_modules = (DataDrivenDiffEq,) DataDrivenDiffEq.sin_basis(2, 3) + @test_opt target_modules = (DataDrivenDiffEq,) DataDrivenDiffEq.cos_basis(2, 3) + @test_opt target_modules = (DataDrivenDiffEq,) DataDrivenDiffEq.fourier_basis(2, 3) + end + + @testset "Problem accessor type stability" begin + X = rand(2, 10) + t = collect(1.0:10.0) + DX = rand(2, 10) + + prob = ContinuousDataDrivenProblem(X, t, DX) + + # Test simple accessor functions that should be type-stable + # These accessors only check type parameters, not field values + @test_opt target_modules = (DataDrivenDiffEq,) DataDrivenDiffEq.is_autonomous(prob) + @test_opt target_modules = (DataDrivenDiffEq,) DataDrivenDiffEq.is_discrete(prob) + @test_opt target_modules = (DataDrivenDiffEq,) DataDrivenDiffEq.is_continuous(prob) + @test_opt target_modules = (DataDrivenDiffEq,) DataDrivenDiffEq.is_direct(prob) + # Note: has_timepoints checks isempty on AbstractVector field, which has + # expected runtime dispatch due to the abstract field type design choice + end + + @testset "Internal constructor type stability" begin + # Test the internal type-stable constructor directly + X = rand(2, 10) + t = collect(1.0:10.0) + DX = rand(2, 10) + Y = Matrix{Float64}(undef, 0, 0) + U = Matrix{Float64}(undef, 0, 0) + p = Float64[] + + # The internal constructor has runtime dispatch in _promote on Julia lts + # due to broadcasting with abstract element types. Fixed in Julia 1.11+. + @test_opt broken = (VERSION < v"1.11") target_modules = (DataDrivenDiffEq,) DataDrivenDiffEq._construct_datadrivenproblem( + Val(false), + Val(DataDrivenDiffEq.DDProbType(3)), + Float64, + X, + t, + DX, + Y, + U, + p, + :test + ) + end +end diff --git a/test/runtests.jl b/test/runtests.jl index afa45bbf..22fbae2f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -16,6 +16,12 @@ function activate_subpkg_env(subpkg) return Pkg.instantiate() end +function activate_nopre_env() + Pkg.activate(joinpath(@__DIR__, "nopre")) + Pkg.develop(PackageSpec(path = dirname(@__DIR__))) + return Pkg.instantiate() +end + @time begin if GROUP == "All" || GROUP == "Core" || GROUP == "Downstream" @testset "All" begin @@ -41,6 +47,13 @@ end include("./commonsolve/commonsolve.jl") end end + elseif GROUP == "nopre" + # nopre tests are excluded from Julia pre-release versions in CI + # to avoid failures from upstream changes (e.g., JET type inference) + activate_nopre_env() + @safetestset "JET Static Analysis" begin + include("nopre/jet_tests.jl") + end else dev_subpkg(GROUP) subpkg_path = joinpath(dirname(@__DIR__), "lib", GROUP)