Replace recursive-Node ScalarExpressionFunction with isbits flat form#310
Merged
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #310 +/- ##
==========================================
- Coverage 71.59% 68.73% -2.87%
==========================================
Files 56 56
Lines 5112 5204 +92
==========================================
- Hits 3660 3577 -83
- Misses 1452 1627 +175 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
b6cebaa to
413ad38
Compare
413ad38 to
31b8253
Compare
Supersedes #309. Rewrites ScalarExpressionFunction's storage from the recursive Node{T, DEFAULT_MAX_DEGREE} tree introduced by #309 to a fixed-size NTuple of FlatNode records, making the function itself isbits. The isbits property dissolves the GPU↔juliac conflict that #309's recursive-Node storage would have created downstream in Carina: the function now passes through KernelAbstractions kernels as a value argument (so BC evaluation runs in-place on the GPU with no host↔device sync per time step), AND survives `juliac --trim` since the storage is pure data — no closures, no `@eval`, no `RuntimeGeneratedFunctions`. The symbolic differentiation logic from #309 carries over. It still operates on a recursive Node{T, D} form (cleanest for constant folding through tree rewriting); the boundary with the persistent flat storage is `_unflatten` / `_flatten`, called once per `differentiate` invocation at TOML-parse time. Diff is +320 / -138: the deletions are #309's recursive-Node infrastructure (the old struct definition, four call methods, and the `differentiate(f, var_name::String)` public API). Changes: - src/Expressions.jl: - Remove #309's recursive ScalarExpressionFunction (storage was `Expression{T, Node{T, DEFAULT_MAX_DEGREE}, ntuple_type}`) and its string-keyed differentiate. - Add `FlatNode{T}` (1-based indexed, isbits) and FEC_EXPR_MAX_NODES=256 cap (Carina's biggest second-derivative tree is ~100 nodes; 256 gives 2-3× headroom at ~6 KB per function). - Add `_flatten(::Node{T,D})` (preorder DFS) and `_unflatten` inverse. - Add `_apply_unary_op` / `_apply_binary_op` op-code dispatch and `_eval_node` recursive walker over the flat tuple. - Replacement ScalarExpressionFunction stores the flat NTuple; all existing call signatures preserved (`(var::T)`, `(vars::AbstractVector{T})`, `(X::SVector{ND, T})`, `(X::SVector{ND, T}, t::T)`) so downstream consumers (DirichletBCs, InitialConditions, AppTools) need no changes. - Replacement `differentiate(f, var_idx::Integer)` with a convenience overload `differentiate(f, var_names, var_name)`. The integer form is needed because var_names is `Vector{String}` — not isbits — and can't be stored on the function. - src/bcs/DirichletBCs.jl: juliac-safe `DirichletBCs{F}` constructor switches from #309's `differentiate(bc.func, "t")` to `Expressions.differentiate(bc.func, Int(bc.func.num_vars))`, following the convention that the last variable in the user's expression is time. - test/TestExpressions.jl: rewrite #309's differentiate tests onto the integer-index API; add isbits property check; keep the parser precedence test from #309. - test/TestBCs.jl: replace #309's juliac-safe DirichletBCs tests with flat-form equivalents. Full suite: 18187/18187 (no net assertion count change vs #309 — the test items are equivalent, only the API form differs). Carina suite: 158/158 against this FEC branch via local path dep. Migration note for downstream: the only breaking change is `differentiate(f, var_name::String)` → `differentiate(f, var_idx::Integer)`, with a string-name convenience overload requiring an explicit var_names list. Internal users (FEC's DirichletBCs) already updated.
31b8253 to
7c79988
Compare
Add a `DirichletBCFunction(func::ScalarExpressionFunction)` specialization
that takes time derivatives symbolically via `Expressions.differentiate`,
producing an all-`isbits` `DirichletBCFunction{F, F, F}` rather than the
ForwardDiff closures the generic `<: Function` constructor builds. This
lets the untyped `DirichletBCs(mesh, dof, bcs)` container constructor
called from `create_parameters` give callers the juliac-safe symbolic
path "for free" — they no longer need to thread an `F` type parameter or
call `DirichletBCs{F}(...)` explicitly to opt in.
Carina passes `ScalarExpressionFunction` BC funcs through
`create_parameters`, which routed straight to the ForwardDiff branch;
ForwardDiff's `Dual` numbers do not satisfy the `T <: Number` constraint
on `ScalarExpressionFunction{T}`'s call sites, so the closures would have
crashed at first evaluation. With this specialization the same flow
yields symbolic derivatives and an `isbits` result.
cmhamel
approved these changes
Jun 9, 2026
cmhamel
left a comment
Contributor
There was a problem hiding this comment.
Removed a stale dependency but otherwise looks great to me!
75f7ca2 to
727fc7c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Supersedes #309. Three-commit PR:
ScalarExpressionFunction's storage from the recursiveNode{T, DEFAULT_MAX_DEGREE}form introduced by Symbolic differentiation for juliac-safe dynamic Dirichlet BCs #309 to a fixed-sizeNTupleofFlatNoderecords, making the function itselfisbits.ScalarExpressionFunctionthrough symbolic time derivatives inDirichletBCFunctionso the untypedDirichletBCs(mesh, dof, bcs)constructor — the onecreate_parameterscalls — produces fullyisbits{F, F, F}BC funcs automatically, without callers needing to thread anFtype parameter.Base.size(::AbstractContinuousField, ::Int)::Intoverload so the trim verifier can resolve the call site Carina uses to report node counts in the setup log.The isbits property dissolves the GPU↔juliac conflict that #309's recursive-Node storage would have created downstream in Carina:
FEC.update_bc_values!runs in-place on the GPU with no host↔device sync per time step (the function passes through KernelAbstractions kernels as a value argument).juliac --trim— the storage is pure data, no closures, no@eval, noRuntimeGeneratedFunctions.The symbolic differentiation logic from #309 carries over. It still operates on the recursive
Node{T, D}form (cleanest for constant folding through tree rewriting); the boundary with the persistent flat storage is_unflatten/_flatten, called once perdifferentiateinvocation at TOML-parse time.What this change deletes (from main, ex-#309)
struct ScalarExpressionFunction{T} <: AbstractExpressionFunction{T, Node{T, DEFAULT_MAX_DEGREE}, ntuple_type}— the recursive-Node-backed struct.differentiate(f, var_name::String)— the string-keyed public API (replaced by the integer form; a string convenience overload remains, but requires an explicitvar_nameslist since the function no longer stores it).What this change adds
Commit 1 —
Replace recursive-Node ScalarExpressionFunction with isbits flat form(7c79988)src/Expressions.jlFlatNode{T}struct (1-based indices, isbits) andFEC_EXPR_MAX_NODES = 256cap. Carina's biggest second-derivative tree (Gaussian-pulse BC) is ~100 nodes; 256 gives 2-3× headroom at ~6 KB per function._flatten(::Node{T,D})(preorder DFS) and_unflatteninverse._apply_unary_op/_apply_binary_opopen-coded op-code dispatch and_eval_noderecursive walker over the flat tuple.ScalarExpressionFunctionstoring the flatNTuple; all existing call signatures preserved ((var::T),(vars::AbstractVector{T}),(X::SVector{ND, T}),(X::SVector{ND, T}, t::T)) so downstream consumers in FEC and Carina need no changes.src/bcs/DirichletBCs.jl— juliac-safeDirichletBCs{F}constructor switches fromExpressions.differentiate(bc.func, "t")toExpressions.differentiate(bc.func, Int(bc.func.num_vars)), following the convention that the last variable in the user's expression is time.test/TestExpressions.jl— rewrites Symbolic differentiation for juliac-safe dynamic Dirichlet BCs #309's differentiate tests onto the integer-index API; adds isbits property check; keeps the parser precedence test from Symbolic differentiation for juliac-safe dynamic Dirichlet BCs #309.test/TestBCs.jl— replaces Symbolic differentiation for juliac-safe dynamic Dirichlet BCs #309's juliac-safeDirichletBCstests with flat-form equivalents.Diff: +320 / -138 lines across 4 files (large minus block = the recursive-Node infrastructure being removed).
Commit 2 —
Auto-route ScalarExpressionFunction through symbolic time derivatives(727fc7c)src/bcs/DirichletBCs.jl— newDirichletBCFunction(func::ScalarExpressionFunction)specialization that takes time derivatives viaExpressions.differentiate(not ForwardDiff), producing an all-isbitsDirichletBCFunction{F, F, F}from a single positional arg. Three motivations:DirichletBCFunction(func::F) where F <: Functionconstructor builds closures aroundForwardDiff.derivative, which the SEF call signature ((X::SVector{ND, T}, t::T) where T <: Number) cannot consume —ForwardDiff.Dual{Tag, Float64, 1}is notFloat64. Without this overload, any caller passing aScalarExpressionFunctionthroughcreate_parameterswould have crashed at first BC evaluation.DirichletBCs(mesh, dof, bcs)container constructor — which is whatcreate_parametersalready routes through — gives callers the juliac-safe symbolic path automatically. Callers no longer need to opt in viaDirichletBCs{F}(mesh, dof, bcs).DirichletBCFunction{F, F, F}isisbitsand identically typed to what the typed constructor produces, so GPU dispatch is unchanged.Diff: +16 / -0 lines, all in
DirichletBCs.jl.Commit 3 —
Add typed size(::AbstractContinuousField, ::Int)::Int overload(4042615)src/Fields.jl— direct two-arg method with concreteInt(NF)arithmetic in the body and::Intreturn type. Surfaced by a juliac--trimdry-run of Carina: the genericBase.size(A, d)::Intfallback runssize(A)[d]and infers the result as::Anywhenever the field'sNFtype parameter isn't fully bound at the call site. Carina's setup log emitssize(field, 2)to report node counts, so the abstract-::Anyflowed through several downstream lines as unresolved calls. The two-arg overload keeps inference exact regardless of how the field's type is bound upstream.Diff: +12 / -1 lines, all in
Fields.jl.Test plan
DirichletBCs{F}test and any user ofsize(::AbstractContinuousField, ::Int)).isbitstype(ScalarExpressionFunction{Float64})confirmed at REPL.isbitstype(DirichletBCFunction(::ScalarExpressionFunction))confirmed — wholly isbits, no captured closures.Migration note
The only breaking API change vs #309 is
differentiate(f, var_name::String)→differentiate(f, var_idx::Integer). A string-name convenience overloaddifferentiate(f, var_names, var_name)remains for the common parse-time case where the caller has the var-name list in scope. Internal users (FEC'sDirichletBCsjuliac path) are already updated.The
var_nameslist is no longer stored on theScalarExpressionFunction(it's aVector{String}— not isbits — and would have broken the GPU kernel argument story). Callers that need to look up names later should keep their own copy.The SEF overload added in commit 2 and the
sizeoverload added in commit 3 are purely additive —DirichletBCFunction(::Function)continues to work unchanged for any non-SEF callable, andsize(::AbstractContinuousField)(one-arg) is untouched.🤖 Generated with Claude Code