Zone Decomposition
The zone decomposition system splits the global inverse problem into overlapping subdomains (zones) that can be solved independently. This is the primary approach for full-scale inverse reconstruction in Sentinel.
Overview
The algorithm proceeds as follows:
Generate a regular grid of zone centers (random seed placement via Park-Miller RNG)
Assign elements to zones based on centroid-in-box test with configurable overlap
Grow zones until each has sufficient interior DOF nodes (numsig target)
Extract zone meshes, BCs, and material from the global problem
Solve each zone with CG/GN/L-BFGS optimization
Consolidate zone results into global material (GP-weighted) and displacement (count-averaged)
Apply post-processing regularization (spatial filter, bounds)
Repeat for multiple global iterations
Configuration
Sentinel.ZoneConfig Type
ZoneConfigConfiguration for zone-based domain decomposition.
Fields
zoneedge::SVector{3, Float64}: zone box half-extents in each direction [m]overlap::Float64: overlap fraction (0.0–1.0); overlap region = overlap × zoneedgeoptimizer::Symbol: zone-level optimizer (:cg, :gn, :lbfgs)max_zone_iter::Int: maximum iterations per zone solvezone_tol::Float64: convergence tolerance for zone solvemax_global_iter::Int: maximum global (outer) iterationsglobal_tol::Float64: convergence tolerance for global iterationseed_element::Int: element index to seed zone grid generation (0 = auto-center)randomize_zones::Bool: if true, regenerate zone grid each global iteration with random seed
Zone Grid Generation
Sentinel.generate_zone_grid Function
generate_zone_grid(grid, config::ZoneConfig[, centroids];
seed_element_override=-1) -> ZoneGridGenerate a regular grid of zone centers covering the FEM mesh.
Algorithm (ports Fortran zonegen):
Compute element centroids (or use precomputed)
Determine seed center (from seed_element, override, or mesh center)
Generate regular grid radiating outward with spacing = zoneedge
Prune zones with no elements
Precompute element lists for each zone
Arguments
grid: Ferrite Gridconfig: ZoneConfig with zoneedge, overlap, seed_elementcentroids: (optional) precomputed element centroids to avoid recomputationseed_element_override: if >= 1, use this element as seed instead of config.seed_element
Sentinel.ZoneGrid Type
ZoneGridCollection of zone centers with precomputed element centroids for fast element-in-zone queries.
Fields
centers::Vector{SVector{3, Float64}}: zone center coordinatesconfig::ZoneConfig: zone configurationelement_centroids::Vector{SVector{3, Float64}}: precomputed element centroidszone_elements::Vector{Vector{Int}}: precomputed element lists per zone
Sentinel.ParkMillerRNG Type
ParkMillerRNGPort of the Fortran random(idum) function from zonefunctions.f90. Park-Miller "minimal" RNG with Bays-Durham shuffle (Numerical Recipes RAN1).
Initialized with idum=-1 and one throwaway call, matching the Fortran idum=-1; rr=random(idum) initialization sequence.
Sentinel.park_miller_random! Function
park_miller_random!(rng::ParkMillerRNG) -> Float64Generate the next uniform random deviate in (0, 1), matching the Fortran random(idum) function exactly.
Sentinel.compute_element_centroids Function
compute_element_centroids(grid) -> Vector{SVector{3, Float64}}Compute the centroid of each element by averaging its corner node coordinates. Works for any Ferrite Grid with Hexahedron cells (8 corner nodes).
sourceZone Extraction
Sentinel.build_zone Function
build_zone(zone_id::Int, center::SVector{3, Float64},
zone_elements::Vector{Int},
global_grid, global_dh, global_dof_coords,
global_material::Material, global_meshes::Vector{MaterialMesh},
global_gp2mtrs::Vector{GP2Mtr},
global_meas::Displacement, global_disp::Displacement,
model::AbstractMaterialModel, config::ZoneConfig;
fiber_field=nothing) -> ZoneMaster function that builds a complete Zone from global problem data.
Steps:
Extract zone grid (Ferrite Grid subset)
Create zone DofHandler
Compute DOF coordinate maps
For each material mesh: extract subset following GP2Mtr links
Build zone-local GP2Mtr
Extract zone material properties
Build zone boundary conditions
Extract measured displacement
Allocate stiffness matrix
build_zone(zone_id, center, zone_elements, global_grid, global_dh,
global_dof_coords, global_material, global_meshes, global_gp2mtrs,
global_meas, global_disp, model, config,
global_dof_elcon, global_bnod; fiber_field=nothing) -> ZoneBuild a zone using topological boundary classification (Fortran-matching).
Uses element connectivity to determine boundary nodes: a DOF node is boundary if any element connected to it (in the global mesh) is NOT present in the zone, or if it's a global mesh boundary node.
Additional Arguments
global_dof_elcon: precomputed DOF-node-to-element connectivity frombuild_dof_node_element_connectivityglobal_bnod: global boundary node flags frombuild_global_bnod
build_zone(zone_id, center, zone_elements, state::GlobalProblemState,
material, disp, config; fiber_field=nothing)Convenience overload that unpacks a GlobalProblemState and delegates to the full 14-argument build_zone. Reduces argument count from 14 to 7.
Sentinel.Zone Type
ZoneComplete zone-local problem data, ready for inverse solve.
Fields
zone_id::Int: zone identifiercenter::SVector{3, Float64}: zone center coordinatesgrid: zone-local Ferrite Griddh: zone-local DofHandlercv_disp: CellValues for displacementmodel::AbstractMaterialModel: material modelmapping::ZoneIndexMapping: index maps to globalmaterial::Material: zone-local materialmeshes::Vector{MaterialMesh}: zone-local material meshesgp2mtrs::Vector{GP2Mtr}: zone-local GP-to-material mappingbcs::BoundaryConditions: zone boundary conditionsmeas::Displacement: zone-local measured displacementK::SparseMatrixCSC{ComplexF64}: preallocated stiffness matrixfiber_field: zone-local fiber directions (or nothing)
Sentinel.ZoneIndexMapping Type
ZoneIndexMappingBidirectional index maps between global and zone-local meshes.
Fields
zne2glb_elem::Vector{Int}: zone element index → global element indexglb2zne_elem::Dict{Int, Int}: global element index → zone element indexzne2glb_grid_node::Vector{Int}: zone grid node → global grid nodeglb2zne_grid_node::Dict{Int, Int}: global grid node → zone grid nodezne2glb_dof_node::Vector{Int}: zone DOF node → global DOF nodeglb2zne_dof_node::Dict{Int, Int}: global DOF node → zone DOF nodezne2glb_mtrl_node::Vector{Vector{Int}}: per-mesh zone material node → globalglb2zne_mtrl_node::Vector{Dict{Int, Int}}: per-mesh global material node → zonezne2glb_mtrl_elem::Vector{Vector{Int}}: per-mesh zone material elem → globalglb2zne_mtrl_elem::Vector{Dict{Int, Int}}: per-mesh global material elem → zoneboundary_dof_nodes::Vector{Int}: zone DOF nodes on boundary (prescribed)interior_dof_nodes::Vector{Int}: zone DOF nodes in interior (free)
Sentinel.extract_zone_dof_mapping Function
extract_zone_dof_mapping(global_dh, zone_dh, global_dof_coords, zone_dof_coords;
tol=1e-10, global_coord_lookup=nothing)
-> (zne2glb_dof_node, glb2zne_dof_node)Build bidirectional DOF node maps by matching physical coordinates.
Hex27 DOF nodes include edge midpoints, face centers, and body centers that are NOT stored as Grid nodes. We match by physical position with tolerance.
Pass a pre-built global_coord_lookup from build_dof_coord_lookup to enable O(n_zone) lookup instead of O(n_zone × n_global) linear search.
Sentinel.classify_zone_dof_nodes Function
classify_zone_dof_nodes(zne2glb_dof_node::Vector{Int},
zone_elements_set::Set{Int},
global_dof_elcon::Vector{Vector{Int}},
global_bnod::Union{BitVector, Nothing}=nothing)
-> (boundary, interior)Classify each zone DOF node as boundary (prescribed) or interior (free) using element connectivity (topological approach), matching Fortran.
A node is boundary if:
Any element connected to it (in the global mesh) is NOT in the zone, OR
It is a global boundary node (
global_bnod[gi] == true)
Arguments
zne2glb_dof_node: maps zone DOF node indices to global DOF node indiceszone_elements_set: Set of global element indices in the zone (for O(1) lookup)global_dof_elcon: global DOF-node-to-element connectivity frombuild_dof_node_element_connectivityglobal_bnod: global boundary node flags (from BC file). If nothing, uses bounding box detection on global coordinates.
Returns vectors of zone-local DOF node indices.
sourceSentinel.build_zone_bcs Function
build_zone_bcs(boundary_dof_nodes::Vector{Int},
global_disp::Displacement,
zne2glb_dof_node::Vector{Int},
zone_dh,
zone_ne::Int,
model::AbstractMaterialModel) -> BoundaryConditionsBuild zone-level BoundaryConditions by prescribing the current global displacement estimate at all boundary DOF nodes.
For each boundary DOF node zi:
Map to global DOF node
gi = zne2glb_dof_node[zi]Get displacement (u, v, w) from
global_disp.disp[gi, ds]Set Dirichlet BCs on DOFs
3(zi-1)+1, 3(zi-1)+2, 3(zi-1)+3
Zone Solving
Sentinel.solve_zone! Function
solve_zone!(zone::Zone, config::ZoneConfig, ω::Float64, ρ::Float64;
solver::AbstractLinearSolver=DirectSolver(),
regularizations=nothing,
iteration_offset::Int=0) -> ZoneResultSolve the inverse problem on a single zone.
Build
ForwardProblemContextfrom zone dataDispatch to configured optimizer (
:cg,:gn, or:lbfgs)Return
ZoneResultwith optimized material and convergence history
Sentinel.ZoneResult Type
ZoneResultResult from solving a single zone's inverse problem.
Fields
zone_id::Int: zone identifiermaterial::Material: zone-local optimized materialcalc::Displacement: zone-local calculated displacementhistory::ConvergenceHistory: convergence historymapping::ZoneIndexMapping: index maps for scattering back to global
Consolidation
Sentinel.prepare_accumulation! Function
prepare_accumulation!(material::Material)Zero all property values and counts in preparation for zone accumulation.
sourceSentinel.prepare_displacement_accumulation! Function
prepare_displacement_accumulation!(disp::Displacement)Zero displacement values and counts for accumulation.
sourceSentinel.scatter_zone_material! Function
scatter_zone_material!(global_material::Material, result::ZoneResult)Accumulate zone-local property values into the global material arrays, using Gauss-point weighted accumulation (matching Fortran MRE-Zone consolidation).
Each zone's contribution is weighted by the number of Gauss points that map to each material node in that zone. The accumulated GP counts (rgp/igp) are used by consolidate_material! to compute the weighted average.
Sentinel.scatter_zone_displacement! Function
scatter_zone_displacement!(global_disp::Displacement,
zone_calc::Displacement,
zne2glb_dof_node::Vector{Int})Accumulate zone-local calculated displacement into the global displacement.
sourceSentinel.consolidate_material! Function
consolidate_material!(material::Material, prev_material::Material)Divide accumulated property values by accumulated Gauss-point counts to complete GP-weighted averaging (matching Fortran MRE-Zone consolidation).
For nodes with no GP contributions (rgp=0), restore the previous iteration's value to avoid zeroing out uncovered regions.
sourceSentinel.consolidate_displacement! Function
consolidate_displacement!(disp::Displacement, prev_disp::Displacement)Divide accumulated displacement values by their counts to complete averaging. For nodes with count == 0 (uncovered by any zone), retain the previous iteration's displacement (matching Fortran where globcalc is NOT zeroed between iterations, so uncovered nodes keep their previous value).
sourceSentinel.consolidate_global! Function
consolidate_global!(material::Material, disp::Displacement,
prev_material::Material, prev_disp::Displacement)Complete the consolidation by averaging both material and displacement. Uncovered material nodes are restored from prev_material. Uncovered displacement nodes are restored from prev_disp (previous iteration's displacement, matching Fortran where globcalc starts zeroed and is never reset).
High-Level Driver
Sentinel.zone_decomposition_solve! Function
zone_decomposition_solve!(material::Material, grid, dh,
gp2mtrs::Vector{GP2Mtr},
meshes::Vector{MaterialMesh},
meas::Displacement,
bcs::BoundaryConditions,
model::AbstractMaterialModel,
ω::Float64, ρ::Float64,
config::ZoneConfig;
solver::AbstractLinearSolver=DirectSolver(),
regularizations=nothing,
fiber_field=nothing,
verbose::Bool=true)
-> (Material, Displacement, GlobalIterationState)Run the zone decomposition inverse solver (serial version).
Algorithm
Generate zone grid covering the mesh
Initialize displacement estimate from measured data
For each global iteration: a. Build zone-local problems from current global state b. Solve each zone independently c. Scatter/consolidate results back to global arrays d. Check convergence (material_epsilon < global_tol)
Returns
- Updated material, calculated displacement, and iteration state
Sentinel.zone_decomposition_solve_parallel! Function
zone_decomposition_solve_parallel!(material::Material, grid, dh,
gp2mtrs::Vector{GP2Mtr},
meshes::Vector{MaterialMesh},
meas::Displacement,
bcs::BoundaryConditions,
model::AbstractMaterialModel,
ω::Float64, ρ::Float64,
config::ZoneConfig;
solver::AbstractLinearSolver=DirectSolver(),
regularizations=nothing,
fiber_field=nothing,
verbose::Bool=true,
pmap_func::Function=map)
-> (Material, Displacement, GlobalIterationState)Run zone decomposition with a user-provided parallel map function.
To use Distributed.jl, pass pmap_func=Distributed.pmap after loading the Distributed stdlib. With the default map, this runs serially.
Arguments
Same as
zone_decomposition_solve!plus:pmap_func: parallel map function (default:mapfor serial)
Sentinel.GlobalIterationState Type
GlobalIterationStateTracks the state of the global zone decomposition iteration loop.
Fields
iteration::Int: current global iteration numbermaterial::Material: current global material estimateglobal_obj::Float64: global objective valuezone_objectives::Vector{Float64}: per-zone objective valuesmat_epsilon::Float64: relative material change from previous iterationconverged::Bool: whether global convergence criterion is met
Inverse Problem Setup
These functions handle the full inverse problem setup from a runfile:
Sentinel.InverseRunfileConfig Type
InverseRunfileConfigComplete parsed inverse problem runfile.
sourceSentinel.parse_inverse_runfile Function
parse_inverse_runfile(filename) -> InverseRunfileConfigParse an inverse problem .dat runfile and return an InverseRunfileConfig.
The inverse runfile alternates between descriptive label lines and data lines. This parser reads all lines and processes them sequentially, skipping label lines explicitly to match the Fortran readinverse read order.
Sentinel.setup_inverse_problem Function
setup_inverse_problem(config::InverseRunfileConfig,
basedir::AbstractString=".") -> InverseProblemSetupLoad all files referenced by an InverseRunfileConfig and return an InverseProblemSetup with the complete inverse problem setup.
Returns
InverseProblemSetup with fields:
grid: Ferrite Grid (hex27)dh: DofHandlercv_disp: CellValues for scalar hex27model: AbstractMaterialModelmaterial: Material with initialized propertiesmeshes: Vectorgp2mtrs: Vectorbcs: BoundaryConditionsmeas: Displacement (measured)omega: angular frequencyK: preallocated stiffness matrixzone_config: ZoneConfigregularizations: Vectorconfig: the original InverseRunfileConfigsolver: DirectSolverfull_connectivity: ne×27 connectivitydof_to_mesh: DOF-to-mesh-node mapping
Sentinel.run_inverse_solve! Function
run_inverse_solve!(setup::InverseProblemSetup; verbose::Bool=true,
callback::Union{Nothing, Function}=nothing,
pmap_func::Function=map)
-> (Material, Displacement, GlobalIterationState)Run the zone decomposition inverse solver with escalating iteration structures.
This is a feature-complete version of zone_decomposition_solve! that handles:
Escalating zone iteration structures (CG count changes with global iteration)
Reconstruction indicators (non-reconstructed properties are preserved)
Post-processing regularizations (VanHouten, Bounds, SpatialFilter)
The optional callback(giter, material, state) is called after each global iteration, enabling per-iteration recording of material state, timing, or file output.
Pass pmap_func (e.g. Distributed.pmap) to parallelize zone solves across workers.
Sentinel.build_regularizations Function
build_regularizations(config::InverseRunfileConfig,
material::Material,
meshes::Vector{MaterialMesh};
gp2mtrs::Union{Vector{GP2Mtr}, Nothing}=nothing,
region_info::Union{RegionInfo, Nothing}=nothing) -> Vector{AbstractRegularization}Construct regularization objects based on the parsed regularization config.
When gp2mtrs is provided, computes a sensitivity mask (matching Fortran nodsense) for each spatial filter property: only material nodes covered by at least one Gauss point participate in Gaussian filtering.
Sentinel.InverseProblemSetup Type
InverseProblemSetupComplete inverse problem setup with compile-time field name checking.
Replaces the NamedTuple returned by setup_inverse_problem. All field names match the original NamedTuple, so existing setup.field access works without changes.
Fields
grid: Ferrite Grid (hex27)dh: DofHandlercv_disp: CellValues for displacement interpolationmodel: material model singletonomega: angular frequency [rad/s]material:Materialwith initialized propertiesmeshes: vector ofMaterialMeshgp2mtrs: vector ofGP2Mtrbcs:BoundaryConditionsmeas: measuredDisplacementdataK: preallocated stiffness matrixzone_config:ZoneConfigfor zone decompositionregularizations: vector ofAbstractRegularizationconfig: originalInverseRunfileConfigsolver: linear solver backendfull_connectivity: full 27-node connectivity from mesh filedof_to_mesh: mapping from Ferrite DOF nodes to mesh file nodes