CompatHelper: bump compat for FastBroadcast to 1, (keep existing compat)#192
Conversation
4df2438 to
f2b9de0
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Coverage Report for CI Build 24713319125Coverage remained the same at 97.305%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
ranocha
left a comment
There was a problem hiding this comment.
It still uses the old version in CI: https://github.com/NumericalMathematics/PositiveIntegrators.jl/actions/runs/23895902213/job/69885732902?pr=192#step:6:125
|
This requires oxfordcontrol/Clarabel.jl#218. |
|
CI is using FastBroadcast.jl v3 now. However, there are a few failing tests. They do not seem to be related to the update of FastBroadcast.jl though, but rather look like something changed in the OrdinaryDiffEq*.jl packages. |
|
I have reduced the issue (for at least three of the five test failures) to the following MWE: using PositiveIntegrators, OrdinaryDiffEqLowOrderRK
prod1! = (P, u, p, t) -> begin
P[1, 1] = 0
P[1, 2] = u[2]
P[2, 1] = u[1]
P[2, 2] = 0
return nothing
end
dest1! = (D, u, p, t) -> begin
fill!(D, 0)
return nothing
end
u0 = [1.0, 0.0]
tspan = (0.0, 1.0)
prob_default = PDSProblem(prod1!, dest1!, u0, tspan)
solve(prob_default, Euler(); dt = 0.1)With OrdinaryDiffEqCore.jl v3.11 this returns retcode: Success
Interpolation: 3rd order Hermite
t: 11-element Vector{Float64}:
0.0
0.1
0.2
0.30000000000000004
0.4
0.5
0.6
0.7
0.7999999999999999
0.8999999999999999
1.0
u: 11-element Vector{Vector{Float64}}:
[1.0, 0.0]
[0.9, 0.1]
[0.8200000000000001, 0.18000000000000002]
[0.756, 0.24400000000000002]
[0.7048, 0.2952]
[0.66384, 0.33616]
[0.631072, 0.36892800000000003]
[0.6048576, 0.3951424]
[0.58388608, 0.41611392]
[0.5671088639999999, 0.432891136]
[0.5536870911999999, 0.4463129088]and with OrdinaryDiffEqCore.jl v3.12 and newer it returns retcode: Success
Interpolation: 3rd order Hermite
t: 12-element Vector{Float64}:
0.0
0.1
0.2
0.30000000000000004
0.4
0.5
0.6
0.7
0.7999999999999999
0.8999999999999999
0.9999999999999999
1.0
u: 12-element Vector{Vector{Float64}}:
[1.0, 0.0]
[0.9, 0.1]
[0.8200000000000001, 0.18000000000000002]
[0.756, 0.24400000000000002]
[0.7048, 0.2952]
[0.66384, 0.33616]
[0.631072, 0.36892800000000003]
[0.6048576, 0.3951424]
[0.58388608, 0.41611392]
[0.5671088639999999, 0.432891136]
[0.5536870911999999, 0.4463129088]
[0.5536870911999999, 0.4463129088]i.e., there is an additional (unexpected) time step at 0.9999999999999999. Since OrdinaryDiffEqCore.jl is part of a monorepo, which does not have tags and releases for the sublibraries it is not so easy (I don't know of an easy way) to see the diff between OrdinaryDiffEqCore.jl v3.11 and v3.12 or the PRs, which were part of the release v3.12. The downstream tests in OrdinaryDiffEq.jl for PositiveIntegrators.jl are also not really useful because they usually don't run because of incompatibilities (like https://github.com/SciML/OrdinaryDiffEq.jl/actions/runs/24119695833/job/70370910063#step:6:19). |
|
oh, that's probably related to SciML/OrdinaryDiffEq.jl#2869. We made a change in order to make sure we very clearly hit every floating point exactly for superdense time, so multiple tstops at the same time and eps apart are handled well (this is required for some types of multi-event scenarios). But yeah this looks like an odd side effect of that. I think the best solution is to simply expand |
When solving with a fixed-dt method (e.g. `solve(prob, Euler(); dt = 0.1)`), the accumulated `t + dt + dt + ...` drifts and used to produce a spurious trailing step at `1 - eps` followed by `1.0`. PR SciML#2869 removed the `100eps(tstop)` snap hack that previously masked this in `fixed_t_for_floatingpoint_error!`. Two coordinated changes restore the expected 11-step result without reintroducing the old hack: 1. `_ode_init` expands `tstops` to `tspan[1]:dt:tspan[end]` when the user specifies `dt` for a fixed-time method and did not supply their own `tstops`/`d_discontinuities`. Julia's range semantics use TwicePrecision internally, so each range element is the exact floating-point representative of `k*dt` and lands on `tspan[end]` cleanly. The expansion is skipped whenever the user supplied any tstops, which preserves the existing "continue at dtcache between user tstops" behavior that tests like `sol.t == [0, 1/3, 1/2, 5/6, 1]` depend on. 2. `modify_dt_for_tstops!` compares `dt` against `distance_to_tstop` with a small floating-point tolerance (`100 * eps(max(t, tstop))`). Without this tolerance, `dtcache = 0.1` reads as strictly less than a `distance_to_tstop` of `0.10000000000000009` and the integrator takes a full step that overshoots the tstop by one ulp, then takes a matching tiny corrective step. The tolerance guard is gated on `isfinite(tdir_tstop) && isfinite(integrator.t)` so that semi-infinite `tspan = (0.0, Inf)` integrations still work. Adds a regression test covering forward, reverse, and non-evenly-dividing `dt` on `tspan = (0.0, 1.0)`. Fixes the PositiveIntegrators.jl CI failure noted in NumericalMathematics/PositiveIntegrators.jl#192 where `solve(prob, Euler(); dt = 0.1)` began returning 12 steps instead of 11. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
When solving with a fixed-dt method (e.g. `solve(prob, Euler(); dt = 0.1)`), the accumulated `t + dt + dt + ...` drifts past `tspan[end]` by one ulp, producing a spurious trailing micro-step. PR SciML#2869 removed the `100eps(tstop)` snap hack that previously masked this. Fix: for non-adaptive, dtchangeable algorithms with no user-supplied tstops / d_discontinuities / callbacks, expand `tstops` to the range `tspan[1]:dt:tspan[end]` whose TwicePrecision arithmetic gives exact floating-point tstops, and inflate `dt` by 10 ulps so that `modify_dt_for_tstops!` always takes the tstop branch and snaps `t` exactly via `fixed_t_for_tstop_error!`. The expansion is intentionally skipped for adaptive algorithms (even when used with `adaptive=false`), `CompositeAlgorithm`, non-finite tspans, and any solve with callbacks — preserving all existing stepping semantics in those cases. Adds a regression test covering forward, reverse, and non-evenly- dividing `dt` on `tspan = (0.0, 1.0)`. Fixes the PositiveIntegrators.jl CI failure noted in NumericalMathematics/PositiveIntegrators.jl#192. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
When solving with a fixed-dt method (e.g. `solve(prob, Euler(); dt = 0.1)`), the accumulated `t + dt + dt + ...` drifts past `tspan[end]` by one ulp, producing a spurious trailing micro-step. PR SciML#2869 removed the old `100eps(tstop)` snap in `fixed_t_for_floatingpoint_error!` that masked this. Fix: add a floating-point tolerance (`100 * eps(max(|t|, |tstop|))`) to the `dt < distance_to_tstop` comparison in `modify_dt_for_tstops!`. When `dt ≈ distance` within rounding the integrator now takes the tstop branch, and `fixed_t_for_tstop_error!` snaps `t` to the exact tstop value — eliminating the extra step. Adds a regression test covering forward, reverse, and non-evenly- dividing `dt` on `tspan = (0.0, 1.0)`. Fixes the PositiveIntegrators.jl CI failure noted in NumericalMathematics/PositiveIntegrators.jl#192. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
When solving with a fixed-dt method (e.g. `solve(prob, Euler(); dt = 0.1)`), the accumulated `t + dt + dt + ...` drifts past `tspan[end]` by one ulp, producing a spurious trailing micro-step. PR #2869 removed the old `100eps(tstop)` snap in `fixed_t_for_floatingpoint_error!` that masked this. Fix: add a floating-point tolerance (`100 * eps(max(|t|, |tstop|))`) to the `dt < distance_to_tstop` comparison in `modify_dt_for_tstops!`. When `dt ≈ distance` within rounding the integrator now takes the tstop branch, and `fixed_t_for_tstop_error!` snaps `t` to the exact tstop value — eliminating the extra step. Adds a regression test covering forward, reverse, and non-evenly- dividing `dt` on `tspan = (0.0, 1.0)`. Fixes the PositiveIntegrators.jl CI failure noted in NumericalMathematics/PositiveIntegrators.jl#192. Co-authored-by: ChrisRackauckas-Claude <accounts@chrisrackauckas.com>
|
Thanks @ChrisRackauckas for the fix in OrdinaryDiffEqCore.jl, which fixed 3 of the 5 test failures here. For the two remaining ones here is an MWE: using PositiveIntegrators
using OrdinaryDiffEqTsit5
using Test
prob = prob_pds_npzd
alg = Tsit5()
dt = (last(prob.tspan) - first(prob.tspan)) / 1e4
sol = solve(prob, alg; dt, isoutofdomain = isnegative) # use explicit f
sol2 = solve(ConservativePDSProblem(prob.f.p, prob.u0, prob.tspan), alg; dt,
isoutofdomain = isnegative) # use p and d to compute f
@test sol.t ≈ sol2.tThis test passes until OrdinaryDiffEqCore.jl v3.9 and fails with OrdinaryDiffEqCore.jl v3.10 and up. Any ideas what might cause this and how to fix it? A similar test for other time integrators is also successful with newer versions of OrdinaryDiffEqCore.jl, but it is the version change from v3.9 to v3.10 in OrdinaryDiffEqCore.jl, which makes the sol3 = solve(ODEProblem(prob.f.std_rhs, prob.u0, prob.tspan), alg; dt,
isoutofdomain = isnegative) # use f to create ODEProblemas expected. So it is |
|
Okay this case is pretty understood, though I don't know what you want to do with it so I'll just give you the information. Your two problems are not identical, your So then your question will be, why were they same before? Good question. Before the qmax acceleration PR, the maximum So you could pass in |
|
So just to be clear two clarifications:
We do that already (we use
We are not talking about differences in the 12th digit, but differences of the order of 1e-4 to 1e-2. julia> sol.t .- sol2.t
421-element Vector{Float64}:
0.0
0.0
-0.0002446514639678071
-0.0005306752357510658
-0.0006660263933044863
-0.0008960739422696484
0.009118037912073884
0.009250671172130298
0.00860234178977426
0.0032171897383779235
0.0021411554781194386
0.0016643597373147134
0.0006729809782541896
-2.0721123352496207e-5
0.00032009719766801226
4.709706773042832e-5
0.00017986037279871248
9.638448074444916e-5
⋮
0.006162175210169707
0.006898746292788083
0.00779818506312413
0.008905355941839943
0.010282000114095524
0.012011926086444191
0.014213548895940953
0.017046988673076413
0.020728832292429722
0.025517463366548476
0.031671296450147324
0.039366079933075504
0.04866429536678396
0.059558828030842115
0.07190310256362853
0.08538681438158413
0.09985857665201259
0.0 |
|
The first step is 1e-12 different, but yes over 1e4 steps that will compound until you have a point where adaptivity takes a different branch. ... Is there a reason to not just fix |
|
PositiveIntegrators.jl requires a special form of the ODE to apply positivity-preserving time integration methods. To make it easy to use, we can deduce the standard RHS from the form we need here. To allow a fair comparison, we also have the option to implement the standard ODE RHS directly. The tests we are talking about shall check whether the automatic computation of the RHS and the specialized implementation will give approximately the same results. This kind of difference persists even if we use very strict tolerances: building on the MWE of @JoshuaLampert above, I get julia> using PositiveIntegrators, OrdinaryDiffEqTsit5, Test
julia> begin
prob = prob_pds_npzd
alg = Tsit5()
abstol = 1.0e-14
reltol = 1.0e-14
dt = (last(prob.tspan) - first(prob.tspan)) / 1e4
sol1 = solve(prob, alg; dt, abstol, reltol) # use explicit f
sol2 = solve(ConservativePDSProblem(prob.f.p, prob.u0, prob.tspan), alg; dt, abstol, reltol) # use p and d to compute f
@test sol1.t ≈ sol2.t
end
Test Failed at REPL[4]:11
julia> maximum(filter(isfinite, @. abs(sol1.t - sol2.t) / sol1.t))
0.004146117491549062Thus, differences of the order of floating-point accuracy in evaluating the RHS lead to differences of order |
I don't know if this was an error from an AI hallucination or something, but this test does not test the difference in the time series. The test is not on the solution or it's continuous convergence but on what time points are used. The solution had effectively zero error in the steps, it satisfies a relative tolerance of 2e-12, which is the maximum accuracy that can be achieved in 1e4 steps due to the 2e-16 tolerance of 64-bit floating point error. The maximum error of the interpolated solution is 8e-14 and at all grid points is approximately 1e-15, mostly due to just pure floating point errors at that level. It hits this level because the solver effectively has super convergence on the problem. So you have a continuous solution that has a relative continuous accuracy of 1e-12 at any point evaluated.
Yes, because as described the solution is already at the edge of floating point accuracy, so the tolerance isn't doing much. But it is still interesting that the steps diverge that much. I'll find the exact line |
Full reconstruction of the
|
Just to clarify: Thanks for your input. We will see what we get from CI when checking interpolated values. |
|
CI is still failing, but with explicit tolerances it passes locally. So let's try that. julia> using PositiveIntegrators, OrdinaryDiffEqTsit5, Test
julia> begin
prob = prob_pds_npzd
alg = Tsit5()
dt = (last(prob.tspan) - first(prob.tspan)) / 1e4
sol1 = solve(prob, alg; dt, isoutofdomain = isnegative) # use explicit f
sol2 = solve(ConservativePDSProblem(prob.f.p, prob.u0, prob.tspan), alg; dt, isoutofdomain = isnegative) # use p and d to compute f
t = range(prob.tspan..., length = 10_000)
@test sol1.(t) ≈ sol2.(t)
end
Test Failed at REPL[2]:10
julia> begin
prob = prob_pds_npzd
alg = Tsit5()
abstol = 1.0e-5
reltol = 1.0e-5
dt = (last(prob.tspan) - first(prob.tspan)) / 1e4
sol1 = solve(prob, alg; dt, abstol, reltol, isoutofdomain = isnegative) # use explicit f
sol2 = solve(ConservativePDSProblem(prob.f.p, prob.u0, prob.tspan), alg; dt, abstol, reltol, isoutofdomain = isnegative) # use p and d to compute f
t = range(prob.tspan..., length = 10_000)
@test sol1.(t) ≈ sol2.(t)
end
Test Passed |
ranocha
left a comment
There was a problem hiding this comment.
Thanks to everybody involved for their help.
|
I had to sit on this a bit to better understand why this property exists, and I think there's a very good way to see that it's fundamental to the system if you think about it as a classical control problem. First of all, a high order integrator with a high order interpolation is another way of stating that the system is insensitive to the chosen It's similar to how if you have a controller for a system that quickly goes to equilibrium (it's highly insensitive), the inverse of the system is highly sensitive since the history of what things looked like before equilibrium is very quickly dampened out and forgotten. Due to this, the control strategy ends up being bang-bang controls and very sensitive to the current values, since any errors are dampened out quickly, the controller should act hard. In the same sense, "medium" (1e-3) differences in what So I think this is pretty fundamental: high order methods (in lower tolerance limits) will effectively give noisy |
This pull request changes the compat entry for the
FastBroadcastpackage from0.3.5to0.3.5, 1.This keeps the compat entries for earlier versions.
Note: I have not tested your package with this new compat entry.
It is your responsibility to make sure that your package tests pass before you merge this pull request.