Introduce differentiation interface#2137
Conversation
|
@gdalle it would be nice if you could have a look at 64091bc. This already works very nicely! We might be able to get rid of our direct |
|
When testing dense Jacobian + ForwardDiff I get an error like this: I wonder whether that has something to do with passing the AD backend to both |
|
Taking a look now |
core/src/model.jl
Outdated
|
|
||
| function get_jac_eval(du::Vector, u::Vector, p::Parameters, solver::Solver) | ||
| backend = if solver.autodiff | ||
| AutoForwardDiff() |
There was a problem hiding this comment.
Maybe you want to configure the tag here if it is available from somewhere else (perhaps the solver?)
There was a problem hiding this comment.
solver is our own solver config object, but it works if I consistently specify the same tag everywhere 👍
core/src/util.jl
Outdated
| jac_prototype | ||
| end | ||
|
|
||
| # Custom overloads |
There was a problem hiding this comment.
If you want to get rid of the SCT dependency, you may want to
- put those overloads in an SCT package extension
- ask the user to provide an AD backend, and if it is an
AutoSparse, retrieve itssparsity_detectorinstead of providing your own
There was a problem hiding this comment.
Hmm this probably doesn't fit our application as described in #2137 (comment). Keeping the SCT dependency is fine
|
|
||
| # Custom overloads | ||
| reduction_factor(x::GradientTracer, threshold::Real) = x | ||
| reduction_factor(x::GradientTracer, ::Real) = x |
There was a problem hiding this comment.
Note that this is explicitly discouraged in the SCT docs: see the following page to add overloads properly
https://adrianhill.de/SparseConnectivityTracer.jl/stable/internals/adding_overloads/
There was a problem hiding this comment.
I know I know 🙃
There was a problem hiding this comment.
Some of these functions have more than 2 arguments, but for all of them we only care about the derivative with respect to one input. It's not clear to me whether/how that fits within the overload functionality
There was a problem hiding this comment.
It makes sense to use our mechanisms. They will generate a couple of superfluous methods, but I don't see the harm in that.
There was a problem hiding this comment.
To be fair, this specific line of code looks harmless. But once you have more than a handful of overloads, you might want to create an SCT extension and follow our docs. The generated code will be more future proof and compatible with local and global Jacobian and Hessian tracers. Your current code only supports global Jacobians.
core/src/model.jl
Outdated
|
|
||
| # Activate all nodes to catch all possible state dependencies | ||
| p.all_nodes_active = true | ||
| prep = prepare_jacobian((du, u) -> water_balance!(du, u, p, t), du, backend, u) |
There was a problem hiding this comment.
| prep = prepare_jacobian((du, u) -> water_balance!(du, u, p, t), du, backend, u) | |
| prep = prepare_jacobian(water_balance!, du, backend, u, Constant(p), Constant(t)) |
There was a problem hiding this comment.
From the docs:
Another option would be creating a closure, but that is sometimes undesirable.
When is a closure undesirable?
gradient(f, backend, x, Constant(c))
gradient(f, backend, x, Cache(c))
In the first call, c is kept unchanged throughout the function evaluation. In the second call, c can be mutated with values computed during the function.
Our p contains caches for in place computations in our RHS (hence the discussion on PreallocationTools etc. in the related issue). does that mean that we should use Cache(p)?
There was a problem hiding this comment.
Should SciMLStructures.jl come in here for more granular control?
There was a problem hiding this comment.
When is a closure undesirable?
With Enzyme in particular it can make things slower or even error. With other backends it doesn't make much of a difference, but explicitly laying out the Contexts also allows taking into account element types (eg. for handling translation to Dual with Caches).
Our p contains caches for in place computations in our RHS (hence the discussion on PreallocationTools etc. in the related issue). does that mean that we should use Cache(p)?
Does p contain anything whose value you care about?
In general, you might want to split it between a Constant part and a Cache part.
Should SciMLStructures.jl come in here for more granular control?
DI has no specific support for SciMLStructures
There was a problem hiding this comment.
@gdalle I'm working on a refactor where e.g. the prep definition now looks like this:
prep = prepare_jacobian(
water_balance!,
du,
backend,
u,
Constant(p_non_diff),
Cache(p_diff),
Constant(p_mutable),
Constant(t),
)This now fails in the sparsity detection because of an attempt to write GradientTracer values to a Vector{Float64} field of p_diff::ParametersDiff. I made ParametersDiff parametric so ParametersDiff{GradientTracer{...}} can exist, and I half expected this to be constructed internally. This probably worked before because of PreallocationTools.
There was a problem hiding this comment.
Oh I just saw this warning in the docs:
Most backends require any Cache context to be an AbstractArray.
Let's see what I can do with that.
There was a problem hiding this comment.
It doesn't look like that quickly solves the problem. I just naively subtyped ParametersDiff{T} <: AbstractVector{T}. Maybe I need to overload some methods?
There was a problem hiding this comment.
There was a problem hiding this comment.
Thanks, that's a tricky one and it's indeed on me. If you don't want to wait for a DI fix (ETA ~ days), a short-term solution would be to use PreallocationTools and a closure, even if it makes Enzyme angry.
core/src/model.jl
Outdated
| jac = | ||
| (J, u, p, t) -> | ||
| jacobian!((du, u) -> water_balance!(du, u, p, t), du, J, prep, backend, u) |
There was a problem hiding this comment.
| jac = | |
| (J, u, p, t) -> | |
| jacobian!((du, u) -> water_balance!(du, u, p, t), du, J, prep, backend, u) | |
| jac(J, u, p, t) = jacobian!(water_balance!, du, J, prep, backend, u, Constant(p), Constant(t)) |
|
Love the confident commit naming. That's the spirit we wanna see |
core/src/model.jl
Outdated
| diff_cache_SCT = | ||
| zeros(GradientTracer{IndexSetGradientPattern{Int64, BitSet}}, length(diff_cache)) |
There was a problem hiding this comment.
With this PR you can use SCT.jacobian_buffer instead, and with the update to DI I'll make once that is merged, you probably won't need any tweak at all
|
@SouthEndMusic can you take the branch from JuliaDiff/DifferentiationInterface.jl#739 for a spin, see if it works? Warning I am aware that using |
|
@gdalle I took the main branch, and it indeed works 👍 |
|
@SouthEndMusic with the branch from JuliaDiff/DifferentiationInterface.jl#741 the |
|
The changes have been released |
|
Some runtimes of the |
|
If the Jacobian is sparse, can you try other orders inside the |
What can be the effect of this? A different number of rhs calls required to compute the Jacobian? |
|
Yes, it influences the number of different colors with which the columns of the Jacobian are colored, and one color equals one function call (not exactly true with ForwardDiff though). This could accelerate the FiniteDiff version significantly if the natural coloring was suboptimal. |
|
You can call |
|
|
|
Heads up, DI v0.6.46 supports nested tuples and named tuples of arrays as caches, at least for the most common backends |
This is really nice, quite a bit of complexity can be removed now 👍 |
visr
left a comment
There was a problem hiding this comment.
Great stuff @SouthEndMusic. And thanks for all the help @gdalle.
I left some minor comments, but let's get this in soon, we can always refine later.
core/src/validation.jl
Outdated
| errors = true | ||
| control_state = key[2] | ||
| @error "$id_controlled flow rates must be non-negative, found $flow_rate_ for control state '$control_state'." | ||
| @error "Negative flow rate(s) for $id_controlled, control state '$control_state' found." |
There was a problem hiding this comment.
| @error "Negative flow rate(s) for $id_controlled, control state '$control_state' found." | |
| @error "Negative flow rate(s) found." node_id=id_controlled control_state |
This adds a function `model.to_fews(region_dir)` that converts the network and results to files that Delft-FEWS can directly handle. It is marked as experimental for now. @gijsber is working on a Delft-FEWS configuration that can be used to visualize model results, to complement our existing tools. We'll likely add this configuration to this monorepo since it is generic. #2159 also pertains to this work. What is especially nice is the spatio-temporal support of Delft-FEWS, so we can make visualizations like this:  In theory we can support similar functionality with QGIS, but looking at the plots in #1369 this would likely need work in QGIS itself. So this is really a quick win to be able to inspect models better. --------- Co-authored-by: Maarten Pronk <git@evetion.nl>
## [v2025.3.0] - 2025-04-14 The only breaking change in this release is to disallow connecting a single FlowBoundary to multiple Basins. There are large improvements in the ability to visualize results on the map in QGIS. We also welcome the Junction node to the family, which will help laying out networks in a recognizable manner. ### Added - Add spatio-temporal results layers to QGIS. [#2208](#2208) - Add topological (straight line) link view toggle to QGIS. [#2208](#2208) - Added [Junction](https://ribasim.org/reference/node/junction.html) node type. [#2175](#2175) - Write results and log bottlenecks also on an interrupt or crash. [#2191](#2191) [#2200](#2200) - Log computation time and save it to `solver_stats.arrow`. [#2209](https://github.com/Deltares/Ribasim/pull/) - Experimental support for writing the model network and results into files used by Delft-FEWS, [`model.to_fews`](`https://ribasim.org/reference/python/Model.html#ribasim.Model.to_fews`). [#2161](#2161) - Document [`results/concentration.arrow`](https://ribasim.org/reference/usage.html#concentration---concentration.arrow). [#2165](#2165) ### Changed - Allow max 1 outflow neighbour for FlowBoundary. [#2192](#2192) - Automatic differentiation is enabled by default again, `autodiff = true`, leading to better performance. [#2137](#2137) [#2183](#2183)
…u` (#2334) Fixes #2315, fixes #2314 There was a silly bug in de flow limiter (the part of the code where we nudge the solver step in the 'right' direction based on knowledge of the problem) where the basin properties where formulated based on du instead of u, giving completely wrong values for e.g. the low storage factors. This bug was introduced in #2137
Fixes #2134