Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
5975b4e
first working prototype
Sep 29, 2025
3f1bb5f
extend to 2D
Sep 29, 2025
bf514b0
rename
Sep 29, 2025
3540807
Merge branch 'main' into oriented-bbox
Sep 29, 2025
ef7a0fd
add tests
Sep 29, 2025
ca49730
revise docs
Sep 29, 2025
4c9e929
add tests is_in_oriented_box
Sep 30, 2025
adef4e3
add tests set operations
Sep 30, 2025
196785c
add to docs
Sep 30, 2025
20c9e72
add automatic reordering
Sep 30, 2025
da82198
fix typo
Sep 30, 2025
07671f4
Merge branch 'main' into automatic-ordering-clamped-particles
Sep 30, 2025
51affb8
revise docs
Sep 30, 2025
270dd86
indentation
Sep 30, 2025
b1b813d
Merge branch 'main' into automatic-ordering-clamped-particles
Oct 2, 2025
1ad1a2b
add tests
Oct 2, 2025
b8beca6
Merge branch 'main' into oriented-bbox
Oct 2, 2025
6263795
Merge branch 'main' into automatic-ordering-clamped-particles
Oct 6, 2025
7602d08
revise
Oct 7, 2025
3c140db
Merge branch 'main' into oriented-bbox
Oct 7, 2025
50c9d97
fix doc tests
Oct 7, 2025
b19c703
Merge branch 'main' into oriented-bbox
Oct 8, 2025
1c71c74
Merge branch 'main' into automatic-ordering-clamped-particles
Oct 8, 2025
a6beb81
Merge branch 'main' into oriented-bbox
Oct 9, 2025
09cf86b
Merge branch 'main' into automatic-ordering-clamped-particles
Oct 10, 2025
707c571
add TLSPH support
Oct 13, 2025
aff99ba
Merge branch 'main' into automatic-ordering-clamped-particles
Oct 13, 2025
bc5a3aa
Merge branch 'main' into oriented-bbox
Oct 13, 2025
779c3bf
Merge branch 'oriented-bbox' into fsi-aorta
Oct 13, 2025
07615b5
Merge branch 'fsi-support-ZhangOBC' into fsi-aorta
Oct 13, 2025
051533c
initial prototype - not tested yet
Sep 25, 2025
06c5155
add check
Sep 25, 2025
93bb63d
make full velocity optional
Sep 26, 2025
df62d79
1st try to make it GPU compatible
Sep 28, 2025
c3b455e
add comment
Oct 1, 2025
6b4efaa
make it work on GPU
Oct 1, 2025
0c37246
add comment
Oct 1, 2025
e642dd3
revise show
Oct 1, 2025
44951f9
add initial pressure
Oct 2, 2025
f9104f4
fix flow rate
Oct 2, 2025
093800d
fix stupid bug
Oct 2, 2025
63cc35a
add test
Oct 2, 2025
1f35aed
adapt for other boundary model
Oct 2, 2025
d752bfb
add docs
Oct 2, 2025
7eb545e
add literature
Oct 2, 2025
5845b9b
fix
Oct 2, 2025
bfbf3b6
rm cached
Oct 13, 2025
1412448
rm weird keyword
Oct 13, 2025
f9cc2db
Merge branch 'add-rcr-windkessel-model' into fsi-aorta
Oct 13, 2025
01091ef
fix flow rate calculation
Oct 13, 2025
dbbcad6
Merge branch 'add-rcr-windkessel-model' into fsi-aorta
Oct 13, 2025
f6532cf
fix again flow rate calculation
Oct 13, 2025
aa799a1
Merge branch 'add-rcr-windkessel-model' into fsi-aorta
Oct 13, 2025
9f3ee22
export is_in_oriented_box
Oct 14, 2025
2e5b291
Merge branch 'oriented-bbox' into fsi-aorta
Oct 14, 2025
37310d2
add vector variant
Oct 14, 2025
ca10f5e
Merge branch 'automatic-ordering-clamped-particles' into fsi-aorta
Oct 14, 2025
d2b8fa7
force unique indices
Oct 14, 2025
4f47fe3
Merge branch 'automatic-ordering-clamped-particles' into fsi-aorta
Oct 14, 2025
fc0acf7
Merge branch 'main' into fsi-aorta
Oct 14, 2025
0dba758
allow multiple patches to be loaded
Oct 20, 2025
c7d4e3e
add `extrude_planar_geometry`
Oct 20, 2025
fae492b
add `union` operator
Oct 20, 2025
b15fba7
fix normals
Oct 20, 2025
9e5dfee
add tests multiple patches
Oct 20, 2025
f1ecd22
rename function
Oct 20, 2025
e75eaf9
add more tests
Oct 20, 2025
02c5ab4
Merge branch 'main' into fsi-aorta
Oct 21, 2025
64ab7e7
Merge branch 'extrude-stl-geometry' into fsi-aorta
Oct 21, 2025
2a5b82c
Merge branch 'main' into add-rcr-windkessel-model
Oct 21, 2025
e309ec2
Merge branch 'main' into add-rcr-windkessel-model
Oct 22, 2025
d4b7345
fix tests
Oct 27, 2025
57e112d
Merge branch 'main' into add-rcr-windkessel-model
Oct 27, 2025
b1c75a2
Merge branch 'main' into fsi-aorta
Oct 27, 2025
6f34301
fix
Oct 27, 2025
82949fe
Merge branch 'main' into add-rcr-windkessel-model
Nov 1, 2025
f5e9ca0
Merge branch 'main' into fsi-aorta
Nov 1, 2025
fafcfdf
Merge branch 'main' into add-rcr-windkessel-model
Nov 6, 2025
95fdd79
Merge branch 'add-rcr-windkessel-model' into fsi-aorta
Nov 6, 2025
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
5 changes: 5 additions & 0 deletions docs/src/general/initial_condition.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,8 @@ Pages = [joinpath("general", "initial_condition.jl")]
Modules = [TrixiParticles]
Pages = map(file -> joinpath("setups", file), readdir(joinpath("..", "src", "setups")))
```

```@autodocs
Modules = [TrixiParticles]
Filter = t -> t === TrixiParticles.OrientedBoundingBox
```
23 changes: 23 additions & 0 deletions docs/src/refs.bib
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,15 @@ @Article{Ganzenmueller2015
publisher = {Elsevier {BV}},
}

@Book{Gasser2021,
author = {Gasser, T. Christian},
title = {Vascular Biomechanics: Concepts, Models, and Applications},
year = {2021},
isbn = {9783030709662},
doi = {10.1007/978-3-030-70966-2},
publisher = {Springer International Publishing},
}

@Article{Giles1990,
author = {Giles, Michael B.},
title = {Nonreflecting boundary conditions for {Euler} equation calculations},
Expand Down Expand Up @@ -828,6 +837,20 @@ @Article{Wendland1995
publisher = {Springer Science and Business Media LLC},
}

@Article{Westerhof2008,
author = {Westerhof, Nico and Lankhaar, Jan-Willem and Westerhof, Berend E.},
journal = {Medical \& Biological Engineering \& Computing},
title = {The arterial Windkessel},
year = {2008},
issn = {1741-0444},
month = jun,
number = {2},
pages = {131--141},
volume = {47},
doi = {10.1007/s11517-008-0359-2},
publisher = {Springer Science and Business Media LLC},
}

@Article{Zhang2025,
author = {Zhang, Shuoguo and Fan, Yu and Wu, Dong and Zhang, Chi and Hu, Xiangyu},
journal = {Physics of Fluids},
Expand Down
6 changes: 6 additions & 0 deletions docs/src/systems/boundary.md
Original file line number Diff line number Diff line change
Expand Up @@ -342,3 +342,9 @@ without the need to specifically identify those near the free surface.
To further handle incomplete kernel support, for example in the viscous term of the momentum equation,
the updated velocity of particles within the [`BoundaryZone`](@ref) is projected onto the face normal,
so that only the component in flow direction is kept.

# Pressure Models
```@autodocs
Modules = [TrixiParticles]
Pages = [joinpath("schemes", "boundary", "open_boundary", "pressure_model.jl")]
```
3 changes: 2 additions & 1 deletion src/TrixiParticles.jl
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,14 @@ export BoundaryModelMonaghanKajtar, BoundaryModelDummyParticles, AdamiPressureEx
export FirstOrderMirroring, ZerothOrderMirroring, SimpleMirroring
export HertzContactModel, LinearContactModel
export PrescribedMotion, OscillatingMotion2D
export RCRWindkesselModel
export examples_dir, validation_dir
export trixi2vtk, vtk2trixi
export RectangularTank, RectangularShape, SphereShape, ComplexShape
export ParticlePackingSystem, SignedDistanceField
export WindingNumberHormann, WindingNumberJacobson
export VoxelSphere, RoundSphere, reset_wall!, extrude_geometry, load_geometry,
sample_boundary, planar_geometry_to_face
sample_boundary, planar_geometry_to_face, OrientedBoundingBox, is_in_oriented_box
export SourceTermDamping
export ShepardKernelCorrection, KernelCorrection, AkinciFreeSurfaceCorrection,
GradientCorrection, BlendedGradientCorrection, MixedKernelGradientCorrection
Expand Down
1 change: 1 addition & 0 deletions src/general/semidiscretization.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1149,5 +1149,6 @@ function set_system_links(system::OpenBoundarySystem, semi)
system.buffer,
system.pressure_acceleration_formulation,
system.shifting_technique,
system.pressure_model_values,
system.cache)
end
248 changes: 242 additions & 6 deletions src/preprocessing/geometries/geometries.jl
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ face, face_normal = planar_geometry_to_face(planar_geometry)
function planar_geometry_to_face(planar_geometry::TriangleMesh)
face_normal = normalize(sum(planar_geometry.face_normals) / nfaces(planar_geometry))

face_vertices = oriented_bounding_box(stack(planar_geometry.vertices))
face_vertices, _, _ = oriented_bounding_box(stack(planar_geometry.vertices))

# Vectors spanning the face
edge1 = face_vertices[:, 2] - face_vertices[:, 1]
Expand Down Expand Up @@ -122,11 +122,247 @@ function oriented_bounding_box(point_cloud)

aligned_coords = eigen_vectors' * centered_data

min_corner = minimum(aligned_coords, dims=2)
max_corner = maximum(aligned_coords, dims=2)
min_corner = minimum(aligned_coords, dims=2) .- eps()
max_corner = maximum(aligned_coords, dims=2) .+ eps()

face_vertices = hcat(min_corner, max_corner,
[min_corner[1], max_corner[2], min_corner[3]])
if length(min_corner) == 2
rect_coords = hcat(min_corner, max_corner, [min_corner[1], max_corner[2]])
else
rect_coords = hcat(min_corner, max_corner,
[min_corner[1], max_corner[2], min_corner[3]])
end

rotated_rect_coords = eigen_vectors * rect_coords .+ means

return rotated_rect_coords, eigen_vectors, (min_corner, max_corner)
end

"""
OrientedBoundingBox(; box_origin, orientation_matrix, edge_lengths, local_axis_scale::Tuple))
OrientedBoundingBox(point_cloud::AbstractMatrix; local_axis_scale::Tuple)
OrientedBoundingBox(geometry::Union{Polygon, TriangleMesh}; local_axis_scale::Tuple)

Creates an oriented bounding box (rectangle in 2D or cuboid in 3D) that can be
rotated and positioned arbitrarily in space.

The box is defined either by explicit parameters
or by automatically fitting it around an existing geometry or a point cloud with optional scaling.

# Arguments
- `point_cloud`: An array where the ``i``-th column holds the coordinates of a point ``i``.
- `geometry`: Geometry returned by [`load_geometry`](@ref).

# Keywords
- `box_origin`: The corner point from which the box is constructed.
- `orientation_matrix`: A matrix defining the orientation of the box in space.
Each column of the matrix represents one of the local axes of the box (e.g., x, y, z in 3D).
The matrix must be orthogonal, ensuring that the local axes are perpendicular to each other
and normalized.
- `edge_lengths`: The lengths of the edges of the box:
- In 2D: `(width, height)`
- In 3D: `(width, height, depth)`
- `local_axis_scale`: Allows for anisotropic scaling along the oriented axes of the `OrientedBoundingBox`
(the eigenvectors of the geometry's covariance matrix). Default is no scaling.
The tuple components correspond to:
- first element: scaling along the first eigenvector (local x-axis),
- second element: scaling along the second eigenvector (local y-axis),
- third element (only in 3D): scaling along the third eigenvector (local z-axis).

**Note:** Scaling is always applied in the local `OrientedBoundingBox`
coordinate system, i.e. along its oriented axes.
Scaling along arbitrary world directions is not supported,
as this would break the orthogonality of the spanning vectors.

# Examples
```jldoctest; output=false, setup = :(using LinearAlgebra: I)
# 2D axis aligned
OrientedBoundingBox(box_origin=[0.0, 0.0], orientation_matrix=I(2), edge_lengths=[2.0, 1.0])

# 2D rotated
orientation_matrix = [cos(π/4) -sin(π/4);
sin(π/4) cos(π/4)]
OrientedBoundingBox(; box_origin=[0.0, 0.0], orientation_matrix, edge_lengths=[2.0, 1.0])

# 2D point cloud
# Create a point cloud from a spherical shape (quarter circle sector)
shape = SphereShape(0.1, 1.5, (0.2, 0.4), 1.0, n_layers=4,
sphere_type=RoundSphere(; start_angle=0, end_angle=π/4))
OrientedBoundingBox(shape.coordinates)

# 3D rotated
orientation_matrix = [cos(π/4) -sin(π/4) 0.0; # x-axis after rotation
sin(π/4) cos(π/4) 0.0; # y-axis after rotation
0.0 0.0 1.0] # z-axis remains unchanged
OrientedBoundingBox(; box_origin=[0.5, -0.2, 0.0], orientation_matrix,
edge_lengths=[1.0, 2.0, 3.0])

# output
┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
│ OrientedBoundingBox (3D) │
│ ════════════════════════ │
│ box origin: ………………………………………………… [0.5, -0.2, 0.0] │
│ edge lengths: …………………………………………… (1.0, 2.0, 3.0) │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘
```
"""
struct OrientedBoundingBox{NDIMS, ELTYPE <: Real, SV}
box_origin :: SVector{NDIMS, ELTYPE}
spanning_vectors :: SV
end

function OrientedBoundingBox(; box_origin, orientation_matrix, edge_lengths,
local_axis_scale::Tuple=ntuple(_ -> 0, length(box_origin)))
box_origin_ = SVector(box_origin...)
NDIMS = length(box_origin_)

@assert size(orientation_matrix) == (NDIMS, NDIMS)
@assert length(edge_lengths) == NDIMS
@assert length(local_axis_scale) == NDIMS

spanning_vectors = ntuple(i -> SVector{NDIMS}(orientation_matrix[:, i] *
edge_lengths[i]), NDIMS)

prod(local_axis_scale) > 0 || return OrientedBoundingBox(box_origin_, spanning_vectors)

return scaled_bbox(SVector(local_axis_scale), box_origin_, spanning_vectors)
end

function OrientedBoundingBox(point_cloud::AbstractMatrix;
local_axis_scale::Tuple=ntuple(_ -> 0, size(point_cloud, 1)))
NDIMS = size(point_cloud, 1)
vertices, eigen_vectors, (min_corner, max_corner) = oriented_bounding_box(point_cloud)

# Use the first vertex as box origin (bottom-left-front corner)
box_origin = SVector{NDIMS}(vertices[:, 1])

# Calculate edge lengths from min/max corners
edge_lengths = max_corner - min_corner

return OrientedBoundingBox(; box_origin, orientation_matrix=eigen_vectors, edge_lengths,
local_axis_scale)
end

function OrientedBoundingBox(geometry::Union{Polygon, TriangleMesh};
local_axis_scale::Tuple=ntuple(_ -> 0, ndims(geometry)))
point_cloud = stack(geometry.vertices)

return OrientedBoundingBox(point_cloud; local_axis_scale)
end

@inline Base.ndims(::OrientedBoundingBox{NDIMS}) where {NDIMS} = NDIMS

function Base.show(io::IO, ::MIME"text/plain", box::OrientedBoundingBox)
@nospecialize box # reduce precompilation time

if get(io, :compact, false)
show(io, box)
else
summary_header(io, "OrientedBoundingBox ($(ndims(box))D)")
summary_line(io, "box origin", box.box_origin)
summary_line(io, "edge lengths", norm.(box.spanning_vectors))
summary_footer(io)
end
end

function scaled_bbox(local_axis_scale::SVector{2}, box_origin, spanning_vectors)
# Uniform scaling about the center, center remains unchanged
v1, v2 = spanning_vectors
center = box_origin + (v1 + v2) / 2

# Scaling factor per oriented axis
s1, s2 = local_axis_scale

# New spanning vectors
v1p, v2p = s1 * v1, s2 * v2

new_origin = center - (v1p + v2p) / 2

return OrientedBoundingBox(new_origin, (v1p, v2p))
end

function scaled_bbox(local_axis_scale::SVector{3}, box_origin, spanning_vectors)
# Uniform scaling about the center, center remains unchanged
v1, v2, v3 = spanning_vectors
center = box_origin + (v1 + v2 + v3) / 2

# Scaling factor per oriented axis
s1, s2, s3 = local_axis_scale

# New spanning vectors
v1p, v2p, v3p = s1 * v1, s2 * v2, s3 * v3

new_origin = center - (v1p + v2p + v3p) / 2

return OrientedBoundingBox(new_origin, (v1p, v2p, v3p))
end

function is_in_oriented_box(coordinates::AbstractArray, box)
is_in_box = fill(false, size(coordinates, 2))
@threaded default_backend(coordinates) for particle in axes(coordinates, 2)
particle_coords = current_coords(coordinates, box, particle)
is_in_box[particle] = is_in_oriented_box(particle_coords, box)
end

return findall(is_in_box)
end

@inline function is_in_oriented_box(particle_coords::SVector{NDIMS}, box) where {NDIMS}
(; spanning_vectors, box_origin) = box
relative_position = particle_coords - box_origin

for dim in 1:NDIMS
span_dim = spanning_vectors[dim]
# Checks whether the projection of the particle position
# falls within the range of the zone
if !(0 <= dot(relative_position, span_dim) <= dot(span_dim, span_dim))

# Particle is not in box
return false
end
end

# Particle is in box
return true
end

@inline function Base.intersect(initial_condition::InitialCondition,
boxes::OrientedBoundingBox...)
(; coordinates, density, mass, velocity, pressure, particle_spacing) = initial_condition
box = first(boxes)

if ndims(box) != ndims(initial_condition)
throw(ArgumentError("all passed `OrientedBoundingBox`s must have the same dimensionality as the initial condition"))
end

keep_indices = is_in_oriented_box(coordinates, box)

result = InitialCondition(; coordinates=coordinates[:, keep_indices],
density=density[keep_indices], mass=mass[keep_indices],
velocity=velocity[:, keep_indices],
pressure=pressure[keep_indices],
particle_spacing)

return intersect(result, Base.tail(boxes)...)
end

@inline function Base.setdiff(initial_condition::InitialCondition,
boxes::OrientedBoundingBox...)
(; coordinates, density, mass, velocity, pressure, particle_spacing) = initial_condition
box = first(boxes)

if ndims(box) != ndims(initial_condition)
throw(ArgumentError("all passed `OrientedBoundingBox`s must have the same dimensionality as the initial condition"))
end

delete_indices = is_in_oriented_box(coordinates, box)
keep_indices = setdiff(eachparticle(initial_condition), delete_indices)

result = InitialCondition(; coordinates=coordinates[:, keep_indices],
density=density[keep_indices],
mass=mass[keep_indices],
velocity=velocity[:, keep_indices],
pressure=pressure[keep_indices],
particle_spacing)

return eigen_vectors * face_vertices .+ means
return setdiff(result, Base.tail(boxes)...)
end
Loading