From 74f9445ced1152cbc560aa3d82b66b3df475e508 Mon Sep 17 00:00:00 2001 From: "Craig M. Hamel" Date: Fri, 29 May 2026 22:34:22 -0400 Subject: [PATCH] Adding nodeset and sideset support. A few tweaks to tests and app tools as well. Bumping to version 0.14 since the ics interface is a breaking change. --- Project.toml | 2 +- src/AppTools.jl | 27 +++++++++++--- src/InitialConditions.jl | 68 ++++++++++++++++++++++++++++++++--- src/bcs/BoundaryConditions.jl | 2 ++ src/bcs/DirichletBCs.jl | 10 +++--- test/TestAppTools.jl | 61 +++++++++++++++++-------------- test/TestICs.jl | 38 +++++++++++++++++--- test/input-file.toml | 4 +-- 8 files changed, 163 insertions(+), 49 deletions(-) diff --git a/Project.toml b/Project.toml index 1db31d4a..a6675c0d 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "FiniteElementContainers" uuid = "d08262e4-672f-4e7f-a976-f2cea5767631" -version = "0.13.5" +version = "0.14.0" authors = ["Craig M. Hamel and contributors"] [deps] diff --git a/src/AppTools.jl b/src/AppTools.jl index 02e56cb7..9ba233a8 100644 --- a/src/AppTools.jl +++ b/src/AppTools.jl @@ -434,6 +434,7 @@ end # polynomial_type::String # end +# TODO currently only supports blocks struct ICSettings{T <: Number} ics::Vector{InitialCondition{ScalarExpressionFunction{T}}} @@ -444,14 +445,32 @@ struct ICSettings{T <: Number} ic_settings = data["initial_conditions"]::Vector{Any} for ic in ic_settings temp = ic::Dict{String, Any} - blocks = temp["blocks"]::Vector{String} func = temp["function"]::String func = functions.scalar_expr_funcs[func] vars = temp["variables"]::Vector{String} - for block in blocks - for var in vars - push!(ics, InitialCondition(var, func, block)) + if haskey(temp, "blocks") + blocks = temp["blocks"]::Vector{String} + for block in blocks + for var in vars + push!(ics, InitialCondition(var, func; block_name = block)) + end end + elseif haskey(temp, "nodesets") + nodesets = temp["nodesets"]::Vector{String} + for nodeset in nodesets + for var in vars + push!(ics, InitialCondition(var, func; nodeset_name = nodeset)) + end + end + elseif haskey(temp, "sidesets") + sidesets = temp["sidesets"]::Vector{String} + for sideset in sidesets + for var in vars + push!(ics, InitialCondition(var, func; sideset_name = sideset)) + end + end + else + @assert false "Couldn't find blocks, nodesets, or sidesets." end end end diff --git a/src/InitialConditions.jl b/src/InitialConditions.jl index b17bcbbe..d2e20950 100644 --- a/src/InitialConditions.jl +++ b/src/InitialConditions.jl @@ -1,10 +1,49 @@ abstract type AbstractInitialCondition{F} end abstract type AbstractInitialConditionContainer end +""" +$(TYPEDEF) +$(TYPEDSIGNATURES) +$(TYPEDFIELDS) +User facing API to define a ```InitialCondition```. +""" struct InitialCondition{F} <: AbstractInitialCondition{F} - var_name::String func::F - block_name::String + block_name::EntityName + nset_name::EntityName + sset_name::EntityName + var_name::String + + """ + $(TYPEDEF) + $(TYPEDSIGNATURES) + $(TYPEDFIELDS) + """ + function InitialCondition( + var_name::String, func::Function; + block_name::EntityName = nothing, + nodeset_name::EntityName = nothing, + sideset_name::EntityName = nothing + ) + if block_name === nothing && nodeset_name === nothing && sideset_name === nothing + _entity_not_provided_error( + "block_name, nodeset_name, or sideset_name required" * + " as input arguments in DirichletBC" + ) + end + count = (block_name !== nothing) + + (nodeset_name !== nothing) + + (sideset_name !== nothing) + if count != 1 + _unsure_entity_type_error("More than one entity type specificed in DirichletBC") + end + # new{typeof(func)}(func, block_name, nodeset_name, sideset_name, var_name) + return InitialCondition{typeof(func)}(var_name, func, block_name, nodeset_name, sideset_name) + end + + function InitialCondition{F}(var_name::String, func::F, block_name, nodeset_name, sideset_name) where F + new{typeof(func)}(func, block_name, nodeset_name, sideset_name, var_name) + end end struct InitialConditionContainer{ @@ -21,9 +60,28 @@ struct InitialConditionContainer{ # to "do the right thing" depending upon the field type # this is set up function InitialConditionContainer(mesh, dof, ic::InitialCondition) - bk = BCBookKeeping(mesh, dof, ic.var_name; block_name = ic.block_name) - vals = zeros(length(bk.dofs)) - new{typeof(bk.dofs), typeof(vals)}(bk.dofs, bk.nodes, vals) + # bk = BCBookKeeping(mesh, dof, ic.var_name; block_name = ic.block_name) + dof_index = _dof_index_from_var_name(dof, ic.var_name) + all_dofs = reshape(1:length(dof), size(dof)) + if ic.block_name !== nothing + conns = mesh.element_conns[ic.block_name] + nodes = sort(unique(conns)) + elseif ic.nset_name !== nothing + nodes = mesh.nodeset_nodes[ic.nset_name] + elseif ic.sset_name !== nothing + nodes = mesh.sideset_nodes[ic.sset_name] + end + + # gather dofs associated with nodes + dofs = all_dofs[dof_index, nodes] + + # sort nodes and dofs for dirichlet bc + dof_perm = _unique_sort_perm(dofs) + dofs = dofs[dof_perm] + nodes = nodes[dof_perm] + + vals = zeros(length(dofs)) + new{typeof(dofs), typeof(vals)}(dofs, nodes, vals) end end diff --git a/src/bcs/BoundaryConditions.jl b/src/bcs/BoundaryConditions.jl index d8848b11..6f356018 100644 --- a/src/bcs/BoundaryConditions.jl +++ b/src/bcs/BoundaryConditions.jl @@ -1,3 +1,5 @@ +const EntityName = Union{Nothing, String} + struct EntityNameNotProvidedError <: AbstractFECError msg::String end diff --git a/src/bcs/DirichletBCs.jl b/src/bcs/DirichletBCs.jl index 8c0f078a..7de25076 100644 --- a/src/bcs/DirichletBCs.jl +++ b/src/bcs/DirichletBCs.jl @@ -1,12 +1,10 @@ abstract type AbstractDirichletBC{F} <: AbstractBC{F} end -const EntityName = Union{Nothing, String} - """ $(TYPEDEF) $(TYPEDSIGNATURES) $(TYPEDFIELDS) -User facing API to define a ```DirichletBC````. +User facing API to define a ```DirichletBC```. """ struct DirichletBC{F} <: AbstractDirichletBC{F} func::F @@ -22,9 +20,9 @@ struct DirichletBC{F} <: AbstractDirichletBC{F} """ function DirichletBC( var_name::String, func::Function; - block_name::Union{Nothing, String} = nothing, - nodeset_name::Union{Nothing, String} = nothing, - sideset_name::Union{Nothing, String} = nothing + block_name::EntityName = nothing, + nodeset_name::EntityName = nothing, + sideset_name::EntityName = nothing ) if block_name === nothing && nodeset_name === nothing && sideset_name === nothing _entity_not_provided_error( diff --git a/test/TestAppTools.jl b/test/TestAppTools.jl index 8c0287c9..8377c677 100644 --- a/test/TestAppTools.jl +++ b/test/TestAppTools.jl @@ -73,34 +73,41 @@ end @test AT.get_cli_arg(parser, "--backend") == "cpu" @test AT.get_cli_arg(parser, "--verbose") == "true" - # help message case long name - args = [ - "--input-file", "input-file.toml", - "--log-file", "log.log", - "--backend", "cpu", - "--verbose", - "--help" - ] - @test_throws AssertionError AT.parse!(parser, args) + # TODO + # currently the help cli arg exits the app cleanly + # so there isn't really a good way to test for + # it in an isolated parse! command + # we should probably rewrite parse to return + # a flag for whether or not to exit the program + # + # # help message case long name + # args = [ + # "--input-file", "input-file.toml", + # "--log-file", "log.log", + # "--backend", "cpu", + # "--verbose", + # "--help" + # ] + # @test_throws AssertionError AT.parse!(parser, args) - # help message case short name - args = [ - "--input-file", "input-file.toml", - "--log-file", "log.log", - "--backend", "cpu", - "--verbose", - "--h" - ] - @test_throws AssertionError AT.parse!(parser, args) + # # help message case short name + # args = [ + # "--input-file", "input-file.toml", + # "--log-file", "log.log", + # "--backend", "cpu", + # "--verbose", + # "--h" + # ] + # @test_throws AssertionError AT.parse!(parser, args) - # test missing option that is required - args = [ - "--input-file", "input-file.toml", - "--log-file", "log.log", - "--verbose", - "--h" - ] - @test_throws AssertionError AT.parse!(parser, args) + # # test missing option that is required + # args = [ + # "--input-file", "input-file.toml", + # "--log-file", "log.log", + # "--verbose", + # "--h" + # ] + # @test_throws AssertionError AT.parse!(parser, args) end @testitem "AppTools - SimpleApp" begin @@ -110,7 +117,7 @@ end "--log-file", "log.log", "--backend", "cpu" ] - app = AT.App("MyApp") + app = AT.App{1}("MyApp") AT.add_cli_arg!(app, "--backend") AT.parse!(app.cli_arg_parser, args) arg = AT.get_cli_arg(app, "--backend") diff --git a/test/TestICs.jl b/test/TestICs.jl index a2f88631..ae488cbd 100644 --- a/test/TestICs.jl +++ b/test/TestICs.jl @@ -7,14 +7,16 @@ end @testitem "ICs - test_ic_input" setup=[ICHelper] begin - ic = InitialCondition("my_var", dummy_ic_func_1, "my_block") + ic = InitialCondition("my_var", dummy_ic_func_1; block_name = "my_block") @test ic.block_name == "my_block" + @test ic.nset_name === nothing + @test ic.sset_name === nothing @test typeof(ic.func) == typeof(dummy_ic_func_1) @test ic.var_name == "my_var" end -@testitem "ICs - ic_container_init_and_update" setup=[ICHelper] begin - ic_in = InitialCondition("displ_x", dummy_ic_func_1, "block_1") +@testitem "ICs - ic_container_init_and_update - block" setup=[ICHelper] begin + ic_in = InitialCondition("displ_x", dummy_ic_func_1; block_name = "block_1") ics = InitialConditions(mesh, dof, [ic_in]) @show ics U = create_field(dof) @@ -26,10 +28,38 @@ end @test all(U[2, :] .== 0.) end +@testitem "ICs - ic_container_init_and_update - nodeset" setup=[ICHelper] begin + nodes = mesh.nodeset_nodes["nset_1"] + ic_in = InitialCondition("displ_x", dummy_ic_func_1; nodeset_name = "nset_1") + ics = InitialConditions(mesh, dof, [ic_in]) + @show ics + U = create_field(dof) + X = mesh.nodal_coords + FiniteElementContainers.update_ic_values!(ics, X) + @test all(values(ics.ic_caches)[1].vals .== 3.) + update_field_ics!(U, ics) + @test all(U[1, nodes] .== 3.) + @test all(U[2, nodes] .== 0.) +end + +@testitem "ICs - ic_container_init_and_update - sideset" setup=[ICHelper] begin + nodes = mesh.sideset_nodes["sset_1"] + ic_in = InitialCondition("displ_x", dummy_ic_func_1; sideset_name = "sset_1") + ics = InitialConditions(mesh, dof, [ic_in]) + @show ics + U = create_field(dof) + X = mesh.nodal_coords + FiniteElementContainers.update_ic_values!(ics, X) + @test all(values(ics.ic_caches)[1].vals .== 3.) + update_field_ics!(U, ics) + @test all(U[1, nodes] .== 3.) + @test all(U[2, nodes] .== 0.) +end + @testitem "ICs - ic_container_init_and_update_juliac_safe" setup=[ICHelper] begin import FiniteElementContainers.Expressions: ScalarExpressionFunction expr_func = ScalarExpressionFunction{Float64}("3.0", ["x", "y"]) - ic_in = InitialCondition{ScalarExpressionFunction{Float64}}("displ_x", expr_func, "block_1") + ic_in = InitialCondition{ScalarExpressionFunction{Float64}}("displ_x", expr_func, "block_1", nothing, nothing) ics = InitialConditions(mesh, dof, [ic_in]) @show ics U = create_field(dof) diff --git a/test/input-file.toml b/test/input-file.toml index 1de53acd..fd83aa17 100644 --- a/test/input-file.toml +++ b/test/input-file.toml @@ -2,12 +2,12 @@ backend = "cpu" [functions.zero] -type = "constant" +type = "scalar expression" expression = "0.0" variables = ["x", "y"] [functions.one] -type = "constant" +type = "scalar expression" expression = "1.0" variables = ["x", "y"]