Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions src/RadialBasisFunctions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,24 @@ export degree, dim
include("utils.jl")
export find_neighbors, reorder_points!

# Boundary types needed by solve.jl and solve_utils.jl
include("boundary_types.jl")

include("operators/operators.jl")
export RadialBasisOperator, ScalarValuedOperator, VectorValuedOperator
export update_weights!, is_cache_valid

include("solve_utils.jl")

# Boundary types needed by solve.jl
include("boundary_types.jl")

include("solve.jl")

# New clean Hermite implementation
include("solve_hermite.jl")
export BoundaryCondition, Dirichlet, Neumann, Robin
export α, β, is_dirichlet, is_neumann, is_robin
export HermiteBoundaryInfo, StencilType, StandardStencil, HermiteStencil
export HermiteBoundaryInfo, StencilType, InternalStencil, HermiteStencil
export stencil_type, has_boundary_points


include("operators/custom.jl")
export Custom, custom

Expand Down
122 changes: 104 additions & 18 deletions src/boundary_types.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@
Boundary condition types and utilities for Hermite interpolation.
"""

using StaticArraysCore: StaticVector

# Simple boundary condition type
struct BoundaryCondition{T<:Real}
α::T
β::T

function BoundaryCondition(α::A, β::B) where {A,B}
T = promote_type(A, B)
new{T}(α, β)
return new{T}(α, β)
end
end

Expand All @@ -19,7 +21,7 @@ end

# Predicate functions
is_dirichlet(bc::BoundaryCondition) = isone(bc.α) && iszero(bc.β)
is_neumann(bc::BoundaryCondition) = iszero(bc.α) && isone(bc.β)
is_neumann(bc::BoundaryCondition) = iszero(bc.α) && isone(bc.β)
is_robin(bc::BoundaryCondition) = !iszero(bc.α) && !iszero(bc.β)

# Constructor helpers
Expand All @@ -28,30 +30,114 @@ Neumann(::Type{T}=Float64) where {T<:Real} = BoundaryCondition(zero(T), one(T))
Robin(α::Real, β::Real) = BoundaryCondition(α, β)

# Boundary information for a local stencil
struct HermiteBoundaryInfo{T<:Real}
"""
This struct is meant to be used to correctly broadcast the build_stencil() function
When Hermite scheme is used, it can be given to _build_stencil!() in place of the sole data.
"""
struct HermiteStencilData{T<:Real}
data::AbstractVector{Vector{T}} # Coordinates of stencil points (stored as Vector{T} for efficiency)
is_boundary::Vector{Bool}
boundary_conditions::Vector{BoundaryCondition{T}}
normals::Vector{Vector{T}}

function HermiteBoundaryInfo(
normals::Vector{Vector{T}} # Normals stored as Vector{T}

# Generic constructor that accepts both Vector{T} and StaticVector{N,T} inputs
function HermiteStencilData(
data::AbstractVector{<:AbstractVector{T}}, # Accept both Vector{T} and StaticVector{N,T}
is_boundary::Vector{Bool},
boundary_conditions::Vector{BoundaryCondition{T}},
normals::Vector{Vector{T}}
normals::AbstractVector{<:AbstractVector{T}}, # Accept both Vector{T} and StaticVector{N,T}
) where {T<:Real}
@assert length(is_boundary) == length(boundary_conditions) == length(normals)
new{T}(is_boundary, boundary_conditions, normals)
@assert length(data) ==
length(is_boundary) ==
length(boundary_conditions) ==
length(normals)

# Convert input data to Vector{Vector{T}} for internal storage
# This handles both Vector{T} and StaticVector{N,T} inputs
data_vectors = [Vector{T}(point) for point in data]
normals_vectors = [Vector{T}(normal) for normal in normals]

return new{T}(data_vectors, is_boundary, boundary_conditions, normals_vectors)
end
end

#pre-allocation constructor
function HermiteStencilData{T}(k::Int, dim::Int) where {T<:Real}
data = [Vector{T}(undef, dim) for _ in 1:k] # Pre-allocate with correct dimension
is_boundary = Vector{Bool}(falses(k))
boundary_conditions = [Dirichlet(T) for _ in 1:k]
normals = [Vector{T}(undef, dim) for _ in 1:k] # Pre-allocate with correct dimension
return HermiteStencilData(data, is_boundary, boundary_conditions, normals)
end

"""
Unified function that handles both Vector{T} and StaticVector{N,T} data types.
The key insight is that both types support broadcasting (`.=`) operations,
so we can write one generic implementation.

This function populates local boundary information for a specific stencil within a kernel,
extracting boundary data for the neighbors of eval_idx and filling the pre-allocated
HermiteStencilData structure.

# Arguments
- `hermite_data`: Pre-allocated HermiteStencilData structure to fill
- `global_data`: Global data vector (accepts both Vector{T} and StaticVector{N,T})
- `neighbors`: Adjacency list for eval_idx (the neighbors)
- `is_boundary`: Global is_boundary vector for all points
- `boundary_conditions`: Global boundary_conditions vector for all points
- `normals`: Global normals vector for all points
"""
function update_stencil_data!(
hermite_data::HermiteStencilData{T}, # Pre-allocated structure passed in
global_data::AbstractVector{<:AbstractVector{T}}, # Accepts both Vector{T} and StaticVector{N,T}
neighbors::Vector{Int}, # adjl[eval_idx]
is_boundary::Vector{Bool},
boundary_conditions::Vector{BoundaryCondition{T}},
normals::AbstractVector{<:AbstractVector{T}}, # Accepts both Vector{T} and StaticVector{N,T}
global_to_boundary::Vector{Int},
) where {T}
k = length(neighbors)

# Fill local boundary info for each neighbor (in-place, no allocation)
# This works for both Vector{T} and StaticVector{N,T} thanks to broadcasting
@inbounds for local_idx in 1:k
global_idx = neighbors[local_idx]
hermite_data.data[local_idx] .= global_data[global_idx] # Broadcasting works for both types
hermite_data.is_boundary[local_idx] = is_boundary[global_idx]

if is_boundary[global_idx]
boundary_idx = global_to_boundary[global_idx]
hermite_data.boundary_conditions[local_idx] = boundary_conditions[boundary_idx]
hermite_data.normals[local_idx] .= normals[boundary_idx] # Broadcasting works for both types
else
# Set default Dirichlet for interior points (not used but keeps type consistency)
hermite_data.boundary_conditions[local_idx] = Dirichlet(T)
fill!(hermite_data.normals[local_idx], zero(T))
end
end

return nothing
end

# Trait types for dispatch
abstract type StencilType end
struct StandardStencil <: StencilType end
struct InternalStencil <: StencilType end
struct DirichletStencil <: StencilType end
struct HermiteStencil <: StencilType end

# Trait function to determine stencil type
stencil_type(boundary_info::Nothing) = StandardStencil()
stencil_type(boundary_info::HermiteBoundaryInfo) = any(boundary_info.is_boundary) ? HermiteStencil() : StandardStencil()

# Convenience function to check if any boundary points in stencil
has_boundary_points(boundary_info::Nothing) = false
has_boundary_points(boundary_info::HermiteBoundaryInfo) = any(boundary_info.is_boundary)
function stencil_type(
is_boundary::Vector{Bool},
boundary_conditions::Vector{BoundaryCondition{T}},
eval_idx::Int,
neighbors::Vector{Int},
global_to_boundary::Vector{Int},
) where {T}
if sum(is_boundary[neighbors]) == 0
return InternalStencil()
elseif is_boundary[eval_idx] &&
is_dirichlet(boundary_conditions[global_to_boundary[eval_idx]])
return DirichletStencil()
else
return HermiteStencil()
end
end
2 changes: 1 addition & 1 deletion src/interpolation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ function Interpolator(x, y, basis::B=PHS()) where {B<:AbstractRadialBasis}
mon = MonomialBasis(dim, basis.poly_deg)
data_type = promote_type(eltype(first(x)), eltype(y))
A = Symmetric(zeros(data_type, n, n))
_build_collocation_matrix!(A, x, basis, mon, k, StandardStencil())
_build_collocation_matrix!(A, x, basis, mon, k)
b = data_type[i < k ? y[i] : 0 for i in 1:n]
w = A \ b
return Interpolator(x, y, w[1:k], w[(k + 1):end], basis, mon)
Expand Down
2 changes: 2 additions & 0 deletions src/operators/custom.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ struct Custom{F<:Function} <: AbstractOperator
end
(op::Custom)(basis) = op.ℒ(basis)

# Hermite-compatible method now uses the generic dispatcher in solve_hermite.jl

# pretty printing
print_op(op::Custom) = "Custom Operator"
90 changes: 90 additions & 0 deletions src/operators/directional.jl
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,38 @@ function directional(
return RadialBasisOperator(ℒ, data, eval_points, basis; k=k, adjl=adjl)
end

"""
function directional(data, eval_points, v, basis, is_boundary, boundary_conditions, normals; k=autoselect_k(data, basis))

Builds a Hermite-compatible `RadialBasisOperator` where the operator is the directional derivative, `Directional`.
The additional boundary information enables Hermite interpolation with proper boundary condition handling.
"""
function directional(
data::AbstractVector,
eval_points::AbstractVector,
v::AbstractVector,
basis::B,
is_boundary::Vector{Bool},
boundary_conditions::Vector{<:BoundaryCondition},
normals::Vector{<:AbstractVector};
k::T=autoselect_k(data, basis),
adjl=find_neighbors(data, eval_points, k),
) where {B<:AbstractRadialBasis,T<:Int}
Dim = length(first(data))
ℒ = Directional{Dim}(v)
return RadialBasisOperator(
ℒ,
data,
eval_points,
basis,
is_boundary,
boundary_conditions,
normals;
k=k,
adjl=adjl,
)
end

function _build_weights(ℒ::Directional{Dim}, data, eval_points, adjl, basis) where {Dim}
v = ℒ.v
if !(length(v) == Dim || length(v) == length(data))
Expand All @@ -68,5 +100,63 @@ function _build_weights(ℒ::Directional{Dim}, data, eval_points, adjl, basis) w
end
end

"""
_build_weights(ℒ::Directional, data, eval_points, adjl, basis, is_boundary, boundary_conditions, normals)

Hermite-compatible method for building directional derivative weights with boundary condition support.
"""
function _build_weights(
ℒ::Directional{Dim},
data::AbstractVector,
eval_points::AbstractVector,
adjl::AbstractVector,
basis::AbstractRadialBasis,
is_boundary::Vector{Bool},
boundary_conditions::Vector{<:BoundaryCondition},
normals::Vector{<:AbstractVector},
) where {Dim}
v = ℒ.v
if !(length(v) == Dim || length(v) == length(data))
throw(
DomainError(
"The geometrical vector for Directional() should match either the dimension of the input or the number of input points. The geometrical vector length is $(length(v)) while there are $(length(data)) points with a dimension of $Dim",
),
)
end

# Build gradient weights using Hermite method
dim = length(first(data))
mon = MonomialBasis(dim, basis.poly_deg)
gradient_op = Gradient{Dim}()
ℒmon = gradient_op(mon)
ℒrbf = gradient_op(basis)

weights = _build_weights(
data,
eval_points,
adjl,
basis,
ℒrbf,
ℒmon,
mon,
is_boundary,
boundary_conditions,
normals,
)

if length(v) == Dim
return mapreduce(+, zip(weights, v)) do zipped
w, vᵢ = zipped
w * vᵢ
end
else
vv = ntuple(i -> getindex.(v, i), Dim)
return mapreduce(+, zip(weights, vv)) do zipped
w, vᵢ = zipped
Diagonal(vᵢ) * w
end
end
end

# pretty printing
print_op(op::Directional) = "Directional Derivative (∇f⋅v)"
33 changes: 33 additions & 0 deletions src/operators/gradient.jl
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,38 @@ function gradient(
return RadialBasisOperator(ℒ, data, eval_points, basis; k=k, adjl=adjl)
end

"""
function gradient(data, eval_points, basis, is_boundary, boundary_conditions, normals; k=autoselect_k(data, basis))

Builds a Hermite-compatible `RadialBasisOperator` where the operator is the gradient, `Gradient`.
The additional boundary information enables Hermite interpolation with proper boundary condition handling.
"""
function gradient(
data::AbstractVector,
eval_points::AbstractVector,
basis::B,
is_boundary::Vector{Bool},
boundary_conditions::Vector{<:BoundaryCondition},
normals::Vector{<:AbstractVector};
k::T=autoselect_k(data, basis),
adjl=find_neighbors(data, eval_points, k),
) where {B<:AbstractRadialBasis,T<:Int}
Dim = length(first(data))
ℒ = Gradient{Dim}()
return RadialBasisOperator(
ℒ,
data,
eval_points,
basis,
is_boundary,
boundary_conditions,
normals;
k=k,
adjl=adjl,
)
end

# Hermite-compatible method now uses the generic dispatcher in solve_hermite.jl

# pretty printing
print_op(op::Gradient) = "Gradient (∇f)"
31 changes: 31 additions & 0 deletions src/operators/laplacian.jl
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,36 @@ function laplacian(
return RadialBasisOperator(Laplacian(), data, eval_points, basis; k=k, adjl=adjl)
end

"""
function laplacian(data, eval_points, basis, is_boundary, boundary_conditions, normals; k=autoselect_k(data, basis))

Builds a Hermite-compatible `RadialBasisOperator` where the operator is the Laplacian, `Laplacian`.
The additional boundary information enables Hermite interpolation with proper boundary condition handling.
"""
function laplacian(
data::AbstractVector,
eval_points::AbstractVector,
basis::B,
is_boundary::Vector{Bool},
boundary_conditions::Vector{<:BoundaryCondition},
normals::Vector{<:AbstractVector};
k::T=autoselect_k(data, basis),
adjl=find_neighbors(data, eval_points, k),
) where {T<:Int,B<:AbstractRadialBasis}
return RadialBasisOperator(
Laplacian(),
data,
eval_points,
basis,
is_boundary,
boundary_conditions,
normals;
k=k,
adjl=adjl,
)
end

# Hermite-compatible method now uses the generic dispatcher in solve_hermite.jl

# pretty printing
print_op(op::Laplacian) = "Laplacian (∇²f)"
Loading
Loading