Regularization
Sentinel provides 10 regularization methods in three categories: penalty methods (added to the objective), Hessian modifiers (modify the update direction), and post-processing methods (applied after each iteration).
See the Mathematical Reference for complete functional definitions.
Abstract Interface
Sentinel.AbstractRegularization Type
AbstractRegularizationAbstract supertype for all regularization methods.
sourceSentinel.WeightSchedule Type
WeightScheduleLinear interpolation of a regularization weight from start_weight to end_weight over iterations delay+1 through max_iter.
Before iteration delay, the weight is 0. After max_iter, it stays at end_weight.
Ports the weight ramp-up logic from the Fortran regularization modules.
sourceSentinel.evaluate_weight Function
evaluate_weight(ws::WeightSchedule, iteration::Int) -> Float64Evaluate the scheduled weight at the given iteration. Returns 0 for iterations ≤ delay, then linearly interpolates.
sourcePenalty Methods
Tikhonov
Sentinel.TikhonovReg Type
TikhonovReg <: AbstractRegularizationTikhonov (L2) regularization penalizing deviation from reference values.
Fields
prop_index::Int: which material property (1-based) this applies toweight_real::WeightSchedule: weight schedule for real partweight_imag::WeightSchedule: weight schedule for imaginary parttheta_ref_real::Vector{Float64}: (npr,) reference values for real parttheta_ref_imag::Vector{Float64}: (npi,) reference values for imaginary part
Total Variation
Sentinel.TotalVariationReg Type
TotalVariationReg <: AbstractRegularizationTotal Variation regularization with smooth (Huber-like) approximation.
Fields
prop_index::Int: which material property (1-based)weight_real::WeightSchedule: weight schedule for real partweight_imag::WeightSchedule: weight schedule for imaginary partepsilon::Float64: smoothing parameter δ for √(|∇θ|² + ε²)active::Bool: false on first iteration, true after update_weights!mtrl_mesh::MaterialMesh: hex8 material mesh for integrationnode_to_elements::Vector{Vector{Int}}: precomputed inverse connectivity
Soft Prior
Region-based Laplacian penalty:
Sentinel.SoftPriorReg Type
SoftPriorReg <: AbstractRegularizationSoft prior regularization using region-based Laplacian penalty.
Fields
prop_index::Int: which material property (1-based)weight_real::WeightSchedule: weight schedule for real partweight_imag::WeightSchedule: weight schedule for imaginary partregion_info::RegionInfo: region assignment for material mesh nodes
Sentinel.RegionInfo Type
RegionInfoRegion assignment information for nodes in a material mesh. Used by soft prior regularization to penalize deviation within regions.
Fields
node_region::Vector{Int}: (nn,) region ID per node (0 = no region)nregions::Int: number of distinct regionsregion_node_count::Vector{Int}: (nregions,) node count per regionregion_node_lists::Vector{Vector{Int}}: (nregions,) node lists per region
Exterior Constraint
Sentinel.ExteriorConstraintReg Type
ExteriorConstraintReg <: AbstractRegularizationExterior constraint penalty that enforces parameter bounds.
Penalty is zero when parameters are within bounds and grows quadratically with violation magnitude. The scalar s = prop.scalar[1] (real) or prop.scalar[2] (imaginary) converts from stored normalized values to physical units before checking bounds.
Fields
prop_index::Int: which material property (1-based)weight::Float64: penalty weightmin_real::Float64: minimum allowed physical real valuemax_real::Float64: maximum allowed physical real valuemin_imag::Float64: minimum allowed physical imaginary valuemax_imag::Float64: maximum allowed physical imaginary value
Hessian Modifiers
Joachimowicz
Scales the diagonal Hessian:
Sentinel.JoachimowiczReg Type
JoachimowiczReg <: AbstractRegularizationJoachimowicz Hessian regularization.
Adds avg_diag · total_error · w to each diagonal entry of the Hessian for the specified property parameters.
Fields
prop_index::Int: which material property (1-based)weight::WeightSchedule: weight schedule
Marquardt
Normalized Levenberg-Marquardt damping: normalize
Sentinel.MarquardtReg Type
MarquardtReg <: AbstractRegularizationLevenberg-Marquardt Hessian regularization with adaptive weight.
Fields
prop_index::Int: which material property (1-based)current_weight::Float64: current λ valuestart_weight::Float64: initial λ value (for reset)min_weight::Float64: minimum λ value (floor)delta::Float64: reduction factor (< 1.0) applied on successful stepsadjust_threshold::Float64: |1−α| threshold for adaptation
Sentinel.normalize_and_regularize! Function
normalize_and_regularize!(H, grad_vec, reg::MarquardtReg, grad::MaterialGradient,
material::Material) -> Vector{Float64}Perform the Marquardt normalization and regularization in-place:
Compute D_k = √|H[k,k]| for relevant parameters
Normalize: H[i,j] /= D_i·D_j, grad[k] /= D_k
Add λ to diagonal: H[k,k] += reg.current_weight
Returns the scaling vector D for post-solve rescaling.
sourceSentinel.rescale_solution! Function
rescale_solution!(delta_theta, D, reg::MarquardtReg, grad::MaterialGradient,
material::Material)Rescale the Marquardt solution: Δθ[k] /= D_k for relevant parameters.
sourcePost-Processing Methods
Van Houten
Clips power-law exponents to
Sentinel.VanHoutenReg Type
VanHoutenReg <: AbstractRegularizationVan Houten post-processing regularization.
Clips power-law exponent values prop.rvalue[k, 2] and prop.ivalue[k, 2] to the range [-level, level] for properties with nvpp >= 3.
Fields
level::Float64: maximum allowed power-law exponent magnitude
Sentinel.apply_van_houten! Function
apply_van_houten!(material::Material, reg::VanHoutenReg)Clip power-law exponents in all properties with nvpp >= 3 to [-level, level].
Spatial Filter
Gaussian smoothing with widths
Sentinel.SpatialFilterReg Type
SpatialFilterReg <: AbstractRegularizationSpatial filter (Gaussian smoothing) post-processing regularization.
Fields
prop_index::Int: which material property (1-based)sigma_real_init::Float64: initial Gaussian width for real partsigma_real_final::Float64: final Gaussian width for real partsigma_imag_init::Float64: initial Gaussian width for imaginary partsigma_imag_final::Float64: final Gaussian width for imaginary partmax_iter::Int: total global iterations (for weight interpolation)const_reg_iters::Int: constant regularization iterations at endcutoff_multiplier::Float64: neighbor search radius = max_sigma × cutoff_multipliermtrl_mesh::MaterialMesh: material mesh for coordinate lookupneighbor_indices::Vector{Vector{Int}}: precomputed neighbor lists per nodeneighbor_distances::Vector{Vector{Float64}}: precomputed distances per noderegion_info::Union{RegionInfo, Nothing}: optional region constraintssensitivity_mask::Union{BitVector, Nothing}: optional mask for GP-covered nodes (Fortran nodsense)
Sentinel.apply_spatial_filter! Function
apply_spatial_filter!(material::Material, reg::SpatialFilterReg;
iteration::Int=1)Apply Gaussian spatial smoothing to material property values: θ_filtered[i] = Σ_j w_j·θ_j / Σ_j w_j where w_j = exp(−d²/(2σ²)).
Sigma is interpolated between init and final values based on iteration.
sourceBounds (McGarry)
Enforces Poisson ratio bounds via clipping.
Sentinel.BoundsReg Type
BoundsReg <: AbstractRegularizationMcGarry bounds post-processing regularization.
Fields
max_poisson::Float64: maximum allowed Poisson's ratio (e.g., 0.485) for Model 3property_bounds::Vector{PropertyBounds}: per-property min/max constraints
Sentinel.apply_mcgarry_bounds! Function
apply_mcgarry_bounds!(material::Material, reg::BoundsReg)Enforce material property bounds (post-processing, no gradient contribution).
General min/max clamping: for each constrained property, clamp
scalar * rvalueto[min, max](matching Fortran mcgarrybounds.f90).Model 3 Poisson ratio: clamp λ so that ν ≤ ν_max.
Force Balance
Physics-consistency penalty: the equilibrium residual of the measured displacement field under the current property estimate,
Weights are configured as fractions of the initial data misfit (relative mode, the default for config-driven runs): the fraction is resolved to an absolute weight once at solve start via benchmark/results/mgh_fb_calibration/): fractions ≤ 0.2 are gentle (data misfit at or slightly better than baseline, per-voxel correlation ≥ 0.99 against the unregularized reconstruction); ≥ 5 over-regularizes. Enabling force balance roughly doubles per-iteration cost (~2.05× on the MGH batched-GPU benchmark, down from 2.26× via exact affine line-search caches for the penalty and gradient — see fb_begin_line_search!); the dominant remaining cost is the per-iteration gradient evaluations, with bounds-clamped line-search trials falling back to full evaluation.
macOS: relative-weight resolution is a one-time iterative solve
Resolving the relative weight (w = frac · D0/S0) needs one global forward solve for D0, once at solve start. On macOS this uses a condensed, factorization-free iterative solve (seconds to a few minutes) instead of a direct factorization, which avoids an upstream Apple-libmalloc crash; the resolved value is accurate and Linux is unaffected (it keeps the faster direct solve). A slow ForceBalanceReg: relative weights resolved, D0/S0 = … step on macOS is the expected one-time cost — see macOS FB / xzone Crash.
Choosing the fraction — noise matters (ground-truth phantom arbitration, benchmark/results/fb_phantom_arbitration/): on equilibrium-consistent (clean / high-SNR / denoised) data, strong force balance genuinely improves accuracy (frac = 1.0: −21 % RMSE vs truth, better contrast recovery). On realistically noisy data it becomes a bias — the equilibrium pull drives the properties to "explain" measurement noise (frac = 1.0 at 2 % noise: +25 % RMSE, flattened contrast). Keep the default frac = 0.1 (neutral) for noisy in-vivo data; reserve larger fractions for high-SNR or denoised acquisitions.
Sentinel.ForceBalanceReg Type
ForceBalanceReg <: AbstractRegularizationForce-balance (equilibrium residual on measured displacements) regularization.
Fields
weight::WeightSchedule: penalty weight schedulerelative::Bool: whentrue,weightholds fractions of the natural scaleD0/S0(initial data misfit over raw equilibrium penalty at the initial material) rather than absolute weights.run_inverse_solve!resolves relative weights to absolute ones once at solve start (one extra global forward solve); a relative instance cannot be bound directly.bound context fields (set by
bind_context!; untyped becauseForwardProblemContextis defined after the regularization includes)
Construct with ForceBalanceReg(weight::WeightSchedule; relative=false) or ForceBalanceReg(w::Float64; relative=false); the context is bound later per zone.
The raw penalty ½‖M·K(θ)·û_meas‖² has force² units and its magnitude is mesh- and dataset-dependent (MGH calibration 2026-07-12: raw penalty 1.3e5 vs data misfit 5.2e-2 — a natural scale of ~4e-7). Relative mode makes the configured number portable: w = 0.2, relative = true means "an equilibrium penalty worth 20% of the initial data misfit", on any mesh and any dataset.
Sentinel.bind_context! Function
bind_context!(reg::AbstractRegularization, ctx; kwargs...)Bind a ForwardProblemContext to a regularization that needs FE-level data (mesh, DOFs, boundary conditions, measured displacements) beyond the Material passed to compute_penalty/add_gradient!. Called from solve_zone! after the context is built. No-op for most regularizations.
bind_context!(regs, ctx; kwargs...)Bind a context to every regularization in a collection (nothing allowed).
bind_context!(reg::ForceBalanceReg, ctx;
gradient_backend=nothing, ka_edata=nothing,
ka_workspace=nothing)Bind a zone's ForwardProblemContext to the regularizer so that compute_penalty/add_gradient! can assemble the equilibrium residual. Called from solve_zone!. Throws for unsupported material models.
Sentinel.resolve_fb_weights! Function
resolve_fb_weights!(regularizations, ctx, material; K=nothing) -> Union{Float64, Nothing}Resolve every relative-weighted ForceBalanceReg in regularizations to an absolute weight, anchored at the current (initial) material:
w_abs = w_frac · D0 / S0where S0 = ½‖M·K(θ₀)·û_meas‖² is the raw equilibrium penalty (computed with a temporary weight-1 instance bound to the global ctx) and D0 is the data misfit of the forward solution at θ₀ (one global forward solve — the only nontrivial cost, comparable to a single global iteration's solve; pass a preallocated global stiffness via K to avoid an extra allocation).
On macOS (default), D0 is computed via a condensed, factorization-free iterative solve (_fb_d0_condensed_iterative) to avoid the xzone-malloc crash; the K preallocation is used only by the direct path (Linux, or macOS with SENTINEL_ALLOW_MACOS_FB_RELATIVE=1).
Returns the scale D0/S0, or nothing if no unresolved relative instance is present (in which case nothing is computed). Called by run_inverse_solve! before the first zone solve; safe to call directly for custom drivers.
MGH calibration (2026-07-12, 12-iter batched-GPU sweep): fractions ≤ 0.2 are gentle (data misfit slightly better than baseline, per-voxel corr ≥ 0.99 vs unregularized), 1.0 gave the best misfit with substantive reconstruction changes, and 5.0 over-regularized. See benchmark/results/mgh_fb_calibration/.
Sentinel.fb_begin_line_search! Function
fb_begin_line_search!(regs, material, delta_theta, grad) -> nothingArm the line-search caches on every bound ForceBalanceReg in regs, for a search from material (θ0) along delta_theta (the direction apply_material_update! applies). Both FB terms are exact in the step α because the Model-1 element stiffness is affine in μ (K_uu = μ·G − ω²ρ·M; K_up, K_pp are μ-independent):
penalty:
½w(c0 + 2α·c1 + α²·c2)from residuals at θ0 and θ0 + 1·d;gradient:
g(α) = g0 + α·g_d, whereg0reuses the CG-point gradient (add_gradient!remembers its last full evaluation keyed by the μ values, and the CG driver always evaluates at the search base), so arming costs the two residual loops plus a single extra gradient evaluation.
Trial-point compute_penalty/add_gradient! then cost O(nμ)/O(nparams). Only activates when the direction touches property 1 (μ) exclusively — a direction with κ content breaks affinity (K_pp ∝ 1/κ) and leaves the cache off (full evaluation per trial, exact as before). Trials knocked off the cached line (e.g. by bounds clamping) are detected by an α-projection consistency guard and evaluated fully. Call fb_end_line_search! after the search commits.
Sentinel.fb_end_line_search! Function
fb_end_line_search!(regs) -> nothingDisarm the line-search quadratic cache on every ForceBalanceReg in regs (the buffers are kept for reuse by the next search).
CG Update Scaling
Sentinel.cg_update_scale Function
cg_update_scale(alpha, material_orig, material_new, threshold) -> Float64Scale the CG step size alpha so that the relative material change does not exceed threshold.
Computes eps = material_epsilon(orig, new), then:
If
eps > threshold: returnalpha * threshold / epsOtherwise: return
alphaunchanged
Specification API
The high-level regularization spec types passed to InversionProblem (regularization = [...]). These are declarative descriptions that the solver lowers to the runtime regularizers documented above; they are distinct from the legacy *Reg classes.
Weights on TotalVariation, SpatialFilter, Tikhonov, Joachimowicz, SoftPrior, Marquardt, ExteriorConstraint, and CGResidualScaling may be per property: pass a vector aligned with props (e.g. weight = [1e-16, 2e-16]). Legacy .dat runfiles that set distinct weights per property now survive load_inversion and the TOML round trip without collapsing to the first property: per-property weights are preserved for every regularizer (no weight collapses). The one remaining scalar collapse is TotalVariation's delta (the Charbonnier smoothing epsilon), which the spec carries as a single value; a legacy runfile with per-property/complex delta collapses to the first value with a warning.
Sentinel.Tikhonov Type
Tikhonov(; props=:all, weight=1e-18)Tikhonov (L2) penalty toward the initial property values (indicator 3).
sourceSentinel.TotalVariation Type
TotalVariation(; props=:all, weight=5e-16, delta=1e-19)Total-variation penalty (runfile indicator 1).
sourceSentinel.SpatialFilter Type
SpatialFilter(; props=:all, sigma=0.003 => 0.0015)Gaussian spatial filtering of the property update (post-processing smoothing, runfile indicator 2). sigma is the Gaussian width in meters, not a penalty weight.
Sentinel.Marquardt Type
Marquardt(; props=:all, start_weight=100.0, min_weight=1e-11,
delta=0.5, adjust_threshold=0.25)Marquardt adaptive damping (indicator 4).
sourceSentinel.Joachimowicz Type
Joachimowicz(; props=:all, weight=5.0)Joachimowicz parameter-magnitude penalty (indicator 5).
sourceSentinel.ExteriorConstraint Type
ExteriorConstraint(; props=:all, weight=1e-14, bounds=nothing)Soft penalty for property values outside bounds (indicator 6). bounds is one (min_re, max_re, min_im, max_im) tuple per property; nothing uses the standard EMPIRE bounds (3-property layout only).
Sentinel.CGResidualScaling Type
CGResidualScaling(; weight=0.04 => 0.01)Conjugate-gradient residual scaling (indicator 7). Applies to all properties.
sourceSentinel.VanHouten Type
VanHouten(level=1.2)Van Houten update-limiting post-processing (indicator 8).
sourceSentinel.McGarryBounds Type
McGarryBounds(; bounds=nothing)Hard clamping of property values to bounds after each iteration (indicator 9). Bound semantics as in ExteriorConstraint; when both are present they must agree (the runfile format shares one bounds table).
Sentinel.SoftPrior Type
SoftPrior(; props=:all, weight=1e-11, delay=0)Region-based soft-prior penalty (indicator 10).
Warning
Not yet wired into the solver: nothing reads the region stack into a RegionInfo, so InversionProblem warns and the term is ignored at setup. The type exists so the TOML schema is stable for when the loader lands.
Sentinel.ForceBalance Type
ForceBalance(; weight=0.1, relative=true)Force-balance (equilibrium residual on the measured displacement field) penalty — a physics-consistency term for Model-1 (isotropic incompressible) reconstructions. It has no Fortran indicator slot; it is a Julia-side extension carried alongside the runfile regularizers.
In relative mode (the default) weight is a fraction of the initial data misfit D0/S0, resolved to an absolute weight once at solve start — the same number is portable across meshes and datasets. With relative=false it is an absolute weight (mesh- and dataset-dependent). weight may be a scalar or an init => final ramp.
Calibrated guidance (benchmark/results/mgh_fb_calibration/, benchmark/results/fb_phantom_arbitration/): weight ≤ 0.2 is gentle and safe on noisy in-vivo data; larger fractions improve accuracy only on clean/high-SNR/denoised data.