From a7e827baafe50dc53049cd817f92c8d51335f2e2 Mon Sep 17 00:00:00 2001 From: ClaudeBot Date: Wed, 30 Sep 2026 12:21:04 -0400 Subject: [PATCH 1/2] Fix BUG-013: generalize create_chunks off concrete Ferrite.Grid Allow threading=true for any Ferrite.AbstractGrid, not just concrete Grid, so external grid implementations (e.g. FerriteIGA.jl's BezierGrid) can use threaded assembly. src/Multithreading/TaskChunks.jl: relax all four create_chunks method signatures from ::Ferrite.Grid to ::Ferrite.AbstractGrid. None of them use Grid-specific fields; automatic coloring already goes through Ferrite.create_coloring, which accepts AbstractGrid, and the user-supplied-chunks overload never used the grid argument beyond dispatch. test/threading_utils.jl: add a unit test with a minimal non-Grid AbstractGrid wrapper (only .cells/.nodes fields, matching Ferrite's generic AbstractGrid accessors) exercising both the automatic-coloring and user-supplied-chunks create_chunks paths. docs/src/literate_tutorials/iga.jl: add a threaded-assembly section using the real FerriteIGA.jl BezierGrid, comparing K/r against the sequential result. Uses num_tasks=2 explicitly (independent of Threads.nthreads()) so the chunk-splitting logic is exercised even in a single-threaded session; the comparison lines use #hide (not #src) so they survive into the generated script/markdown that Documenter actually executes, not just local manual runs. .github/workflows/CI.yml: add JULIA_NUM_THREADS: 2 to the docs job, matching the test job, so the threaded comparison in the IGA tutorial is exercised with real concurrency in CI (previously the docs job ran with Julia's default of 1 thread). Reviewed via /dual-review with Codex (read-only independent reviewer): - Plan review caught that the docs job runs single-threaded in CI, which would have left the new regression check untested there -> added JULIA_NUM_THREADS: 2 to the docs job. - Final-diff review caught that the two @test lines were marked #src, which Literate strips from every generated output (script, markdown, notebook) -> switched to #hide (this repo's established pattern, e.g. docs/src/literate_howto/automatic_differentiation.jl) so the assertions actually run as part of the Documentation CI job. - Second final-diff review after that fix: no further findings. Validation: Pkg.test() passes (all suites, including new BUG-013 unit test); every tutorial and how-to under docs/src/literate_* runs cleanly; full docs/make.jl build completes with no @example block errors. Also validated BUG-024 (docs/make.jl world-age failures) as an environmental issue, not a source bug: reproduced it live via a competing long-lived Julia process still attached to the docs project, then confirmed a clean docs/make.jl build once no competing process remained. Updated identified_bugs.md accordingly; BUG-013 marked done and its open-item section removed. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01KySgpVgJ5fWR9AvPK1JQoU --- .github/workflows/CI.yml | 2 ++ docs/src/literate_tutorials/iga.jl | 19 ++++++++++++++++++- src/Multithreading/TaskChunks.jl | 10 +++++----- test/threading_utils.jl | 25 +++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 6 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 8de0b327..839dbeaf 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -47,6 +47,8 @@ jobs: name: Documentation runs-on: ubuntu-latest timeout-minutes: 30 + env: + JULIA_NUM_THREADS: 2 steps: - uses: actions/checkout@v7 - uses: julia-actions/setup-julia@v3 diff --git a/docs/src/literate_tutorials/iga.jl b/docs/src/literate_tutorials/iga.jl index 01f774b2..af8986ca 100644 --- a/docs/src/literate_tutorials/iga.jl +++ b/docs/src/literate_tutorials/iga.jl @@ -12,6 +12,7 @@ # Start by loading the necessary packages using Ferrite, FerriteIGA, LinearAlgebra, FerriteAssembly +using Test #src import FerriteAssembly.ExampleElements: ElasticPlaneStrain # ## Setup @@ -81,6 +82,23 @@ apply!(a, ch) work!(assembler, buffer; a=a) apply!(r, lh, 0.0); +# ## Threaded IGA assembly +# `setup_domainbuffer` also supports `threading=true` for `BezierGrid`, since +# `FerriteAssembly` no longer requires a concrete `Ferrite.Grid` for chunk creation. +# We request 2 tasks explicitly (independent of `Threads.nthreads()`) so the +# chunk-splitting is exercised even in a single-threaded Julia session, and +# compare against the sequential result above. +threaded_buffer = setup_domainbuffer(domain; threading=true, num_tasks=2) +K_threaded = allocate_matrix(dh) +r_threaded = zeros(ndofs(dh)) +assembler_threaded = start_assemble(K_threaded, r_threaded) +work!(assembler_threaded, threaded_buffer; a=a) +apply!(r_threaded, lh, 0.0); + +using Test #hide +@test K_threaded ≈ K #hide +@test r_threaded ≈ r #hide + # before solving it, apply_zero!(K, r, ch) a .-= K\r @@ -111,7 +129,6 @@ FerriteIGA.VTKIGAFile("plate_with_hole.vtu", grid) do vtk write_solution(vtk, dh, a) end -using Test #src # @test sum(norm, σ_nodes) ≈ 3087.2447327126742 #src @test norm(norm.(qe.data)) ≈ 679.3207411544098 #src diff --git a/src/Multithreading/TaskChunks.jl b/src/Multithreading/TaskChunks.jl index dfa4de22..59104c34 100644 --- a/src/Multithreading/TaskChunks.jl +++ b/src/Multithreading/TaskChunks.jl @@ -81,7 +81,7 @@ function split_in_chunks(set::Vector{T}; num_tasks = Threads.nthreads()) where T end # If chunks already given, check that they match the intersected set -function create_chunks(::Grid, intersected_set::Vector{I}, chunks::Vector{Vector{Vector{I}}}) where I +function create_chunks(::Ferrite.AbstractGrid, intersected_set::Vector{I}, chunks::Vector{Vector{Vector{I}}}) where I chunk_set = sizehint!(Set{I}(), length(intersected_set)) full_set = Set(intersected_set) for chunk_vector in chunks @@ -100,14 +100,14 @@ function create_chunks(::Grid, intersected_set::Vector{I}, chunks::Vector{Vector return chunks end -# Colors given with same type as set -function create_chunks(g::Grid, intersected_set::Vector{I}, colors::Vector{Vector{I}}) where I +# Colors given with same type as set +function create_chunks(g::Ferrite.AbstractGrid, intersected_set::Vector{I}, colors::Vector{Vector{I}}) where I colors_intersect = map(sort! ∘ collect ∘ Base.Fix1(intersect, intersected_set), colors) chunks = [split_in_chunks(set) for set in colors_intersect] return create_chunks(g, intersected_set, chunks) end # Colors given with different type as set -function create_chunks(g::Grid, intersected_set::Vector{FacetIndex}, colors::Vector{Vector{Int}}) +function create_chunks(g::Ferrite.AbstractGrid, intersected_set::Vector{FacetIndex}, colors::Vector{Vector{Int}}) cellset = first.(intersected_set) colors_intersect = map(sort! ∘ collect ∘ Base.Fix1(intersect, cellset), colors) chunks = [convert_chunk(split_in_chunks(set), intersected_set) for set in colors_intersect] @@ -115,7 +115,7 @@ function create_chunks(g::Grid, intersected_set::Vector{FacetIndex}, colors::Vec end # Colors not given -function create_chunks(grid::Grid, intersected_set::Vector, ::Nothing) +function create_chunks(grid::Ferrite.AbstractGrid, intersected_set::Vector, ::Nothing) makecellset(v::Vector{Int}) = Set(v) makecellset(v::Vector) = Set(first.(v)) return create_chunks(grid, intersected_set, create_coloring(grid, makecellset(intersected_set))) diff --git a/test/threading_utils.jl b/test/threading_utils.jl index fba7fcc6..eaf72554 100644 --- a/test/threading_utils.jl +++ b/test/threading_utils.jl @@ -59,6 +59,31 @@ end end end +# `create_chunks` previously dispatched on concrete `Ferrite.Grid`, so any other +# `Ferrite.AbstractGrid` implementation (e.g. FerriteIGA.jl's `BezierGrid`) could not +# be used with `threading=true`, even though `create_coloring` and the generic +# `AbstractGrid` accessors it relies on only require `.cells` and `.nodes` fields. +struct DummyGrid{C,N} <: Ferrite.AbstractGrid{2} + cells::Vector{C} + nodes::Vector{N} +end + +@testset "create_chunks with non-Grid AbstractGrid (BUG-013)" begin + grid = generate_grid(Quadrilateral, (4, 4)) + dummygrid = DummyGrid(grid.cells, grid.nodes) + cellset = collect(1:getncells(grid)) + + # Automatic coloring path (colors_or_chunks = nothing) + chunks_grid = FerriteAssembly.create_chunks(grid, cellset, nothing) + chunks_dummy = FerriteAssembly.create_chunks(dummygrid, cellset, nothing) + @test Set(Iterators.flatten(Iterators.flatten(chunks_dummy))) == Set(cellset) + @test [sort!(collect(Iterators.flatten(c))) for c in chunks_dummy] == [sort!(collect(Iterators.flatten(c))) for c in chunks_grid] + + # User-supplied chunks path (doesn't use the grid argument beyond dispatch) + chunks = [[cellset[1:8], cellset[9:16]], [cellset[17:end]]] + @test FerriteAssembly.create_chunks(dummygrid, cellset, chunks) == chunks +end + @testset "work! with empty custom chunks (PR89)" begin # End-to-end reproduction: a domain whose user-supplied chunks contain # empty sub-chunks must still visit every cell during threaded `work!`. From f64c3abf9a3342e4f4631a8c09998d7eaec83f82 Mon Sep 17 00:00:00 2001 From: ClaudeBot Date: Wed, 30 Sep 2026 14:38:21 -0400 Subject: [PATCH 2/2] Address PR review comments: drop redundant using Test, use #hide consistently MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01KySgpVgJ5fWR9AvPK1JQoU - Removed the redundant `using Test #src` at the top of iga.jl: it was stripped from every generated output anyway (Literate removes #src lines unconditionally), and the tutorial's own `using Test #hide` right before the threaded-assembly assertions already covers all @test lines further down in the file. - Switched the pre-existing bottom-of-file `@test norm(norm.(qe.data)) ≈ 679.3207411544098` from #src to #hide, for the same reason as the threaded-assembly assertions fixed earlier in this PR: #src strips the line from every generated script/markdown/notebook, so it never actually runs as part of the Documentation CI job. Validation: re-ran the iga.jl tutorial standalone and the full docs/make.jl build (both clean, no errors); Pkg.test() passes (all suites, including the BUG-013 unit test). --- docs/src/literate_tutorials/iga.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/src/literate_tutorials/iga.jl b/docs/src/literate_tutorials/iga.jl index af8986ca..9e639509 100644 --- a/docs/src/literate_tutorials/iga.jl +++ b/docs/src/literate_tutorials/iga.jl @@ -12,7 +12,6 @@ # Start by loading the necessary packages using Ferrite, FerriteIGA, LinearAlgebra, FerriteAssembly -using Test #src import FerriteAssembly.ExampleElements: ElasticPlaneStrain # ## Setup @@ -130,7 +129,7 @@ FerriteIGA.VTKIGAFile("plate_with_hole.vtu", grid) do vtk end # @test sum(norm, σ_nodes) ≈ 3087.2447327126742 #src -@test norm(norm.(qe.data)) ≈ 679.3207411544098 #src +@test norm(norm.(qe.data)) ≈ 679.3207411544098 #hide #md # ## [Plain program](@id iga_plain_program) #md #