From adef3efd455fa9203b762558e741955228fa2c3d Mon Sep 17 00:00:00 2001 From: nhz2 Date: Wed, 5 Aug 2026 23:52:11 +0200 Subject: [PATCH 01/14] initial point cell list --- .gitignore | 2 + Project.toml | 7 +- src/SimplexCellLists.jl | 223 +----------- src/distances.jl | 536 ++++++++++++++-------------- src/naive.jl | 155 --------- src/painter.jl | 694 ------------------------------------- src/pointcelllist.jl | 177 ++++++++++ src/util.jl | 31 ++ test/runtests.jl | 10 +- test/test-mindistance.jl | 77 ++++ test/test-pointcelllist.jl | 312 +++++++++++++++++ test/test-util.jl | 151 ++++++++ test/test_mindistance.jl | 180 ---------- test/test_naive.jl | 156 --------- test/test_painter-basic.jl | 159 --------- test/test_painter.jl | 553 ----------------------------- 16 files changed, 1050 insertions(+), 2373 deletions(-) delete mode 100644 src/naive.jl delete mode 100644 src/painter.jl create mode 100644 src/pointcelllist.jl create mode 100644 src/util.jl create mode 100644 test/test-mindistance.jl create mode 100644 test/test-pointcelllist.jl create mode 100644 test/test-util.jl delete mode 100644 test/test_mindistance.jl delete mode 100644 test/test_naive.jl delete mode 100644 test/test_painter-basic.jl delete mode 100644 test/test_painter.jl diff --git a/.gitignore b/.gitignore index 475497b..825cc39 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ #VS Code /.vscode/ +/agent-depot/ +/agent-logs/ *.jl.*.cov *.jl.cov diff --git a/Project.toml b/Project.toml index cacc7e9..aa244c1 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "SimplexCellLists" uuid = "fbe69cd6-f244-4ce4-a90b-8979d86c9ea4" authors = ["Nathan Zimmerberg"] -version = "0.1.0" +version = "0.2.0-dev" [deps] ArgCheck = "dce04be8-c92d-5529-be00-80e4d2c0e197" @@ -11,4 +11,7 @@ StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" [compat] ArgCheck = "2" StaticArrays = "1" -julia = "1.8" +julia = "1.12" + +[workspace] +projects = ["test"] diff --git a/src/SimplexCellLists.jl b/src/SimplexCellLists.jl index 7e15adf..3c0604b 100644 --- a/src/SimplexCellLists.jl +++ b/src/SimplexCellLists.jl @@ -1,219 +1,28 @@ module SimplexCellLists -include("distances.jl") - +using LinearAlgebra using StaticArrays using ArgCheck -const PUBLIC = "public\n\n" -const Simplex{N} = SVector{N, SVector{3, Float32}} +const Vec3 = SVector{3} +const Simplex{N,T} = SVector{N, Vec3{T}} const Point = Simplex{1} const Line = Simplex{2} const Triangle = Simplex{3} +include("util.jl") +export ball_intersects_unit_cube +export ball_intersects_biunit_cube -distSqr(x::Point, y::Point) = dist2PointPoint( x, y) -distSqr(x::Point, y::Line) = dist2PointLine( x, y) -distSqr(x::Point, y::Triangle) = dist2PointTriangle( x, y) -distSqr(x::Line, y::Point) = dist2PointLine( y, x) -distSqr(x::Line, y::Line) = dist2LineLine( x, y) -distSqr(x::Line, y::Triangle) = dist2LineTriangle( x, y) -distSqr(x::Triangle, y::Point) = dist2PointTriangle( y, x) -distSqr(x::Triangle, y::Line) = dist2LineTriangle( y, x) -distSqr(x::Triangle, y::Triangle) = dist2TriangleTriangle(x, y) - - -distSqr_sMin(x::Point, y::Point) = dist2PointPoint(x, y), SA_F32[] -distSqr_sMin(x::Point, y::Line) = dist2PointLine( x, y), SA_F32[] -distSqr_sMin(x::Point, y::Triangle) = dist2PointTriangle( x, y), SA_F32[] -distSqr_sMin(x::Line, y::Point) = ((i -> (i[1], SA_F32[i[2]])) ∘ dist2PointLine_t)( y, x) -distSqr_sMin(x::Line, y::Line) = ((i -> (i[1], SA_F32[i[2]])) ∘ dist2LineLine_s_t)( x, y) -distSqr_sMin(x::Line, y::Triangle) = ((i -> (i[1], i[2])) ∘ dist2LineTriangle_s_t)( x, y) -distSqr_sMin(x::Triangle, y::Point) = dist2PointTriangle_t( y, x) -distSqr_sMin(x::Triangle, y::Line) = ((i -> (i[1], i[3])) ∘ dist2LineTriangle_s_t)( y, x) -distSqr_sMin(x::Triangle, y::Triangle) = ((i -> (i[1], i[2])) ∘ dist2TriangleTriangle_s_t)( x, y) - -""" -Abstract type for all SimplexCellList implementations. - -Implementations must have a constructor with signature like - -```julia -T(numpointgroups::Integer, numlinegroups::Integer, numtrianglegroups::Integer; kwargs...)::T -``` - -Where `numpointgroups` is the number of groups of points, `numlinegroups` is the number of groups of lines, -and `numtrianglegroups` is the number of groups of triangles. - -`kwargs` are options specific for `T` -""" -abstract type SimplexCellList end -export SimplexCellList - -include("naive.jl") -include("painter.jl") - - -""" -Reset the elements stored in `s` in batch: - -```julia -setElements!(s::SimplexCellList, points, lines, triangles)::Nothing -``` - -Where `points`, `lines` and `triangles` are collections of collections of objects convertible to -`Point`, `Line`, and `Triangle` respectively. - -For example, each collection in `points` is a group of points that can be mapped over independently from, or together with, other groups. - -Added elements will have a group index and element index based on the order of the inputs. -The first group in each type has group index 1, and the first element in each group has element index 1. -""" -function setElements! end -export setElements! - - -""" -Add a new element to `s`, and return its element index: - -```julia -addElement!(s::SimplexCellList, group_idx::Integer, element::Simplex{N})::Int32 -``` -The new element will be pushed to the end of the specified group. -""" -function addElement! end -export addElement! - - -""" -Deactivate an existing element in `s` - -```julia -deactivate!(s::SimplexCellList, group_idx::Integer, element_idx::Integer, element_type::Type{Simplex{N}})::Nothing -``` -Inactive elements are not mapped over. Elements are active by default. -""" -function deactivate! end -export deactivate! - - -""" -Re-activate an existing element in `s` - -```julia -activate!(s::SimplexCellList, group_idx::Integer, element_idx::Integer, element_type::Type{Simplex{N}})::Nothing -``` -Inactive elements are not mapped over. Elements are active by default. -""" -function activate! end -export activate! - - -""" -Return if an existing element in `s` is active. - -```julia -isActive(s::SimplexCellList, group_idx::Integer, element_idx::Integer, element_type::Type{Simplex{N}})::Bool -``` -Inactive elements are not mapped over. Elements are active by default. -""" -function isActive end -export isActive - - -const mapped_function_docs = """ -### Mapped function `f` - -The function f should have the same form as used in CellListMap.jl. - -`i` is the element index of simplex `x`, `j` is the element index of simplex `y`. - -`d2` is an approximate `Float32` squared distance between `x` and `y`. - -Except here `x` and `y` are `Simplex{N}`, `Simplex{M}` - -```julia - function f(x,y,i,j,d2,output) - # update output - return output - end -``` -""" - - -""" -Map `f` to all simplexes in a group close to a single simplex. - -```julia -mapSimplexElements( - f, - output, - s::SimplexCellList, - x::Simplex{N}, - group_idx::Integer, - elements_type::Type{Simplex{M}}, - cutoff::Float32, - ) where {N, M} -``` - -Apply function `f` to all elements in group `group_idx` within the cutoff range of the simplex `x`, and -return the output of the final `f` call. - -`x` is always `x` and `i` is always 0, in calls to `f`. - -$mapped_function_docs -""" -function mapSimplexElements end -export mapSimplexElements - - -""" -Map `f` to all pairs of nearby simplexes in a single group. - -```julia -mapPairElements( - f, - output, - s::SimplexCellList, - group_idx::Integer, - elements_type::Type{Simplex{N}}, - cutoff::Float32, - ) where {N} -``` -Apply function `f` to all unordered pairs of elements in group `group_idx` within cutoff range, and return the output of the final `f` call. - -`f` is never called more than once per unordered pair. Which element is `x` and `y` in calls to `f` is implementation dependent. - -$mapped_function_docs -""" -function mapPairElements end -export mapPairElements - - -""" -Map `f` to all pairs of nearby simplexes between two different groups. - -```julia -mapElementsElements( - f, - output, - s::SimplexCellList, - x_group_idx::Integer, - x_type::Type{Simplex{N}}, - y_group_idx::Integer, - y_type::Type{Simplex{M}}, - cutoff::Float32, - ) where {N, M} -``` -Apply function `f` to each pair of elements from two different groups that are within cutoff range of each other, and return the output of the final `f` call. - -The first element has is an `x_type` in group `x_group_idx` and the second element is a `y_type` in group `y_group_idx`. - -`f` is never called more than once per pair. - -$mapped_function_docs -""" -function mapElementsElements end -export mapElementsElements +include("distances.jl") +export dist_sqr +export dist_sqr_r_s_t + +include("pointcelllist.jl") +export PointCellList +export CellPointEntry +export cell_point_add! +export map_nearby_points +export cell_point_clear! end \ No newline at end of file diff --git a/src/distances.jl b/src/distances.jl index 79cf243..7b2cf53 100644 --- a/src/distances.jl +++ b/src/distances.jl @@ -1,142 +1,126 @@ -using LinearAlgebra -using StaticArrays +""" + dist_sqr_r_s_t(x::Simplex{N,T}, y::Simplex{M,T})::Tuple{T, Vec3{T}, SVector{N-1,T}, SVector{M-1,T}} -clamp01nan(x) = ifelse(isnan(x), zero(x), clamp(x,zero(x),one(x))) +Return a tuple of: +1. the minimum squared distance between the two simplexes. +2. the minimum distance vector from x to y. +3. the barycentric coordinates on x. +4. the barycentric coordinates on y. -dist2PointPoint(a,b) = (a[1] - b[1]) ⋅ (a[1] - b[1]) +TODO: better docs for what s and t are. +""" +function dist_sqr_r_s_t end +""" + dist_sqr(x::Simplex{N,T}, y::Simplex{M,T})::T -"Return the minimum distance squared and where on the line it occurs, from 0 to 1" -function dist2PointLine_t(x,y) - r= y[2]-y[1] - p= x[1]-y[1] - # c= a[1]-b[2] - t = clamp01nan(p⋅r/(r⋅r)) - d = t*r - p - d⋅d, t +Return the minimum squared distance between two simplexes. +""" +function dist_sqr(x::Simplex{N, T}, y::Simplex{M, T}) where {T, N, M} + d2, r, s, t = @inline dist_sqr_r_s_t(y, x) + d2 end -function dist2PointLine(x,y) - @inline dist2PointLine_t(x,y)[1] +# Dispatch N > M to N ≤ M +function dist_sqr_r_s_t(x::Simplex{3, T}, y::Simplex{2, T}) where T + d2, r, s, t = @inline dist_sqr_r_s_t(y, x) + d2, -r, t, s +end +function dist_sqr_r_s_t(x::Simplex{3, T}, y::Simplex{1, T}) where T + d2, r, s, t = @inline dist_sqr_r_s_t(y, x) + d2, -r, t, s +end +function dist_sqr_r_s_t(x::Simplex{2, T}, y::Simplex{1, T}) where T + d2, r, s, t = @inline dist_sqr_r_s_t(y, x) + d2, -r, t, s +end +function dist_sqr(x::Simplex{3, T}, y::Simplex{2, T}) where T + @inline dist_sqr(y, x) +end +function dist_sqr(x::Simplex{3, T}, y::Simplex{1, T}) where T + @inline dist_sqr(y, x) +end +function dist_sqr(x::Simplex{2, T}, y::Simplex{1, T}) where T + @inline dist_sqr(y, x) end -""" - dist2LineLine(x,y) +# The methods -Return the squared distance between two line segments. -Using the simple algorithm and some comments from -https://www.geometrictools.com/Documentation/DistanceLine3Line3.pdf -// David Eberly, Geometric Tools, Redmond WA 98052 -// Copyright (c) 1998-2022 -// Distributed under the Boost Software License, Version 1.0. -// https://www.boost.org/LICENSE_1_0.txt -// https://www.geometrictools.com/License/Boost/LICENSE_1_0.txt -// Version: 6.0.2022.01.06 -Ignores the case of degenerate line segments. -""" -function dist2LineLine(x,y) - P0 = x[1] - P1 = x[2] - Q0 = y[1] - Q1 = y[2] - P = P1-P0 - Q = Q1-Q0 - P0mQ0 = P0 - Q0 - a = P ⋅ P - b = P ⋅ Q - c = Q ⋅ Q - d = P ⋅ P0mQ0 - e = Q ⋅ P0mQ0 - f = P0mQ0 ⋅ P0mQ0 - Δ = a*c - b^2 - invΔ = inv(Δ) - #assuming both segments are not zero length - #critical points - s0 = clamp01nan(-d/a) - s1 = clamp01nan((b-d)/a) - t0 = clamp01nan(e/c) - t1 = clamp01nan((b+e)/c) - sbar = clamp01nan((b*e - c*d)*invΔ) - tbar = clamp01nan((a*e - b*d)*invΔ) - r(s,t) = a*s^2 - 2b*s*t + c*t^2 + 2d*s - 2e*t - return Base.FastMath.max_fast(zero(s0),f+Base.FastMath.min_fast( - r(s0,0), - r(s1,1), - r(0,t0), - r(1,t1), - r(sbar,tbar), - )) +function dist_sqr_r_s_t(x::Point{T}, y::Point{T}) where T + d = y[1] - x[1] + d ⋅ d, d, SVector{0,T}(), SVector{0,T}() end -""" - dist2LineLine_s_t(x,y) +function dist_sqr_r_s_t(x::Point{T}, y::Line{T}) where T + r = y[2] - y[1] + p = x[1] - y[1] + t = clamp01nan((p ⋅ r)/(r ⋅ r)) + d = t*r - p + d ⋅ d, d, SVector{0,T}(), SVector{1,T}(t) +end -Return the squared distance between two line segments. -And where on the line segments it occurs, from 0 to 1. -Using the simple algorithm and some comments from -https://www.geometrictools.com/Documentation/DistanceLine3Line3.pdf +#= +Based on https://www.geometrictools.com/Documentation/DistancePoint3Triangle3.pdf +But not fully optimized. // David Eberly, Geometric Tools, Redmond WA 98052 // Copyright (c) 1998-2022 // Distributed under the Boost Software License, Version 1.0. // https://www.boost.org/LICENSE_1_0.txt // https://www.geometrictools.com/License/Boost/LICENSE_1_0.txt // Version: 6.0.2022.01.06 -Ignores the case of degenerate line segments. -""" -function dist2LineLine_s_t(x,y) - P0 = x[1] - P1 = x[2] - Q0 = y[1] - Q1 = y[2] - P = P1-P0 - Q = Q1-Q0 - P0mQ0 = P0 - Q0 - a = P ⋅ P - b = P ⋅ Q - c = Q ⋅ Q - d = P ⋅ P0mQ0 - e = Q ⋅ P0mQ0 - f = P0mQ0 ⋅ P0mQ0 +=# +function dist_sqr_r_s_t(x::Point{T}, y::Triangle{T}) where T + P = x[1] + B = y[1] + E0 = y[2] - B + E1 = y[3] - B + BP = B - P + a = E0 ⋅ E0 + b = E0 ⋅ E1 + c = E1 ⋅ E1 + d = E0 ⋅ BP + e = E1 ⋅ BP + f = BP ⋅ BP Δ = a*c - b^2 - #assuming both segments are not zero length - #critical points + invΔ = inv(Δ) + sbar = clamp01nan((b*e - c*d)*invΔ) + tbar = clamp01nan((-a*e + b*d)*invΔ) + + # this ensures sbar and tbar are in the domain + # if they are out of the domain, the min distance is on a boundary + outside = sbar+tbar > one(sbar) + sbar = ifelse(outside,zero(sbar),sbar) + tbar = ifelse(outside,zero(tbar),tbar) s0 = clamp01nan(-d/a) - s1 = clamp01nan((b-d)/a) - t0 = clamp01nan(e/c) - t1 = clamp01nan((b+e)/c) - sbar = clamp01nan((b*e - c*d)/Δ) - tbar = clamp01nan((a*e - b*d)/Δ) - r(s,t) = a*s^2 - 2b*s*t + c*t^2 + 2d*s - 2e*t + t0 = clamp01nan(-e/c) + sd = clamp01nan(-(b-c+d-e)/(a-2b+c)) + td = one(sd) - sd + r(s,t) = a*s^2 + 2b*s*t + c*t^2 + 2d*s + 2e*t + mins = sbar mint = tbar minr = r(sbar,tbar) - if r(s0,0) < minr - minr = r(s0,0) + if r(s0,false) < minr + minr = r(s0,false) mins = s0 mint = zero(a) end - if r(s1,1) < minr - minr = r(s1,1) - mins = s1 - mint = one(a) - end - if r(0,t0) < minr - minr = r(0,t0) + if r(false,t0) < minr + minr = r(false,t0) mins = zero(a) mint = t0 end - if r(1,t1) < minr - minr = r(1,t1) - mins = one(a) - mint = t1 + if r(sd,td) < minr + minr = r(sd,td) + mins = sd + mint = td end - d2cl = max(0,f+minr) - return d2cl, mins, mint + d2cl = Base.FastMath.max_fast(zero(a),f+minr) + rcl = mins*E0 + mint*E1 + BP + d2cl, rcl, SVector{0,T}(), SVector{2,T}(mins, mint) end -""" -Return the distance between a point and triangle in 3D. - +#= Based on https://www.geometrictools.com/Documentation/DistancePoint3Triangle3.pdf But not fully optimized. // David Eberly, Geometric Tools, Redmond WA 98052 @@ -145,12 +129,12 @@ But not fully optimized. // https://www.boost.org/LICENSE_1_0.txt // https://www.geometrictools.com/License/Boost/LICENSE_1_0.txt // Version: 6.0.2022.01.06 -""" -function dist2PointTriangle(x,y) +=# +function dist_sqr(x::Point{T}, y::Triangle{T}) where T P = x[1] B = y[1] - E0= y[3] - B - E1= y[2] - B + E0 = y[3] - B + E1 = y[2] - B BP = B - P a = E0 ⋅ E0 b = E0 ⋅ E1 @@ -163,8 +147,8 @@ function dist2PointTriangle(x,y) sbar = clamp01nan((b*e - c*d)*invΔ) tbar = clamp01nan((-a*e + b*d)*invΔ) - # # this ensures sbar and tbar are in the domain - # # if they are out of the domain, the min distance is on a boundary + # this ensures sbar and tbar are in the domain + # if they are out of the domain, the min distance is on a boundary outside = sbar+tbar > one(sbar) sbar = ifelse(outside,zero(sbar),sbar) tbar = ifelse(outside,zero(tbar),tbar) @@ -174,91 +158,130 @@ function dist2PointTriangle(x,y) td = one(sd) - sd q(s,t) = a*s^2 + 2b*s*t + c*t^2 + 2d*s + 2e*t - return Base.FastMath.max_fast(zero(sd),f + Base.FastMath.min_fast( - q(s0,0), - q(0,t0), + Base.FastMath.max_fast(zero(sd),f + Base.FastMath.min_fast( + q(s0,false), + q(false,t0), q(sd,td), q(sbar,tbar), )) end -""" -Return the squared distance between a point and a triangle. -And where on the triangle it occurs. - -(0,0) is at y[1] -(1,0) is at y[2] -(0,1) is at y[3] - -Based on https://www.geometrictools.com/Documentation/DistancePoint3Triangle3.pdf -But not fully optimized. +#= +Using the simple algorithm and some comments from +https://www.geometrictools.com/Documentation/DistanceLine3Line3.pdf // David Eberly, Geometric Tools, Redmond WA 98052 // Copyright (c) 1998-2022 // Distributed under the Boost Software License, Version 1.0. // https://www.boost.org/LICENSE_1_0.txt // https://www.geometrictools.com/License/Boost/LICENSE_1_0.txt // Version: 6.0.2022.01.06 - -""" -function dist2PointTriangle_t(x,y) - P = x[1] - B = y[1] - E0= y[2] - B - E1= y[3] - B - BP = B - P - a = E0 ⋅ E0 - b = E0 ⋅ E1 - c = E1 ⋅ E1 - d = E0 ⋅ BP - e = E1 ⋅ BP - f = BP ⋅ BP +Ignores the case of degenerate line segments. +=# +function dist_sqr_r_s_t(x::Line{T}, y::Line{T}) where T + P0 = x[1] + P1 = x[2] + Q0 = y[1] + Q1 = y[2] + P = P1-P0 + Q = Q1-Q0 + P0mQ0 = P0 - Q0 + a = P ⋅ P + b = P ⋅ Q + c = Q ⋅ Q + d = P ⋅ P0mQ0 + e = Q ⋅ P0mQ0 + f = P0mQ0 ⋅ P0mQ0 Δ = a*c - b^2 - invΔ = inv(Δ) - sbar = clamp01nan((b*e - c*d)*invΔ) - tbar = clamp01nan((-a*e + b*d)*invΔ) - - # # this ensures sbar and tbar are in the domain - # # if they are out of the domain, the min distance is on a boundary - outside = sbar+tbar > one(sbar) - sbar = ifelse(outside,zero(sbar),sbar) - tbar = ifelse(outside,zero(tbar),tbar) + #assuming both segments are not zero length + #critical points s0 = clamp01nan(-d/a) - t0 = clamp01nan(-e/c) - sd = clamp01nan(-(b-c+d-e)/(a-2b+c)) - td = one(sd) - sd - r(s,t) = a*s^2 + 2b*s*t + c*t^2 + 2d*s + 2e*t - + s1 = clamp01nan((b-d)/a) + t0 = clamp01nan(e/c) + t1 = clamp01nan((b+e)/c) + sbar = clamp01nan((b*e - c*d)/Δ) + tbar = clamp01nan((a*e - b*d)/Δ) + r(s,t) = a*s^2 - 2b*s*t + c*t^2 + 2d*s - 2e*t mins = sbar mint = tbar minr = r(sbar,tbar) - if r(s0,0) < minr - minr = r(s0,0) + if r(s0,false) < minr + minr = r(s0,false) mins = s0 mint = zero(a) end - if r(0,t0) < minr - minr = r(0,t0) + if r(s1,true) < minr + minr = r(s1,true) + mins = s1 + mint = one(a) + end + if r(false,t0) < minr + minr = r(false,t0) mins = zero(a) mint = t0 end - if r(sd,td) < minr - minr = r(sd,td) - mins = sd - mint = td + if r(true,t1) < minr + minr = r(true,t1) + mins = one(a) + mint = t1 end - d2cl = max(zero(a),f+minr) - return d2cl, SA[mins, mint] + d2cl = Base.FastMath.max_fast(zero(a),f+minr) + rcl = (mint*Q - mins*P) - P0mQ0 + d2cl, rcl, SVector{1,T}(mins), SVector{1,T}(mint) end +#= +Using the simple algorithm and some comments from +https://www.geometrictools.com/Documentation/DistanceLine3Line3.pdf +// David Eberly, Geometric Tools, Redmond WA 98052 +// Copyright (c) 1998-2022 +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt +// https://www.geometrictools.com/License/Boost/LICENSE_1_0.txt +// Version: 6.0.2022.01.06 +Ignores the case of degenerate line segments. +=# +function dist_sqr(x::Line{T}, y::Line{T}) where T + P0 = x[1] + P1 = x[2] + Q0 = y[1] + Q1 = y[2] + P = P1-P0 + Q = Q1-Q0 + P0mQ0 = P0 - Q0 + a = P ⋅ P + b = P ⋅ Q + c = Q ⋅ Q + d = P ⋅ P0mQ0 + e = Q ⋅ P0mQ0 + f = P0mQ0 ⋅ P0mQ0 + Δ = a*c - b^2 + invΔ = inv(Δ) + #assuming both segments are not zero length + #critical points + s0 = clamp01nan(-d/a) + s1 = clamp01nan((b-d)/a) + t0 = clamp01nan(e/c) + t1 = clamp01nan((b+e)/c) + sbar = clamp01nan((b*e - c*d)*invΔ) + tbar = clamp01nan((a*e - b*d)*invΔ) + r(s,t) = a*s^2 - 2b*s*t + c*t^2 + 2d*s - 2e*t + Base.FastMath.max_fast(zero(s0),f+Base.FastMath.min_fast( + r(s0,false), + r(s1,true), + r(false,t0), + r(true,t1), + r(sbar,tbar), + )) +end -""" +#= Given point O, ray vector D, and a triangle with points (A,B,C), test whether the ray intersects the triangle. If there is an intersection P. Then P can be represented as P = A + u(B-A) + v(C-A) = O + tD If there is not an intersection, `t`, `u` and `v` are undefined. -""" +=# struct SegTriangleIntersectResult{Float} intersect::Bool t::Float @@ -266,10 +289,10 @@ struct SegTriangleIntersectResult{Float} v::Float end -""" +#= A fast ray-triangle intersection algorithm by Moller and Trumbore. Ref: Tomas Möller and Ben Trumbore, "Fast, minimum storage ray-triangle intersection" (1997) Journal of Graphics Tools. -""" +=# function moller_trumbore_intersect(o, d, a, b, c) fzero = zero(eltype(o)) seg_triangle_no_intersect = SegTriangleIntersectResult(false, fzero, fzero, fzero) @@ -304,42 +327,8 @@ function moller_trumbore_intersect(o, d, a, b, c) SegTriangleIntersectResult(true, t, u, v) end - -function dist2LineTriangle(x,y) - fzero = zero(eltype(eltype(x))) - o = x[1] - d = x[2] - x[1] - a = y[1] - b = y[2] - c = y[3] - result = moller_trumbore_intersect(o, d, a, b, c) - if result.intersect == true - return fzero - else - # no intersection or seg and triangle are parallel. - # min distance is on an edge of the triangle. - @inline dab = dist2LineLine(x,SA[a,b]) - @inline dbc = dist2LineLine(x,SA[b,c]) - @inline dca = dist2LineLine(x,SA[c,a]) - @inline dx1y = dist2PointTriangle(SA[x[1],],y) - @inline dx2y = dist2PointTriangle(SA[x[2],],y) - return Base.FastMath.min_fast( - dab, - dbc, - dca, - dx1y, - dx2y, - ) - end -end - - -""" -Return the squared distance between a line segment and a triangle. -And where on the line segment and triangle it occurs. -""" -function dist2LineTriangle_s_t(x,y) - fzero = zero(eltype(eltype(x))) +function dist_sqr_r_s_t(x::Line{T}, y::Triangle{T}) where T + fzero = zero(T) o = x[1] d = x[2] - x[1] a = y[1] @@ -347,105 +336,126 @@ function dist2LineTriangle_s_t(x,y) c = y[3] result = moller_trumbore_intersect(o, d, a, b, c) if result.intersect == true - return fzero, SA[result.t,], SA[result.u, result.v] + fzero, zero(Vec3{T}), SA[result.t,], SA[result.u, result.v] else # no intersection or seg and triangle are parallel. # min distance is on an edge of the triangle. - d2min = typemax(fzero) - mins = SA[fzero,] - mint = SA[fzero, fzero] - d2, _smin, _tmin = dist2LineLine_s_t(x,SA[a,b]) - if d2 < d2min - d2min = d2 - mins = SA[_smin,] - mint = SA[_tmin, fzero] - end - d2, _smin, _tmin = dist2LineLine_s_t(x,SA[b,c]) + d2min, minr, mins, _tmin = dist_sqr_r_s_t(x,SA[a,b]) + mint = SA[_tmin[1], fzero] + d2, _rmin, _smin, _tmin = dist_sqr_r_s_t(x,SA[b,c]) if d2 < d2min d2min = d2 - mins = SA[_smin,] - mint = SA[one(fzero) - _tmin, _tmin] + minr = _rmin + mins = _smin + mint = SA[one(T) - _tmin[1], _tmin[1]] end - d2, _smin, _tmin = dist2LineLine_s_t(x,SA[c,a]) + d2, _rmin, _smin, _tmin = dist_sqr_r_s_t(x,SA[c,a]) if d2 < d2min d2min = d2 - mins = SA[_smin,] - mint = SA[fzero, one(fzero) - _tmin] + minr = _rmin + mins = _smin + mint = SA[fzero, one(T) - _tmin[1]] end - d2, _tri_tmin = dist2PointTriangle_t(SA[x[1],],y) + d2, _rmin, _, _tri_tmin = dist_sqr_r_s_t(SA[x[1],],y) if d2 < d2min d2min = d2 - mins = SA[fzero,] + minr = _rmin + mins = SA[fzero] mint = _tri_tmin end - d2, _tri_tmin = dist2PointTriangle_t(SA[x[2],],y) + d2, _rmin, _, _tri_tmin = dist_sqr_r_s_t(SA[x[2],],y) if d2 < d2min d2min = d2 - mins = SA[one(fzero),] + minr = _rmin + mins = SA[one(T)] mint = _tri_tmin end - return d2min, mins, mint + d2min, minr, mins, mint end end - -function dist2TriangleTriangle(x,y) - T = eltype(eltype(x)) +function dist_sqr(x::Line{T}, y::Triangle{T})::T where T fzero = zero(T) - d2 = typemax(T) - ai = 1 - bi = 2 - ci = 3 - for i in 1:3 - @inline this_d2 = dist2LineTriangle(SA[x[ai],x[bi]],y) - d2 = Base.FastMath.min_fast(d2, this_d2) - d2 == fzero && return d2 - ai, bi, ci = bi, ci, ai - end - for i in 1:3 - @inline this_d2 = dist2LineTriangle(SA[y[ai],y[bi]],x) - d2 = Base.FastMath.min_fast(d2, this_d2) - d2 == fzero && return d2 - ai, bi, ci = bi, ci, ai + o = x[1] + d = x[2] - x[1] + a = y[1] + b = y[2] + c = y[3] + result = moller_trumbore_intersect(o, d, a, b, c) + if result.intersect == true + fzero + else + # no intersection or seg and triangle are parallel. + # min distance is on an edge of the triangle. + dab = dist_sqr(x,SA[a,b]) + dbc = dist_sqr(x,SA[b,c]) + dca = dist_sqr(x,SA[c,a]) + dx1y = dist_sqr(SA[x[1],],y) + dx2y = dist_sqr(SA[x[2],],y) + Base.FastMath.min_fast( + dab, + dbc, + dca, + dx1y, + dx2y, + ) end - return d2 end - -function dist2TriangleTriangle_s_t(x,y) - T = eltype(eltype(x)) +function dist_sqr_r_s_t(x::Triangle{T}, y::Triangle{T}) where T fzero = zero(T) fone = one(T) - d2 = typemax(T) - ai = 1 - bi = 2 - ci = 3 - mins = SA[fzero, fzero] - mint = SA[fzero, fzero] - for i in 1:3 - @inline this_d2, _s, _t = dist2LineTriangle_s_t(SA[x[ai],x[bi]],y) + d2, minr, _s, mint = dist_sqr_r_s_t(SA[x[1],x[2]], y) + _s_all = SA[fzero, _s[1], fone - _s[1]] + mins = SA[_s_all[2], _s_all[1]] + iszero(d2) && return d2, minr, mins, mint + ai = 2 + bi = 3 + ci = 1 + for i in 2:3 + this_d2, _r, _s, _t = dist_sqr_r_s_t(SA[x[ai],x[bi]], y) if this_d2 < d2 d2 = this_d2 + minr = _r _s_all = SA[fzero, _s[1], fone - _s[1]] - mins = SA[_s_all[bi],_s_all[ai]] + mins = SA[_s_all[bi], _s_all[ai]] mint = _t end - d2 == fzero && return d2, mins, mint + iszero(d2) && return d2, minr, mins, mint ai, bi, ci = bi, ci, ai end for i in 1:3 - @inline this_d2, _s, _t = dist2LineTriangle_s_t(SA[y[ai],y[bi]],x) + this_d2, _r, _s, _t = dist_sqr_r_s_t(SA[y[ai],y[bi]], x) if this_d2 < d2 d2 = this_d2 + minr = -_r _s_all = SA[fzero, _s[1], fone - _s[1]] - mint = SA[_s_all[bi],_s_all[ai]] + mint = SA[_s_all[bi], _s_all[ai]] mins = _t end - d2 == fzero && return d2, mins, mint + iszero(d2) && return d2, minr, mins, mint ai, bi, ci = bi, ci, ai end - return d2, mins, mint + return d2, minr, mins, mint end - - +function dist_sqr(x::Triangle{T}, y::Triangle{T}) where T + d2 = dist_sqr(SA[x[1],x[2]], y) + iszero(d2) && return d2 + ai = 2 + bi = 3 + ci = 1 + for i in 2:3 + this_d2 = dist_sqr(SA[x[ai],x[bi]], y) + d2 = Base.FastMath.min_fast(d2, this_d2) + iszero(d2) && return d2 + ai, bi, ci = bi, ci, ai + end + for i in 1:3 + this_d2 = dist_sqr(SA[y[ai],y[bi]], x) + d2 = Base.FastMath.min_fast(d2, this_d2) + iszero(d2) && return d2 + ai, bi, ci = bi, ci, ai + end + return d2 +end diff --git a/src/naive.jl b/src/naive.jl deleted file mode 100644 index 091b4f2..0000000 --- a/src/naive.jl +++ /dev/null @@ -1,155 +0,0 @@ -# Naive implementation using double for loops - - - -"""$PUBLIC -A simple `SimplexCellList` with no extra options that uses double for loops. - - Naive(numpointgroups::Integer, numlinegroups::Integer, numtrianglegroups::Integer) - -Where `numpointgroups` is the number of groups of points, `numlinegroups` is the number of groups of lines, -and `numtrianglegroups` is the number of groups of triangles. -""" -struct Naive <: SimplexCellList - data::Tuple{Vector{Vector{Point}}, Vector{Vector{Line}}, Vector{Vector{Triangle}}} - - "true if element exists" - exists::Tuple{Vector{Vector{Bool}}, Vector{Vector{Bool}}, Vector{Vector{Bool}}} -end - - -function Naive(numpointgroups::Integer, numlinegroups::Integer, numtrianglegroups::Integer) - Naive( - ( - [Point[] for i in 1:numpointgroups], - [Line[] for i in 1:numlinegroups], - [Triangle[] for i in 1:numtrianglegroups], - ), - ( - [Bool[] for i in 1:numpointgroups], - [Bool[] for i in 1:numlinegroups], - [Bool[] for i in 1:numtrianglegroups], - ), - ) -end - - -function setElements!(s::Naive, points, lines, triangles)::Nothing - @argcheck length(points) == length(s.data[1]) - @argcheck length(lines) == length(s.data[2]) - @argcheck length(triangles) == length(s.data[3]) - in_data_tuple = (points, lines, triangles) - Base.Cartesian.@nexprs 3 n -> begin - in_data = in_data_tuple[n] - for (group_idx, in_group) in enumerate(in_data) - group = s.data[n][group_idx] - group_exists = s.exists[n][group_idx] - resize!(group, length(in_group)) - resize!(group_exists, length(in_group)) - group .= in_group - group_exists .= true - end - end - return -end - - -function addElement!(s::Naive, group_idx::Integer, element::Simplex{N})::Int32 where {N} - group = push!(s.data[N][group_idx],element) - push!(s.exists[N][group_idx], true) - return length(group) -end - - -function deactivate!(s::Naive, group_idx::Integer, element_idx::Integer, elementtype::Type{Simplex{N}})::Nothing where {N} - s.exists[N][group_idx][element_idx] = false - return -end - - -function activate!(s::Naive, group_idx::Integer, element_idx::Integer, element_type::Type{Simplex{N}})::Nothing where {N} - s.exists[N][group_idx][element_idx] = true - return -end - - -function isActive(s::Naive, group_idx::Integer, element_idx::Integer, element_type::Type{Simplex{N}})::Bool where {N} - s.exists[N][group_idx][element_idx] -end - - -function mapSimplexElements(f, output, s::Naive, x::Simplex{N}, group_idx::Integer, elements_type::Type{Simplex{M}}, cutoff::Float32) where {N, M} - # just loop through all element in groupid - group = s.data[M][group_idx] - exists = s.exists[M][group_idx] - cutoff_sqr = cutoff^2 - for (j, y) in enumerate(group) - if exists[j] - @inline d2 = distSqr(x, y) - if d2 ≤ cutoff_sqr - @inline output = f(x, y, 0%Int32, j%Int32, d2, output) - end - end - end - return output -end - - -function mapPairElements(f, output, s::Naive, group_idx::Integer, elements_type::Type{Simplex{N}}, cutoff::Float32) where {N} - # just double loop through all element in groupid - cutoff_sqr = cutoff^2 - group = s.data[N][group_idx] - exists = s.exists[N][group_idx] - n = length(group) - for i in 1:(n-1) - if exists[i] - x = group[i] - for j in (i+1):n - if exists[j] - y = group[j] - @inline d2 = distSqr(x, y) - if d2 ≤ cutoff_sqr - @inline output = f(x, y, i%Int32, j%Int32, d2, output) - end - end - end - end - end - return output -end - - -function mapElementsElements( - f, - output, - s::Naive, - x_group_idx::Integer, - x_type::Type{Simplex{N}}, - y_group_idx::Integer, - y_type::Type{Simplex{M}}, - cutoff::Float32, - ) where {N, M} - cutoff_sqr = cutoff^2 - # just double loop through all elements - x_group = s.data[N][x_group_idx] - x_exists = s.exists[N][x_group_idx] - y_group = s.data[M][y_group_idx] - y_exists = s.exists[M][y_group_idx] - xn = length(x_group) - yn = length(y_group) - for i in 1:xn - if x_exists[i] - x = x_group[i] - for j in 1:yn - if y_exists[j] - y = y_group[j] - @inline d2 = distSqr(x, y) - if d2 ≤ cutoff_sqr - @inline output = f(x, y, i%Int32, j%Int32, d2, output) - end - end - end - end - end - return output -end \ No newline at end of file diff --git a/src/painter.jl b/src/painter.jl deleted file mode 100644 index eba3ae9..0000000 --- a/src/painter.jl +++ /dev/null @@ -1,694 +0,0 @@ -const MAX_SAMPLE_SPACING = Float32(1//8) - -"""$PUBLIC -A grid is painted with element ids based on a max range. Based on the ideas in "cytosim". - - Painter(numpointgroups::Integer, numlinegroups::Integer, numtrianglegroups::Integer; - grid_start::SVector{3,<:AbstractFloat}, - grid_size::SVector{3,<:Integer}, - voxel_length::AbstractFloat, - max_range::Union{SVector{3, <:Vector{<:AbstractFloat}}, AbstractFloat}, - ) - -Where `numpointgroups` is the number of groups of points, `numlinegroups` is the number of groups of lines, -and `numtrianglegroups` is the number of groups of triangles. - -The grid is in a box with one corner at `grid_start .- voxel_length/2` -and another corner at `(grid_start .- voxel_length/2) + grid_size*voxel_length` - -`grid_size` is the number of cubic voxels in each dimension and `voxel_length` is the side length of each voxel. - -`min_point` and `max_point` instead of `grid_size` and `grid_start` to specify corners of the grid. -The actual grid corners can extend by up to half a voxel length out from `min_point` and `max_point`. - -Elements can safely be outside of the grid, but a large number of elements outside the grid will degrade performance. - -`max_range` is the max cutoff that can be used when using the mapping functions. It is specified per group, or for all groups. - -When `mapElementsElements` is used, the cutoff just has to be less than the `max_range` of the `y` group. - -This implementation is based on the idea in https://gitlab.com/f-nedelec/cytosim/-/blob/master/src/sim/fiber_grid.h -And in figure 10 of "Francois Nedelec and Dietrich Foethke 2007 New J. Phys. 9 427" -to paint a grid with elements based on a maximum range. -""" -struct Painter <: SimplexCellList - grid_start::SVector{3,Float32} - - grid_size::SVector{3,Int32} - - voxel_length::Float32 - - inv_voxel_length::Float32 - - "Max ranges of each group in grid units" - max_range::SVector{3, Vector{Float32}} - - "indexed by shape id then (groupid,x,y,z) to get a vector of elements painted" - grids::SVector{3, Array{Vector{Tuple{Int32, Float32}}, 4}} - - "A place to put stuff that is outside the grid - indexed by shape id then groupid to get a vector of elements painted outside the grid" - outsidegrids::SVector{3, Vector{Vector{Tuple{Int32, Float32}}}} - - "in grid units" - data::Tuple{Vector{Vector{Point}}, Vector{Vector{Line}}, Vector{Vector{Triangle}}} - - "true if element exists" - exists::Tuple{Vector{Vector{Bool}}, Vector{Vector{Bool}}, Vector{Vector{Bool}}} -end - - -function Painter(numpointgroups::Integer, numlinegroups::Integer, numtrianglegroups::Integer; - grid_start::Union{SVector{3,<:AbstractFloat}, Nothing}=nothing, - grid_size::Union{SVector{3,<:Integer}, Nothing}=nothing, - min_point::Union{SVector{3,<:AbstractFloat}, Nothing}=nothing, - max_point::Union{SVector{3,<:AbstractFloat}, Nothing}=nothing, - voxel_length::AbstractFloat, - max_range::Union{SVector{3, <:Vector{<:AbstractFloat}}, AbstractFloat}, - ) - if isnothing(grid_start) & isnothing(grid_size) - @argcheck !isnothing(min_point) - @argcheck !isnothing(max_point) - @argcheck all(max_point .≥ min_point) - grid_size = max.(ceil.(Int32, (max_point-min_point) ./ voxel_length), Int32(1)) - real_grid_size = grid_size .* voxel_length - grid_center = (1//2)*(max_point + min_point) - grid_start = grid_center - (1//2)*real_grid_size .+ (1//2)*voxel_length - end - @argcheck !isnothing(grid_start) - @argcheck !isnothing(grid_size) - @argcheck voxel_length > 0 - _max_range = if max_range isa AbstractFloat - [fill(max_range,n) for n in (numpointgroups, numlinegroups, numtrianglegroups)] - else - @argcheck length(max_range[1]) == numpointgroups - @argcheck length(max_range[2]) == numlinegroups - @argcheck length(max_range[3]) == numtrianglegroups - max_range - end - pointgrid = [Tuple{Int32, Float32}[] for i in 1:numpointgroups, j in 1:grid_size[1], k in 1:grid_size[2], l in 1:grid_size[3]] - linegrid = [Tuple{Int32, Float32}[] for i in 1:numlinegroups, j in 1:grid_size[1], k in 1:grid_size[2], l in 1:grid_size[3]] - trianglegrid = [Tuple{Int32, Float32}[] for i in 1:numtrianglegroups, j in 1:grid_size[1], k in 1:grid_size[2], l in 1:grid_size[3]] - Painter( - grid_start, - grid_size, - voxel_length, - inv(voxel_length), - _max_range .* inv(voxel_length), - SA[pointgrid, linegrid, trianglegrid], - SA[[Tuple{Int32, Float32}[] for i in 1:numpointgroups], [Tuple{Int32, Float32}[] for i in 1:numlinegroups], [Tuple{Int32, Float32}[] for i in 1:numtrianglegroups]], - ([Point[] for i in 1:numpointgroups], [Line[] for i in 1:numlinegroups], [Triangle[] for i in 1:numtrianglegroups]), - ([Bool[] for i in 1:numpointgroups], [Bool[] for i in 1:numlinegroups], [Bool[] for i in 1:numtrianglegroups]), - ) -end - - -""" -Return nearest grid point, x is in grid units -Return nothing if outside of the grid -""" -function _nearestGridPoint(grid_size::SVector{3,Int32}, x::SVector{3,Float32}) - x_int = round.(Int32, x) - if any(x_int .> (grid_size .- Int32(1))) || any(x_int .< Int32(0)) - nothing - else - x_int - end -end - -""" -Paint an element onto the grid. -All inputs have grid units. -Max range is extended by `MAX_SAMPLE_SPACING` because of how elements are sampled. -Any voxel that may contain points within `max_range + MAX_SAMPLE_SPACING` of `element` -will have `i`, and the distance squared, added to that voxel's list. - -In grid units, voxel (0,0,0) is a unit cube with center at SA[0.0, 0.0, 0.0] -""" -function _paintElement(s::Painter, groupid, i, element::Simplex{N}, max_range) where {N} - grid = s.grids[N] - outsidegrid = s.outsidegrids[N] - #TODO try removing Float32(1//2*√(3)) and using an axis aligned unit cube to simplex distance function - extended_max_range = max_range + MAX_SAMPLE_SPACING - voxelcutoff = Float32(1//2*√(3)) + max_range + MAX_SAMPLE_SPACING - voxelcutoff2 = voxelcutoff^2 - #TODO try cytosim's method of rasterizing instead of using an axis aligned bounding box - # Get the axis aligned bounding box - bottom = min.(element...) - top = max.(element...) - # Add max_range + MAX_SAMPLE_SPACING - bottom = bottom .- extended_max_range - top = top .+ extended_max_range - bottom_int = round.(Int32, bottom) - top_int = round.(Int32, top) - # Go through all voxel center points and check distance to element and paint if in range. - min_outside_d2 = Inf32 - for voxel_int in Iterators.product(range.(bottom_int,top_int)...) - # @show voxel_int - @inline voxel_float = SVector{3,Float32}(voxel_int...) - @inline d2 = distSqr(SA[voxel_float,], element) - if d2 ≤ voxelcutoff2 - if any(voxel_int .> (s.grid_size .- Int32(1))) || any(voxel_int .< Int32(0)) - min_outside_d2 = min(min_outside_d2, d2) - else - voxelid = voxel_int .+ Int32(1) - voxel = grid[groupid, voxelid...] - push!(voxel, (i,d2)) - end - end - end - if isfinite(min_outside_d2) - push!(outsidegrid[groupid],(i,min_outside_d2)) - end -end - - -function setElements!(s::Painter, points, lines, triangles)::Nothing - @argcheck length(points) == length(s.data[1]) - @argcheck length(lines) == length(s.data[2]) - @argcheck length(triangles) == length(s.data[3]) - in_data_tuple = (points, lines, triangles) - Base.Cartesian.@nexprs 3 n -> begin - in_data = in_data_tuple[n] - for (groupid, in_group) in enumerate(in_data) - group = s.data[n][groupid] - groupexists = s.exists[n][groupid] - resize!(group, length(in_group)) - resize!(groupexists, length(in_group)) - i = 0 - for stuff in in_group - i += 1 - group[i] = (stuff .- (s.grid_start,)) .* s.inv_voxel_length - end - groupexists .= true - end - end - Base.Cartesian.@nexprs 3 n -> begin - for (groupid, group) in enumerate(s.data[n]) - max_range = s.max_range[n][groupid] - for (i, element) in enumerate(group) - _paintElement(s, groupid, i, element, max_range) - end - end - end -end - - -function addElement!(s::Painter, group_idx::Integer, element::Simplex{N})::Int32 where {N} - x = (element .- (s.grid_start,)) .* s.inv_voxel_length - group = push!(s.data[N][group_idx], x) - push!(s.exists[N][group_idx], true) - i = length(group) - _paintElement(s, group_idx, i, x, s.max_range[N][group_idx]) - return i -end - - -function deactivate!(s::Painter, group_idx::Integer, element_idx::Integer, elementtype::Type{Simplex{N}})::Nothing where {N} - s.exists[N][group_idx][element_idx] = false - return -end - - -function activate!(s::Painter, group_idx::Integer, element_idx::Integer, element_type::Type{Simplex{N}})::Nothing where {N} - s.exists[N][group_idx][element_idx] = true - return -end - - -function isActive(s::Painter, group_idx::Integer, element_idx::Integer, element_type::Type{Simplex{N}})::Bool where {N} - s.exists[N][group_idx][element_idx] -end - - -""" -This function is to avoid any floating point weirdness -""" -@noinline function _getSampleGridPoint(inv_n::Float32, n::Int32, R::Int32, x::Simplex{2}, grid_size) - Q::Int32 = n-R - p1 = Q*x[1] + R*x[2] - p2 = inv_n*p1 - _nearestGridPoint(grid_size, p2) -end - -""" -Get the grid point that is sampled by x, to y. -x and y are in grid units -""" -@noinline function _canonicalGridPoint(x::Simplex{N}, y::Simplex{M}, grid_size, extra)::Union{Nothing,SVector{3, Int32}} where {N,M} - if N == 1 - return _nearestGridPoint(grid_size, x[1]) - elseif N == 2 - if isnothing(extra) - return _nearestGridPoint(grid_size, 1//2*(x[2] + x[1])) - else - halfn::Int32, inv_n::Float32 = extra - d2, smin = distSqr_sMin(x,y) - n = Int32(2)*halfn - R::Int32 = clamp(trunc(Int32, smin[1]*halfn),Int32(0),halfn-Int32(1))*Int32(2) + Int32(1) - return _getSampleGridPoint(inv_n, n, R, x, grid_size) - end - # elseif N == 3 - # if isnothing(extra) - # return _nearestGridPoint(grid_size, 1//3*(x[3] + x[2] + x[1])) - # else - # d2, smin = distSqr_sMin(x,y) - # pt = (1-(smin[1]+smin[2])).*x[1] + smin[1].*x[2] + smin[2].*x[3] - # return _nearestGridPoint(grid_size, pt) - # end - else - error("simplexes with $N points not supported") - end -end - - -function mapSimplexElements( - f, - output, - s::Painter, - in_x::Simplex{N}, - group_idx::Integer, - elements_type::Type{Simplex{M}}, - in_cutoff::Float32, - ; - x = (in_x .- (s.grid_start,)) .* s.inv_voxel_length, - i::Int32=Int32(0), - filterj=Returns(true), - ) where {N, M} - error("Not implemented for those simplexes") -end - -#point to other -function mapSimplexElements( - f, - output, - s::Painter, - in_x::Simplex{1}, - group_idx::Integer, - elements_type::Type{Simplex{M}}, - in_cutoff::Float32, - ; - x = (in_x .- (s.grid_start,)) .* s.inv_voxel_length, - i::Int32=Int32(0), - filterj=Returns(true), - ) where{M} - cutoff = in_cutoff * s.inv_voxel_length - @argcheck cutoff ≤ s.max_range[M][group_idx] - group = s.data[M][group_idx] - exists = s.exists[M][group_idx] - grid = s.grids[M] - outsidegrid = s.outsidegrids[M] - voxelcutoff::Float32 = Float32(1//2*√(3)) + cutoff + MAX_SAMPLE_SPACING - voxelcutoff_sqr = voxelcutoff^2 - cutoff_sqr = cutoff^2 - voxel_length_sqr = s.voxel_length^2 - # sample unique voxels that the element goes in based on sampling points that are at most 1/8 voxel_length away from any point in the element. - gridpoint = _nearestGridPoint(s.grid_size, x[1]) - voxel = if isnothing(gridpoint) - outsidegrid[group_idx] - else - voxelid = gridpoint .+ Int32(1) - grid[group_idx, voxelid...] - end - for (j, sqrdist) in voxel - if filterj(j) - if sqrdist ≤ voxelcutoff_sqr - # load other element - if exists[j] - y = group[j] - @inline d2 = distSqr(x, y) - if d2 ≤ cutoff_sqr - in_y = (y .* s.voxel_length) .+ (s.grid_start,) - output = f(in_x, in_y, i, j, d2*voxel_length_sqr, output) - end - end - end - end - end - output -end - -#line to other -function mapSimplexElements( - f, - output, - s::Painter, - in_x::Simplex{2}, - group_idx::Integer, - elements_type::Type{Simplex{M}}, - in_cutoff::Float32, - ; - x = (in_x .- (s.grid_start,)) .* s.inv_voxel_length, - i::Int32=Int32(0), - filterj=Returns(true), - ) where {M} - cutoff = in_cutoff * s.inv_voxel_length - @argcheck cutoff ≤ s.max_range[M][group_idx] - group = s.data[M][group_idx] - exists = s.exists[M][group_idx] - grid = s.grids[M] - outsidegrid = s.outsidegrids[M] - voxelcutoff::Float32 = Float32(1//2*√(3)) + cutoff + MAX_SAMPLE_SPACING - voxelcutoff_sqr = voxelcutoff^2 - cutoff_sqr = cutoff^2 - voxel_length_sqr = s.voxel_length^2 - # sample unique voxels that the element goes in based on sampling points that are at most 1/8 voxel_length away from any point in the element. - # if the line segment is short enough just us the mid point - r = x[2] - x[1] - len2 = r ⋅ r - midgridpoint = _nearestGridPoint(s.grid_size, 1//2*(x[2] + x[1])) - usemidpoint = false - if len2 < (MAX_SAMPLE_SPACING*2)^2 - usemidpoint = true - else - # if both ends, moved in by MAX_SAMPLE_SPACING are in the same voxel, just sample that voxel. - len = √len2 - small_s = MAX_SAMPLE_SPACING/len - start_pt = (1-small_s)*x[1] + (small_s)*x[2] - end_pt = (1-small_s)*x[2] + (small_s)*x[1] - gridpoint1 = _nearestGridPoint(s.grid_size, start_pt) - gridpoint2 = _nearestGridPoint(s.grid_size, end_pt) - if gridpoint1 == gridpoint2 && !isnothing(gridpoint1) - usemidpoint = true - end - end - if usemidpoint - voxel = if isnothing(midgridpoint) - outsidegrid[group_idx] - else - voxelid = midgridpoint .+ Int32(1) - grid[group_idx, voxelid...] - end - for (j, sqrdist) in voxel - if filterj(j) - if sqrdist ≤ voxelcutoff_sqr - # load other element - if exists[j] - y = group[j] - @inline d2 = distSqr(x, y) - if d2 ≤ cutoff_sqr - in_y = (y .* s.voxel_length) .+ (s.grid_start,) - output = f(in_x, in_y, i, j, d2*voxel_length_sqr, output) - end - end - end - end - end - else - # Now more than one voxel needs to be sampled, yikes. - # I need to ensure that _canonicalGridPoint can find a gridpoint that is actually sampled - # This is to avoid double counting - num_sub_div = ceil(inv(2*MAX_SAMPLE_SPACING)*len) - inv_n::Float32 = inv(2num_sub_div) - halfn = Int32(num_sub_div) - extra = (halfn, inv_n) - n = Int32(2)*halfn - prevgridpoint = SA[Int32(-1),Int32(-1),Int32(-1)] - checked_outside = false - for samplei in Int32(1):Int32(2):n - gridpoint = _getSampleGridPoint(inv_n, n, samplei, x, s.grid_size) - if gridpoint != prevgridpoint && !(checked_outside && isnothing(gridpoint)) - voxel = if isnothing(gridpoint) - checked_outside = true - outsidegrid[group_idx] - else - voxelid = gridpoint .+ Int32(1) - grid[group_idx, voxelid...] - end - for (j, sqrdist) in voxel - if filterj(j) - if sqrdist ≤ voxelcutoff_sqr - # load other element - if exists[j] - y = group[j] - @inline d2 = distSqr(x, y) - if d2 ≤ cutoff_sqr - c_gridpoint = _canonicalGridPoint(x, y, s.grid_size, extra) - if gridpoint == c_gridpoint - in_y = (y .* s.voxel_length) .+ (s.grid_start,) - @inline output = f(in_x, in_y, i, j, d2*voxel_length_sqr, output) - end - end - end - end - end - end - prevgridpoint = gridpoint - end - end - end - return output -end - -#triangle to other -function mapSimplexElements( - f, - output, - s::Painter, - in_x::Simplex{3}, - group_idx::Integer, - elements_type::Type{Simplex{M}}, - in_cutoff::Float32, - ; - x = (in_x .- (s.grid_start,)) .* s.inv_voxel_length, - i::Int32=Int32(0), - filterj=Returns(true), - ) where {M} - cutoff = in_cutoff * s.inv_voxel_length - @argcheck cutoff ≤ s.max_range[M][group_idx] - group = s.data[M][group_idx] - exists = s.exists[M][group_idx] - grid = s.grids[M] - outsidegrid = s.outsidegrids[M] - voxelcutoff::Float32 = Float32(1//2*√(3)) + cutoff + MAX_SAMPLE_SPACING - voxelcutoff_sqr = voxelcutoff^2 - cutoff_sqr = cutoff^2 - voxel_length_sqr = s.voxel_length^2 - # sample unique voxels that the element goes in based on sampling points that are at most 1/8 voxel_length away from any point in the element. - # if the line segment is short enough just us the mid point - mid_pt = 1//3*(x[3] + x[2] + x[1]) - # midgridpoint is nothing if it is outside the grid - midgridpoint = _nearestGridPoint(s.grid_size, 1//3*(x[3] + x[2] + x[1])) - usemidpoint = false - rs = x .- Ref(mid_pt) - ds = norm.(rs) - if all(<(MAX_SAMPLE_SPACING), ds) - usemidpoint = true - elseif !isnothing(midgridpoint) - voxel_rs = x .- Ref(midgridpoint) - voxel_ds = norm.(voxel_rs) - # if all ends, moved toward the center of the mid point voxel by MAX_SAMPLE_SPACING are in the same voxel, just sample that voxel. - # TODO move the points into a single voxel more efficently. - move_ins = min.(MAX_SAMPLE_SPACING ./ voxel_ds, 1.0f0) - moved_in_rs = (1 .- move_ins) .* voxel_rs - moved_in_x = Ref(midgridpoint) .+ moved_in_rs - gridpoints = _nearestGridPoint.(Ref(s.grid_size), moved_in_x) - if allequal((gridpoints..., midgridpoint)) - usemidpoint = true - end - end - # @show usemidpoint - if usemidpoint - voxel = if isnothing(midgridpoint) - outsidegrid[group_idx] - else - voxelid = midgridpoint .+ Int32(1) - grid[group_idx, voxelid...] - end - for (j, sqrdist) in voxel - if filterj(j) - if sqrdist ≤ voxelcutoff_sqr - # load other element - if exists[j] - y = group[j] - @inline d2 = distSqr(x, y) - if d2 ≤ cutoff_sqr - in_y = (y .* s.voxel_length) .+ (s.grid_start,) - output = f(in_x, in_y, i, j, d2*voxel_length_sqr, output) - end - end - end - end - end - else - # Now more than one voxel needs to be sampled, yikes. - # - visited = Dict{Int32,Float32}() - bottom = min.(x...) - top = max.(x...) - maxabs = max.(abs.(top),abs.(bottom)) - error_est = 32*eps(maximum(maxabs)) - top_extend_int = round.(Int32, top .+ error_est) - bottom_extend_int = round.(Int32, bottom .- error_est) - check_outside = false - if any(top_extend_int .> (s.grid_size .- Int32(1))) || any(bottom_extend_int .< Int32(0)) - check_outside = true - top_extend_int = clamp.(top_extend_int, Int32(0), (s.grid_size .- Int32(1))) - bottom_extend_int = clamp.(bottom_extend_int, Int32(0), (s.grid_size .- Int32(1))) - end - samplevoxelcutoff = Float32(1//2*√(3)) + error_est + MAX_SAMPLE_SPACING - samplevoxelcutoff2 = samplevoxelcutoff^2 - for gridpoint_tuple in Iterators.flatten(((nothing,), Iterators.product(range.(bottom_extend_int,top_extend_int)...))) - gridpoint = isnothing(gridpoint_tuple) ? nothing : SVector(gridpoint_tuple) - # if gridpoint is outside but we don't need to check outside, skip - if isnothing(gridpoint) && !check_outside - continue - end - # continue if the triangle could not possibly be in the voxel - if !isnothing(gridpoint) - @inline voxel_float = SVector{3,Float32}(gridpoint...) - @inline sample_voxel_d2 = distSqr(SA[voxel_float,], x) - if sample_voxel_d2 > samplevoxelcutoff2 - continue - end - end - # at this point we want to sample gridpoint - voxel = if isnothing(gridpoint) - outsidegrid[group_idx] - else - voxelid = gridpoint .+ Int32(1) - grid[group_idx, voxelid...] - end - for (j, sqrdist) in voxel - if filterj(j) - if sqrdist ≤ voxelcutoff_sqr - # load other element - if exists[j] - if !(haskey(visited,j)) - y = group[j] - @inline d2 = distSqr(x, y) - visited[j] = d2 - end - end - end - end - end - end - for (j,d2) in pairs(visited) - if d2 ≤ cutoff_sqr - y = group[j] - in_y = (y .* s.voxel_length) .+ (s.grid_start,) - @inline output = f(in_x, in_y, i, j, d2*voxel_length_sqr, output) - end - end - # I need to ensure that _canonicalGridPoint can find a gridpoint that is actually sampled - # This is to avoid double counting - # start by getting a axis aligned bounding box around x - # we need to sample any voxels that x touches or could touch - # given floating point rounding. - - # bottom = min.(x...) - # top = max.(x...) - # maxabs = max.(abs.(top),abs.(bottom)) - # error_est = 32*eps(maximum(maxabs)) - # top_extend_int = round.(Int32, top .+ error_est) - # bottom_extend_int = round.(Int32, bottom .- error_est) - # check_outside = false - # if any(top_extend_int .> (s.grid_size .- Int32(1))) || any(bottom_extend_int .< Int32(0)) - # check_outside = true - # top_extend_int = clamp.(top_extend_int, Int32(0), (s.grid_size .- Int32(1))) - # bottom_extend_int = clamp.(bottom_extend_int, Int32(0), (s.grid_size .- Int32(1))) - # end - # samplevoxelcutoff = Float32(1//2*√(3)) + error_est + MAX_SAMPLE_SPACING - # samplevoxelcutoff2 = samplevoxelcutoff^2 - # for gridpoint_tuple in Iterators.flatten(((nothing,), Iterators.product(range.(bottom_extend_int,top_extend_int)...))) - # gridpoint = isnothing(gridpoint_tuple) ? nothing : SVector(gridpoint_tuple) - # # if gridpoint is outside but we don't need to check outside, skip - # if isnothing(gridpoint) && !check_outside - # continue - # end - # # continue if the triangle could not possibly be in the voxel - # if !isnothing(gridpoint) - # @inline voxel_float = SVector{3,Float32}(gridpoint...) - # @inline sample_voxel_d2 = distSqr(SA[voxel_float,], x) - # if sample_voxel_d2 > samplevoxelcutoff2 - # continue - # end - # end - # # at this point we want to sample gridpoint - # voxel = if isnothing(gridpoint) - # outsidegrid[group_idx] - # else - # voxelid = gridpoint .+ Int32(1) - # grid[group_idx, voxelid...] - # end - # for (j, sqrdist) in voxel - # if filterj(j) - # if sqrdist ≤ voxelcutoff_sqr - # # load other element - # if exists[j] - # y = group[j] - # @inline d2 = distSqr(x, y) - # if d2 ≤ cutoff_sqr - # c_gridpoint = _canonicalGridPoint(x, y, s.grid_size, true) - # if gridpoint == c_gridpoint - # in_y = (y .* s.voxel_length) .+ (s.grid_start,) - # @inline output = f(in_x, in_y, i, j, d2*voxel_length_sqr, output) - # end - # end - # end - # end - # end - # end - # end - end - return output -end - - -function mapPairElements( - f, - output, - s::Painter, - group_idx::Integer, - elements_type::Type{Simplex{N}}, - cutoff::Float32, - ) where {N} - # loop through all element in groupid - # and call mapSimplexElements with extra optional parameters - # to only run on elements with id > i, to avoid double counting - group = s.data[N][group_idx] - exists = s.exists[N][group_idx] - n = length(group) - for i in Int32(1):Int32(n-1) - if exists[i] - x = group[i] - in_x = (x .* s.voxel_length) .+ (s.grid_start,) - output = mapSimplexElements(f, output, s, in_x, group_idx, elements_type, cutoff; - i, - filterj = >(i), - x, - ) - end - end - return output -end - - -function mapElementsElements( - f, - output, - s::Painter, - x_group_idx::Integer, - x_type::Type{Simplex{N}}, - y_group_idx::Integer, - y_type::Type{Simplex{M}}, - cutoff::Float32, - ) where {N, M} - # loop through all element in groupid - # and call mapSimplexElements with extra optional parameters - x_group = s.data[N][x_group_idx] - x_exists = s.exists[N][x_group_idx] - x_n = length(x_group) - for i in Int32(1):Int32(x_n) - if x_exists[i] - x = x_group[i] - in_x = (x .* s.voxel_length) .+ (s.grid_start,) - output = mapSimplexElements(f, output, s, in_x, y_group_idx, y_type, cutoff; - i, - x, - ) - end - end - return output -end \ No newline at end of file diff --git a/src/pointcelllist.jl b/src/pointcelllist.jl new file mode 100644 index 0000000..2391680 --- /dev/null +++ b/src/pointcelllist.jl @@ -0,0 +1,177 @@ +""" + CellPointEntry{T,F} + +A single point's entry in one voxel cell. Created by [`cell_point_add!`](@ref) +and read by [`map_nearby_points`](@ref). + +# Fields +- `pos::SVector{3,F}`: point position. +- `point_id::T`: point ID. +""" +struct CellPointEntry{T,F} + pos::SVector{3,F} + point_id::T +end + +struct CellPoint{T,F} + list::Memory{CellPointEntry{T,F}} + len::Int +end +CellPoint{T,F}() where{T,F} = CellPoint{T,F}(Memory{CellPointEntry{T,F}}(), 0) + +function _cell_point_push_entry!(old::CellPoint{T,F}, entry::CellPointEntry{T,F}) where {T,F} + old_list = old.list + cap = length(old_list) + len = old.len + list = if cap ≤ len + new_cap = max(cap<<1, 1) + @assert new_cap > len + new_list = Memory{CellPointEntry{T,F}}(undef, new_cap) + unsafe_copyto!(new_list, 1, old_list, 1, cap) + new_list + else + old_list + end + list[len+1] = entry + CellPoint{T,F}(list, len+1) +end + +""" + PointCellList{T,F}(size::SVector{3, Int}, grid_spacing::F) + PointCellList{T,F}(size::NTuple{3, Int}, grid_spacing::F) + +An origin-centered 3D voxel grid for spatial queries on points. The grid spans +`[-size * grid_spacing/2, size * grid_spacing/2)` in each dimension. + +Insert points with [`cell_point_add!`](@ref) and query with +[`map_nearby_points`](@ref). Use [`cell_point_clear!`](@ref) to reset. + +Positions are not required to be within the grid bounds: points outside the +grid are stored in the nearest boundary cell, and queries account for this. +Points far outside the grid accumulate in boundary cells, which can slow down +queries near those boundaries. + +# Arguments +- `size`: number of cells along each axis (x, y, z). +- `grid_spacing::F`: side length of each cubic cell. +""" +struct PointCellList{T,F} + cells::Memory{CellPoint{T,F}} + size::SVector{3, Int} + inv_half_grid_spacing::F +end +function PointCellList{T,F}(size::SVector{3, Int}, grid_spacing::F) where {T,F} + @argcheck all(≥(0), size) + @argcheck grid_spacing > 0 + inv_half_grid_spacing = 2*inv(grid_spacing) + @argcheck isfinite(inv_half_grid_spacing) + n = prod(size) + cells = Memory{CellPoint{T,F}}(undef, n) + fill!(cells, CellPoint{T,F}()) + PointCellList{T,F}(cells, size, inv_half_grid_spacing) +end +PointCellList{T,F}(size::NTuple{3, Int}, grid_spacing::F) where {T,F} = PointCellList{T,F}(SVector{3,Int}(size...), grid_spacing) + +""" + cell_point_clear!(pcl::PointCellList)::Nothing + +Remove all points. +""" +function cell_point_clear!(pcl::PointCellList{T,F})::Nothing where {T,F} + for i in eachindex(pcl.cells) + pcl.cells[i] = CellPoint{T,F}(pcl.cells[i].list, 0) + end +end + +""" + cell_point_add!(pcl::PointCellList{T,F}, pos::SVector{3, F}, point_id::T)::Nothing + +Insert a point into the cell list, adding a [`CellPointEntry`](@ref) to the cell +containing the point. If the point is outside the grid bounds, it is added to +the nearest boundary cell. + +# Arguments +- `pcl`: the point cell list to insert into. +- `pos`: point position. +- `point_id`: ID for this point. +""" +function cell_point_add!(pcl::PointCellList{T,F}, pos::SVector{3, F}, point_id::T)::Nothing where {T,F} + h2_inv = pcl.inv_half_grid_spacing + sz = pcl.size + @argcheck all(>(0), sz) + strides = SVector(1, sz[1], sz[1]*sz[2]) + # Work in doubled coordinates 2*pos/h so the origin-centering offset is + # added exactly in Int after the floor, keeping cell assignment accurate + # for any grid size. Clamp in F before converting to Int so far away + # positions don't overflow Int; the prevfloat upper bound keeps the + # resulting index ≤ size - 1. + pos_norm = clamp.(pos*h2_inv, -F.(sz), prevfloat.(F.(sz))) + i = (floor.((Int,), pos_norm) .+ sz) .>> 1 + li = i ⋅ strides + entry = CellPointEntry{T,F}(pos, point_id) + pcl.cells[begin + li] = _cell_point_push_entry!(pcl.cells[begin + li], entry) + nothing +end + +""" + map_nearby_points(f, pcl::PointCellList{T,F}, pos::SVector{3, F}, cutoff::F, out) -> out + +Call `f` for every point in `pcl` within `cutoff` distance of `pos`. + +# Arguments +- `f(entry::CellPointEntry{T,F}, out) -> (out, cont::Bool)`: callback called for each nearby point. + Return `(new_out, true)` to continue, or `(new_out, false)` to stop early. +- `pcl`: the point cell list to query. +- `pos`: query point. +- `cutoff`: maximum distance. If zero, returns `out` immediately. +- `out`: initial accumulator value, threaded through `f` and returned. +""" +function map_nearby_points( + f, + pcl::PointCellList{T,F}, + pos::SVector{3, F}, + cutoff::F, + out, + ) where {T,F} + cutoff ≤ 0 && return out + sz = pcl.size + all(>(0), sz) || return out + cutoff2 = cutoff * cutoff + h2_inv = pcl.inv_half_grid_spacing + strides = SVector(1, sz[1], sz[1]*sz[2]) + # Clamping the query into the grid box makes the cube test below treat + # boundary cells as extending outward to infinity, so points clamped into + # boundary cells by cell_point_add! are never pruned away. For any stored + # point q in cell c, dist(clamped pos_norm, cube of c) ≤ |pos - q| * 2/h, + # so the test stays conservative. Clamping in F before converting to Int + # also keeps far away queries and huge cutoffs from overflowing Int. + pos_norm = clamp.(pos * h2_inv, -F.(sz), prevfloat.(F.(sz))) + r_cells = cutoff * h2_inv + lo = (floor.((Int,), max.(pos_norm .- r_cells, -F.(sz))) .+ sz) .>> 1 + hi = (floor.((Int,), min.(pos_norm .+ r_cells, prevfloat.(F.(sz)))) .+ sz) .>> 1 + for iz in lo[3]:hi[3] + liz = iz*strides[3] + cell_center_z = 2*iz + 1 - sz[3] + for iy in lo[2]:hi[2] + liy = iy*strides[2] + cell_center_y = 2*iy + 1 - sz[2] + for ix in lo[1]:hi[1] + lix = ix + cell_center_x = 2*ix + 1 - sz[1] + li = lix + liy + liz + cell_center = SVector{3, F}(cell_center_x, cell_center_y, cell_center_z) + @inline ball_intersects_biunit_cube(pos_norm - cell_center, r_cells) || continue + cs = pcl.cells[begin + li] + for j in 1:cs.len + entry = cs.list[j] + diff = pos - entry.pos + d2 = diff ⋅ diff + d2 ≤ cutoff2 || continue + out, cont = f(entry, out) + cont || return out + end + end + end + end + out +end diff --git a/src/util.jl b/src/util.jl new file mode 100644 index 0000000..d77da38 --- /dev/null +++ b/src/util.jl @@ -0,0 +1,31 @@ +clamp01nan(x) = ifelse(isnan(x), zero(x), clamp(x,zero(x),one(x))) + +function relu(x::T)::T where T + (x > zero(x) ? x : zero(x)) +end + +""" + ball_intersects_unit_cube(center, radius) -> Bool + +Check if a ball (sphere) intersects with the unit cube centered at the origin, +extending from -0.5 to 0.5 in each axis. +""" +function ball_intersects_unit_cube(center, radius) + # Squared distance from sphere center to nearest point on [-0.5, 0.5]^3 + h = oftype(radius, 1//2) + d2 = relu(abs(center[1]) - h)^2 + relu(abs(center[2]) - h)^2 + relu(abs(center[3]) - h)^2 + d2 <= radius * radius +end + +""" + ball_intersects_biunit_cube(center, radius) -> Bool + +Check if a ball (sphere) intersects with the biunit cube centered at the origin, +extending from -1 to 1 in each axis. +""" +function ball_intersects_biunit_cube(center, radius) + # Squared distance from sphere center to nearest point on [-1, 1]^3 + h = oneunit(radius) + d2 = relu(abs(center[1]) - h)^2 + relu(abs(center[2]) - h)^2 + relu(abs(center[3]) - h)^2 + d2 <= radius * radius +end diff --git a/test/runtests.jl b/test/runtests.jl index 9ba85fd..88eeae2 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,10 +1,12 @@ +include("test-util.jl") +include("test-mindistance.jl") -include("test_mindistance.jl") +include("test-pointcelllist.jl") -include("test_naive.jl") +# include("test_naive.jl") -include("test_painter-basic.jl") +# include("test_painter-basic.jl") -include("test_painter.jl") +# include("test_painter.jl") diff --git a/test/test-mindistance.jl b/test/test-mindistance.jl new file mode 100644 index 0000000..c927374 --- /dev/null +++ b/test/test-mindistance.jl @@ -0,0 +1,77 @@ +# Test min distance functions + +using JuMP +using StaticArrays +using LinearAlgebra +using SimplexCellLists +using Random +using Test +using Clarabel +using SimplexCellLists: dist_sqr, dist_sqr_r_s_t + + +function refDist2(points1,points2) + model = Model(Clarabel.Optimizer) + set_optimizer_attribute(model, "tol_gap_rel", 1E-12) + set_optimizer_attribute(model, "tol_gap_abs", 0.0) + set_silent(model) + n1 = length(points1) + n2 = length(points2) + dims = length(points1[1]) + @assert length(points1[1])==length(points2[1]) + @variable(model, s[i = 1:n1] ≥ 0) + @variable(model, t[i = 1:n2] ≥ 0) + @constraint(model, sum(s[i] for i in 1:n1) == 1) + @constraint(model, sum(t[i] for i in 1:n2) == 1) + @expression(model, pt1[i = 1:dims], sum(s[j]*points1[j][i] for j in 1:n1)) + @expression(model, pt2[i = 1:dims], sum(t[j]*points2[j][i] for j in 1:n2)) + @expression(model, d[i = 1:dims], pt1[i]-pt2[i]) + @objective(model, Min, sum(d[i]*d[i] for i in 1:dims)) + optimize!(model) + objective_value(model) +end + +Random.seed!(1234) + +function distance_tests(as, bs) + for i in eachindex(as, bs) + local a = as[i] + local b = bs[i] + local d2, r, s, t = dist_sqr_r_s_t(a, b) + @test refDist2(a, b) ≈ d2 atol = 1E-10 rtol = 1E-11 + @test d2 ≈ dist_sqr(a, b) atol = 1E-10 rtol = 1E-11 + @test r ⋅ r ≈ d2 atol = 1E-10 rtol = 1E-11 + local s_all = [one(eltype(s)) - sum(s); s;] + local t_all = [one(eltype(t)) - sum(t); t;] + @test norm(r - (sum(b .* t_all) - sum(a .* s_all))) ≤ 1E-12 + @test all(≥(0), s) + @test all(≥(0), t) + @test sum(s) ≤ 1 + @test sum(t) ≤ 1 + end +end + +@testset "distance $N to $M" for N in 1:3, M in 1:3 + trials = 1000 + as = rand(SVector{N,SVector{3,Float64}},trials) + bs = rand(SVector{M,SVector{3,Float64}},trials) + distance_tests(as, bs) +end + +@testset "line to triangle edge case" begin + a = SA[ + SA[0.7962602087717146, 0.7678176789137129, 0.33365738129157096], + SA[0.8787232878884521, 0.22723122900042247, 0.29743853859367475], + ] + b = SA[ + SA[0.32244253613423346, 0.07951105154799964, 0.3048568965486481], + SA[0.48609530074630236, 0.14522371134511514, 0.7921987380603223], + SA[0.1937122261241142, 0.9991063549453244, 0.3332458906215262], + ] + ref_d2min = refDist2(a,b) + d2min = SimplexCellLists.dist_sqr(a,b) + # This test was broken due to an issue with Clarabel.jl + # See https://github.com/oxfordcontrol/Clarabel.jl/issues/103 + # This issue was resolved in https://github.com/oxfordcontrol/Clarabel.jl/pull/114 + @test ref_d2min ≈ d2min atol = 1E-11 rtol = 1E-11 +end diff --git a/test/test-pointcelllist.jl b/test/test-pointcelllist.jl new file mode 100644 index 0000000..52aa462 --- /dev/null +++ b/test/test-pointcelllist.jl @@ -0,0 +1,312 @@ +using SimplexCellLists +using LinearAlgebra +using StaticArrays +using Test + +# Helper to collect all entries from a PointCellList as (ix,iy,iz) => Vector{CellPointEntry} +function collect_entries(pcl::PointCellList{T,F}) where {T,F} + result = Dict{NTuple{3,Int}, Vector{CellPointEntry{T,F}}}() + for iz in 0:pcl.size[3]-1, iy in 0:pcl.size[2]-1, ix in 0:pcl.size[1]-1 + # li = SA[ix,iy,iz] ⋅ pcl.strides + cs = reshape(pcl.cells, pcl.size[1], pcl.size[2], pcl.size[3])[ix+1,iy+1,iz+1] + if cs.len > 0 + result[(ix,iy,iz)] = collect(view(cs.list, 1:cs.len)) + end + end + result +end + +""" +Brute-force O(n) version of `map_nearby_points` for testing. +""" +function map_nearby_points_naive( + f, + positions::AbstractVector{SVector{3,Float32}}, + point_ids::AbstractVector{Int64}, + pos::SVector{3,Float32}, + cutoff::Float32, + out, + ) + cutoff ≤ 0 && return out + cutoff2 = cutoff * cutoff + for i in eachindex(positions, point_ids) + diff = pos - positions[i] + dist2 = diff ⋅ diff + if dist2 ≤ cutoff2 + out, cont = f(CellPointEntry{Int64,Float32}(positions[i], point_ids[i]), out) + cont || return out + end + end + out +end + +total_entries(pcl::PointCellList) = sum(pcl.cells[i].len for i in 1:prod(pcl.size)) + +@testset "pointcelllist" begin + # Grid (4,4,4) with spacing 2.0 is centered at origin: + # x: [-4, 4), y: [-4, 4), z: [-4, 4) + # Cell (0,0,0) covers [-4,-2) × [-4,-2) × [-4,-2) + # Cell (2,2,2) covers [0,2) × [0,2) × [0,2) + # Cell (3,3,3) covers [2,4) × [2,4) × [2,4) + + @testset "invalid and degenerate sizes" begin + @test_throws ArgumentError PointCellList{Int64,Float32}((-1,4,4), 2.0f0) + # A zero-size axis makes an empty grid: queries find nothing. + pcl = PointCellList{Int64,Float32}((0,4,4), 2.0f0) + count = map_nearby_points(pcl, SA[0.5f0, 0.5f0, 0.5f0], 1.0f0, 0) do entry, out + out + 1, true + end + @test count == 0 + end + + @testset "single point" begin + pcl = PointCellList{Int64, Float32}((4,4,4), 2.0f0) + cell_point_add!(pcl, SA[0.5f0, 0.5f0, 0.5f0], Int64(1)) + @test total_entries(pcl) == 1 + entries = collect_entries(pcl) + @test haskey(entries, (2,2,2)) + e = entries[(2,2,2)][1] + @test e.point_id == 1 + @test e.pos == SA[0.5f0, 0.5f0, 0.5f0] + end + + @testset "point in corner cell" begin + pcl = PointCellList{Int64,Float32}((4,4,4), 2.0f0) + cell_point_add!(pcl, SA[-3.5f0, -3.5f0, -3.5f0], Int64(2)) + @test total_entries(pcl) == 1 + entries = collect_entries(pcl) + @test haskey(entries, (0,0,0)) + e = entries[(0,0,0)][1] + @test e.point_id == 2 + end + + @testset "point in last cell" begin + pcl = PointCellList{Int64,Float32}((4,4,4), 2.0f0) + cell_point_add!(pcl, SA[3.5f0, 3.5f0, 3.5f0], Int64(3)) + @test total_entries(pcl) == 1 + entries = collect_entries(pcl) + @test haskey(entries, (3,3,3)) + end + + @testset "multiple points in same cell" begin + pcl = PointCellList{Int64,Float32}((4,4,4), 2.0f0) + cell_point_add!(pcl, SA[0.5f0, 0.5f0, 0.5f0], Int64(1)) + cell_point_add!(pcl, SA[1.5f0, 1.5f0, 1.5f0], Int64(2)) + @test total_entries(pcl) == 2 + entries = collect_entries(pcl) + @test issetequal(entries[(2,2,2)], [ + CellPointEntry{Int64,Float32}(SA[0.5f0, 0.5f0, 0.5f0], Int64(1)), + CellPointEntry{Int64,Float32}(SA[1.5f0, 1.5f0, 1.5f0], Int64(2)), + ]) + end + + @testset "points in different cells" begin + pcl = PointCellList{Int64,Float32}((4,4,4), 2.0f0) + cell_point_add!(pcl, SA[-3.5f0, -3.5f0, -3.5f0], Int64(1)) + cell_point_add!(pcl, SA[3.5f0, 3.5f0, 3.5f0], Int64(2)) + @test total_entries(pcl) == 2 + entries = collect_entries(pcl) + @test haskey(entries, (0,0,0)) + @test haskey(entries, (3,3,3)) + @test entries[(0,0,0)][1].point_id == 1 + @test entries[(3,3,3)][1].point_id == 2 + end + + @testset "point on cell boundary" begin + pcl = PointCellList{Int64,Float32}((4,4,4), 2.0f0) + # x=0 is boundary between cells 1 and 2; floor(0/2 + 2) = floor(2.0) = 2 + cell_point_add!(pcl, SA[0.0f0, -3.5f0, -3.5f0], Int64(1)) + @test total_entries(pcl) == 1 + entries = collect_entries(pcl) + # Cells are half-open [lo, hi), so a point exactly on a boundary + # deterministically belongs to the upper cell. + @test haskey(entries, (2,0,0)) + end + + @testset "non-cubic grid" begin + # (2,4,6) grid with spacing 1.0 + # x: [-1,1), y: [-2,2), z: [-3,3) + pcl = PointCellList{Int64,Float32}((2,4,6), 1.0f0) + cell_point_add!(pcl, SA[-0.5f0, -1.5f0, -2.5f0], Int64(1)) # cell (0,0,0) + cell_point_add!(pcl, SA[0.5f0, 1.5f0, 2.5f0], Int64(2)) # cell (1,3,5) + entries = collect_entries(pcl) + @test haskey(entries, (0,0,0)) + @test haskey(entries, (1,3,5)) + @test total_entries(pcl) == 2 + end + + @testset "many points in one cell" begin + pcl = PointCellList{Int64,Float32}((4,4,4), 2.0f0) + n = 100 + for i in 1:n + cell_point_add!(pcl, SA[0.5f0, 0.5f0, 0.5f0], Int64(i)) + end + entries = collect_entries(pcl) + es = entries[(2,2,2)] + @test length(es) == n + @test issetequal(map(e->e.point_id, es), 1:n) + @test total_entries(pcl) == n + end + + @testset "multiple points same position different IDs" begin + pcl = PointCellList{Int64,Float32}((4,4,4), 2.0f0) + pos = SA[0.5f0, 0.5f0, 0.5f0] + cell_point_add!(pcl, pos, Int64(10)) + cell_point_add!(pcl, pos, Int64(20)) + count = map_nearby_points(pcl, pos, 0.1f0, 0) do entry, out + out + 1, true + end + @test count == 2 + end + + @testset "cell_point_clear!" begin + pcl = PointCellList{Int64,Float32}((4,4,4), 2.0f0) + cell_point_add!(pcl, SA[0.5f0, 0.5f0, 0.5f0], Int64(1)) + cell_point_add!(pcl, SA[-3.5f0, -3.5f0, -3.5f0], Int64(2)) + @test total_entries(pcl) == 2 + cell_point_clear!(pcl) + @test total_entries(pcl) == 0 + end + + @testset "points outside the grid are clamped to boundary cells" begin + pcl = PointCellList{Int64,Float32}((4,4,4), 2.0f0) + # Grid covers [-4,4)^3 + cell_point_add!(pcl, SA[100.0f0, 0.5f0, 0.5f0], Int64(1)) # beyond +x + cell_point_add!(pcl, SA[-100.0f0, 0.5f0, 0.5f0], Int64(2)) # beyond -x + cell_point_add!(pcl, SA[1.0f10, -1.0f10, 3.0f0], Int64(3)) # far outside in x and y + cell_point_add!(pcl, SA[4.0f0, 0.5f0, 0.5f0], Int64(4)) # exactly on the upper bound + cell_point_add!(pcl, SA[-4.0f0, 0.5f0, 0.5f0], Int64(5)) # exactly on the lower bound + @test total_entries(pcl) == 5 + entries = collect_entries(pcl) + @test map(e->e.point_id, entries[(3,2,2)]) == [1, 4] + @test map(e->e.point_id, entries[(0,2,2)]) == [2, 5] + @test entries[(3,0,3)][1].point_id == 3 + end + + @testset "map_nearby_points" begin + @testset "empty" begin + pcl = PointCellList{Int64,Float32}((4,4,4), 2.0f0) + count = map_nearby_points(pcl, SA[0.5f0, 0.5f0, 0.5f0], 0.5f0, 0) do entry, out + out + 1, true + end + @test count == 0 + end + + @testset "single point found" begin + pcl = PointCellList{Int64,Float32}((4,4,4), 2.0f0) + cell_point_add!(pcl, SA[0.5f0, 0.5f0, 0.5f0], Int64(1)) + count = map_nearby_points(pcl, SA[0.5f0, 0.5f0, 0.5f0], 0.5f0, 0) do entry, out + out + 1, true + end + @test count == 1 + end + + @testset "no matches - outside cutoff" begin + pcl = PointCellList{Int64,Float32}((4,4,4), 2.0f0) + cell_point_add!(pcl, SA[0.5f0, 0.5f0, 0.5f0], Int64(1)) + count = map_nearby_points(pcl, SA[-3.0f0, -3.0f0, -3.0f0], 0.5f0, 0) do entry, out + out + 1, true + end + @test count == 0 + end + + @testset "early exit" begin + pcl = PointCellList{Int64,Float32}((4,4,4), 2.0f0) + cell_point_add!(pcl, SA[0.5f0, 0.5f0, 0.5f0], Int64(1)) + cell_point_add!(pcl, SA[0.6f0, 0.5f0, 0.5f0], Int64(2)) + count = map_nearby_points(pcl, SA[0.5f0, 0.5f0, 0.5f0], 1.0f0, 0) do entry, out + out + 1, false + end + @test count == 1 + end + + @testset "point across cell boundary within cutoff" begin + pcl = PointCellList{Int64,Float32}((4,4,4), 2.0f0) + # Point just inside cell (1,2,2), query just inside cell (2,2,2) + cell_point_add!(pcl, SA[-0.1f0, 0.5f0, 0.5f0], Int64(1)) + count = map_nearby_points(pcl, SA[0.1f0, 0.5f0, 0.5f0], 0.5f0, 0) do entry, out + out + 1, true + end + @test count == 1 + end + + @testset "query from outside grid finds nearby point" begin + pcl = PointCellList{Int64,Float32}((4,4,4), 2.0f0) + # Grid covers [-4,4). Point near edge. + cell_point_add!(pcl, SA[3.5f0, 3.5f0, 3.5f0], Int64(1)) + # Query from outside grid, distance = 1.0 + count = map_nearby_points(pcl, SA[4.5f0, 3.5f0, 3.5f0], 2.0f0, 0) do entry, out + out + 1, true + end + @test count == 1 + end + + @testset "query from outside grid finds points also outside grid" begin + pcl = PointCellList{Int64,Float32}((4,4,4), 2.0f0) + # Grid covers [-4,4). Point and query are both beyond the +x face. + cell_point_add!(pcl, SA[10.0f0, 0.5f0, 0.5f0], Int64(1)) + count = map_nearby_points(pcl, SA[11.0f0, 0.5f0, 0.5f0], 2.0f0, 0) do entry, out + out + 1, true + end + @test count == 1 + # The same query must not return an outside point beyond the cutoff. + cell_point_add!(pcl, SA[11.0f0, 0.5f0, 30.0f0], Int64(2)) + count = map_nearby_points(pcl, SA[11.0f0, 0.5f0, 0.5f0], 2.0f0, 0) do entry, out + out + 1, true + end + @test count == 1 + end + + @testset "query near grid edge with cutoff extending outside" begin + pcl = PointCellList{Int64,Float32}((4,4,4), 2.0f0) + # Points near the low-x boundary of the grid + cell_point_add!(pcl, SA[-3.5f0, 0.5f0, 0.5f0], Int64(1)) + # Query with large cutoff that extends past grid boundary on -x side + count = map_nearby_points(pcl, SA[-2.5f0, 0.5f0, 0.5f0], 3.0f0, 0) do entry, out + out + 1, true + end + @test count == 1 + end + + @testset "zero cutoff returns out" begin + pcl = PointCellList{Int64,Float32}((4,4,4), 2.0f0) + cell_point_add!(pcl, SA[0.5f0, 0.5f0, 0.5f0], Int64(1)) + result = map_nearby_points(pcl, SA[0.5f0, 0.5f0, 0.5f0], 0.0f0, 42) do entry, out + out + 1, true + end + @test result == 42 + end + + @testset "brute force comparison" begin + for trial in 1:10000 + pcl = PointCellList{Int64,Float32}((10,10,10), 2.0f0) + # Grid covers [-10,10)^3, but points and queries are sampled in + # [-12,12)^3 so some fall outside the grid and get clamped. + n_pts = 500 + positions = [SVector{3,Float32}(rand(Float32, 3) .* 24f0 .- 12f0) for _ in 1:n_pts] + for i in 1:n_pts + cell_point_add!(pcl, positions[i], Int64(i)) + end + + function collect_id(entry, out) + push!(out, entry.point_id) + out, true + end + for _ in 1:20 + cutoff = Float32(rand()) * 8f0 + cutoff2 = cutoff * cutoff + q = SVector{3,Float32}(rand(Float32, 3) .* 24f0 .- 12f0) + cell_ids = map_nearby_points(collect_id, pcl, q, cutoff, Set{Int64}()) + brute_ids = map_nearby_points_naive(collect_id, positions, Int64(1):n_pts, q, cutoff, Set{Int64}()) + # Any id in one set but not the other must be right on the cutoff boundary. + for id in symdiff(cell_ids, brute_ids) + diff = q - positions[id] + d2 = diff ⋅ diff + @test d2 ≈ cutoff2 rtol=1e-6 + end + end + end + end + end +end +nothing diff --git a/test/test-util.jl b/test/test-util.jl new file mode 100644 index 0000000..282c430 --- /dev/null +++ b/test/test-util.jl @@ -0,0 +1,151 @@ +using StaticArrays +using LinearAlgebra +using SimplexCellLists +using Test + +@testset "ball_intersects_unit_cube" begin + buc = SimplexCellLists.ball_intersects_unit_cube + @testset "center at origin" begin + # Any positive radius intersects + @test buc(SA[0.0, 0.0, 0.0], 0.0) + @test buc(SA[0.0, 0.0, 0.0], 0.1) + @test buc(SA[0.0, 0.0, 0.0], 10.0) + end + @testset "center inside cube" begin + @test buc(SA[0.4, 0.4, 0.4], 0.0) + @test buc(SA[-0.3, 0.2, -0.1], 0.0) + @test buc(SA[0.5, 0.5, 0.5], 0.0) # on corner, still touching + @test buc(SA[0.5, 0.0, 0.0], 0.0) # on face, still touching + end + @testset "face separation" begin + # Ball center along +x axis, only x exceeds cube + @test buc(SA[0.7, 0.0, 0.0], 0.21) # distance = 0.2, radius > distance + @test !buc(SA[0.7, 0.0, 0.0], 0.19) # distance = 0.2, radius < distance + # Along -y axis + @test buc(SA[0.0, -1.0, 0.0], 0.51) + @test !buc(SA[0.0, -1.0, 0.0], 0.49) + # Along +z axis + @test buc(SA[0.0, 0.0, 0.8], 0.31) + @test !buc(SA[0.0, 0.0, 0.8], 0.29) + end + @testset "edge separation" begin + # Center near an edge: two coordinates exceed cube + # e.g. (+x, +y) edge, center at (1, 1, 0) + # nearest point on cube is (0.5, 0.5, 0), dist = sqrt(0.25 + 0.25) = sqrt(0.5) ≈ 0.7071 + @test buc(SA[1.0, 1.0, 0.0], 0.71) + @test !buc(SA[1.0, 1.0, 0.0], 0.70) + # (-x, +z) edge + @test buc(SA[-1.0, 0.0, 1.0], 0.71) + @test !buc(SA[-1.0, 0.0, 1.0], 0.70) + end + @testset "vertex separation" begin + # Center near vertex: all three coordinates exceed cube + # center at (1, 1, 1), nearest vertex is (0.5, 0.5, 0.5), dist = sqrt(0.75) ≈ 0.8660 + @test buc(SA[1.0, 1.0, 1.0], 0.87) + @test !buc(SA[1.0, 1.0, 1.0], 0.86) + # Negative vertex + @test buc(SA[-1.0, -1.0, -1.0], 0.87) + @test !buc(SA[-1.0, -1.0, -1.0], 0.86) + end + @testset "exact boundary" begin + # Exactly touching: d == r, should intersect (<=) + @test buc(SA[1.0, 0.0, 0.0], 0.5) # face touch + @test buc(SA[1.0, 1.0, 0.0], sqrt(0.5)) # edge touch + # vertex touch: sqrt(0.75)^2 != 0.75 due to fp rounding, + # so test just inside and just outside instead + @test buc(SA[1.0, 1.0, 1.0], sqrt(0.75) + eps()) + @test !buc(SA[1.0, 1.0, 1.0], sqrt(0.75) - 1e-10) + end + @testset "large radius" begin + @test buc(SA[100.0, 100.0, 100.0], 200.0) + @test !buc(SA[100.0, 100.0, 100.0], 170.0) + end + @testset "symmetry" begin + # All 8 octant corners should give same result + for sx in (-1, 1), sy in (-1, 1), sz in (-1, 1) + c = SA[sx * 0.8, sy * 0.9, sz * 0.7] + @test buc(c, 0.5) == buc(abs.(c), 0.5) + end + end + @testset "Float32" begin + @test buc(SA[0.7f0, 0.0f0, 0.0f0], 0.21f0) + @test !buc(SA[0.7f0, 0.0f0, 0.0f0], 0.19f0) + end +end + +@testset "ball_intersects_biunit_cube" begin + bbc = SimplexCellLists.ball_intersects_biunit_cube + @testset "center at origin" begin + # Any positive radius intersects + @test bbc(SA[0.0, 0.0, 0.0], 0.0) + @test bbc(SA[0.0, 0.0, 0.0], 0.1) + @test bbc(SA[0.0, 0.0, 0.0], 10.0) + end + @testset "center inside cube" begin + @test bbc(SA[0.8, 0.8, 0.8], 0.0) + @test bbc(SA[-0.6, 0.4, -0.2], 0.0) + @test bbc(SA[1.0, 1.0, 1.0], 0.0) # on corner, still touching + @test bbc(SA[1.0, 0.0, 0.0], 0.0) # on face, still touching + end + @testset "face separation" begin + # Ball center along +x axis, only x exceeds cube + @test bbc(SA[1.4, 0.0, 0.0], 0.41) # distance = 0.4, radius > distance + @test !bbc(SA[1.4, 0.0, 0.0], 0.39) # distance = 0.4, radius < distance + # Along -y axis + @test bbc(SA[0.0, -2.0, 0.0], 1.01) + @test !bbc(SA[0.0, -2.0, 0.0], 0.99) + # Along +z axis + @test bbc(SA[0.0, 0.0, 1.6], 0.61) + @test !bbc(SA[0.0, 0.0, 1.6], 0.59) + end + @testset "edge separation" begin + # Center near an edge: two coordinates exceed cube + # e.g. (+x, +y) edge, center at (2, 2, 0) + # nearest point on cube is (1, 1, 0), dist = sqrt(1 + 1) = sqrt(2) ≈ 1.4142 + @test bbc(SA[2.0, 2.0, 0.0], 1.42) + @test !bbc(SA[2.0, 2.0, 0.0], 1.41) + # (-x, +z) edge + @test bbc(SA[-2.0, 0.0, 2.0], 1.42) + @test !bbc(SA[-2.0, 0.0, 2.0], 1.41) + end + @testset "vertex separation" begin + # Center near vertex: all three coordinates exceed cube + # center at (2, 2, 2), nearest vertex is (1, 1, 1), dist = sqrt(3) ≈ 1.7321 + @test bbc(SA[2.0, 2.0, 2.0], 1.74) + @test !bbc(SA[2.0, 2.0, 2.0], 1.73) + # Negative vertex + @test bbc(SA[-2.0, -2.0, -2.0], 1.74) + @test !bbc(SA[-2.0, -2.0, -2.0], 1.73) + end + @testset "exact boundary" begin + # Exactly touching: d == r, should intersect (<=) + @test bbc(SA[2.0, 0.0, 0.0], 1.0) # face touch + @test bbc(SA[2.0, 2.0, 0.0], sqrt(2.0)) # edge touch + # vertex touch: sqrt(3)^2 != 3 due to fp rounding, + # so test just inside and just outside instead + @test bbc(SA[2.0, 2.0, 2.0], sqrt(3.0) + 2*eps()) + @test !bbc(SA[2.0, 2.0, 2.0], sqrt(3.0) - 1e-10) + end + @testset "large radius" begin + @test bbc(SA[100.0, 100.0, 100.0], 200.0) + @test !bbc(SA[100.0, 100.0, 100.0], 170.0) + end + @testset "symmetry" begin + # All 8 octant corners should give same result + for sx in (-1, 1), sy in (-1, 1), sz in (-1, 1) + c = SA[sx * 1.6, sy * 1.8, sz * 1.4] + @test bbc(c, 1.0) == bbc(abs.(c), 1.0) + end + end + @testset "Float32" begin + @test bbc(SA[1.4f0, 0.0f0, 0.0f0], 0.41f0) + @test !bbc(SA[1.4f0, 0.0f0, 0.0f0], 0.39f0) + end + @testset "agrees with unit cube version at half scale" begin + for trial in 1:1000 + c = SVector{3,Float64}(randn(3)) * 2.0 + r = abs(randn()) * 1.5 + @test bbc(c, r) == SimplexCellLists.ball_intersects_unit_cube(c/2, r/2) + end + end +end diff --git a/test/test_mindistance.jl b/test/test_mindistance.jl deleted file mode 100644 index 4f67f8a..0000000 --- a/test/test_mindistance.jl +++ /dev/null @@ -1,180 +0,0 @@ -# Test min distance functions - -using JuMP -using StaticArrays -using LinearAlgebra -using SimplexCellLists -using Random -using Test -using Clarabel - - -function refDist2(points1,points2) - model = Model(Clarabel.Optimizer) - set_optimizer_attribute(model, "tol_gap_rel", 1E-12) - set_optimizer_attribute(model, "tol_gap_abs", 0.0) - set_silent(model) - n1 = length(points1) - n2 = length(points2) - dims = length(points1[1]) - @assert length(points1[1])==length(points2[1]) - @variable(model, s[i = 1:n1] ≥ 0) - @variable(model, t[i = 1:n2] ≥ 0) - @constraint(model, sum(s[i] for i in 1:n1) == 1) - @constraint(model, sum(t[i] for i in 1:n2) == 1) - @expression(model, pt1[i = 1:dims], sum(s[j]*points1[j][i] for j in 1:n1)) - @expression(model, pt2[i = 1:dims], sum(t[j]*points2[j][i] for j in 1:n2)) - @expression(model, d[i = 1:dims], pt1[i]-pt2[i]) - @objective(model, Min, sum(d[i]*d[i] for i in 1:dims)) - optimize!(model) - objective_value(model) -end - -Random.seed!(1234) - -@testset "point to point" begin - N = 1000 - a = rand(SVector{1,SVector{3,Float64}},N) - b = rand(SVector{1,SVector{3,Float64}},N) - for i in 1:N - @test refDist2(a[i],b[i]) ≈ SimplexCellLists.dist2PointPoint(a[i],b[i]) - end -end - -@testset "point to line" begin - N = 1000 - a = rand(SVector{1,SVector{3,Float64}},N) - b = rand(SVector{2,SVector{3,Float64}},N) - for i in 1:N - ref_d2min = refDist2(a[i],b[i]) - d2min = SimplexCellLists.dist2PointLine(a[i],b[i]) - d2min_t, tmin = SimplexCellLists.dist2PointLine_t(a[i],b[i]) - @test 0 ≤ d2min_t - @test d2min ≈ d2min_t - @test 0 ≤ d2min - @test ref_d2min ≈ d2min - @test 0 ≤ tmin ≤ 1 - s_full = SA[1.0] - t_full = SA[1.0-tmin, tmin] - r = (sum(a[i] .* s_full) - sum(b[i] .* t_full)) - @test d2min ≈ r⋅r - end -end - -@testset "line to line" begin - N = 1000 - a = rand(SVector{2,SVector{3,Float64}},N) - b = rand(SVector{2,SVector{3,Float64}},N) - for i in 1:N - ref_d2min = refDist2(a[i],b[i]) - d2min = SimplexCellLists.dist2LineLine(a[i],b[i]) - d2min_s_t, smin, tmin = SimplexCellLists.dist2LineLine_s_t(a[i],b[i]) - @test 0 ≤ d2min_s_t - @test d2min ≈ d2min_s_t - @test 0 ≤ d2min - @test ref_d2min ≈ d2min - @test 0 ≤ tmin ≤ 1 - s_full = SA[1.0-smin, smin] - t_full = SA[1.0-tmin, tmin] - r = (sum(a[i] .* s_full) - sum(b[i] .* t_full)) - @test d2min ≈ r⋅r - end -end - -@testset "point to triangle" begin - N = 1000 - a = rand(SVector{1,SVector{3,Float64}},N) - b = rand(SVector{3,SVector{3,Float64}},N) - for i in 1:N - ref_d2min = refDist2(a[i],b[i]) - d2min = SimplexCellLists.dist2PointTriangle(a[i],b[i]) - d2min_s_t, tmin = SimplexCellLists.dist2PointTriangle_t(a[i],b[i]) - @test 0 ≤ d2min_s_t - @test d2min ≈ d2min_s_t - @test 0 ≤ d2min - @test ref_d2min ≈ d2min - @test 0 ≤ tmin[1] - @test 0 ≤ tmin[2] - @test sum(tmin) ≤ 1 - s_full = SA[1.0] - t_full = SA[1.0-sum(tmin), tmin...] - r = (sum(a[i] .* s_full) - sum(b[i] .* t_full)) - @test d2min ≈ r⋅r - end -end - -@testset "line to triangle edge case" begin - a = SVector{3, Float64}[ - [0.7962602087717146, 0.7678176789137129, 0.33365738129157096], - [0.8787232878884521, 0.22723122900042247, 0.29743853859367475], - ] - b = SVector{3, Float64}[ - [0.32244253613423346, 0.07951105154799964, 0.3048568965486481], - [0.48609530074630236, 0.14522371134511514, 0.7921987380603223], - [0.1937122261241142, 0.9991063549453244, 0.3332458906215262], - ] - ref_d2min = refDist2(a,b) - d2min = SimplexCellLists.dist2LineTriangle(a,b) - # This test was broken due to an issue with Clarabel.jl - # See https://github.com/oxfordcontrol/Clarabel.jl/issues/103 - # This issue was resolved in https://github.com/oxfordcontrol/Clarabel.jl/pull/114 - @test ref_d2min ≈ d2min atol = 1E-11 rtol = 1E-11 -end - - -@testset "line to triangle" begin - N = 1000 - a = rand(SVector{2,SVector{3,Float64}},N) - b = rand(SVector{3,SVector{3,Float64}},N) - for i in 1:N - ref_d2min = refDist2(a[i],b[i]) - d2min = SimplexCellLists.dist2LineTriangle(a[i],b[i]) - d2min_s_t, smin, tmin = SimplexCellLists.dist2LineTriangle_s_t(a[i],b[i]) - @test 0 ≤ d2min_s_t - @test d2min ≈ d2min_s_t atol = 1E-11 - @test 0 ≤ d2min - if abs(ref_d2min - d2min) > 1E-11 - @show abs(ref_d2min - d2min) - @show d2min - @show ref_d2min - @show a[i] - @show b[i] - println() - end - @test ref_d2min ≈ d2min atol = 1E-11 - @test 0 ≤ smin[1] - @test sum(smin) ≤ 1 - @test 0 ≤ tmin[1] - @test 0 ≤ tmin[2] - @test sum(tmin) ≤ 1 - s_full = SA[1.0-sum(smin), smin...] - t_full = SA[1.0-sum(tmin), tmin...] - r = (sum(a[i] .* s_full) - sum(b[i] .* t_full)) - @test d2min ≈ r⋅r atol = 1E-11 - end -end - -@testset "triangle to triangle" begin - N = 1000 - a = rand(SVector{3,SVector{3,Float64}},N) - b = rand(SVector{3,SVector{3,Float64}},N) - for i in 1:N - ref_d2min = refDist2(a[i],b[i]) - d2min = SimplexCellLists.dist2TriangleTriangle(a[i],b[i]) - d2min_s_t, smin, tmin = SimplexCellLists.dist2TriangleTriangle_s_t(a[i],b[i]) - @test 0 ≤ d2min_s_t - @test d2min ≈ d2min_s_t atol = 1E-11 - @test 0 ≤ d2min - @test ref_d2min ≈ d2min atol = 1E-11 - @test 0 ≤ smin[1] - @test 0 ≤ smin[2] - @test sum(smin) ≤ 1 - @test 0 ≤ tmin[1] - @test 0 ≤ tmin[2] - @test sum(tmin) ≤ 1 - s_full = SA[1.0-sum(smin), smin...] - t_full = SA[1.0-sum(tmin), tmin...] - r = (sum(a[i] .* s_full) - sum(b[i] .* t_full)) - @test d2min ≈ r⋅r atol = 1E-11 - end -end \ No newline at end of file diff --git a/test/test_naive.jl b/test/test_naive.jl deleted file mode 100644 index 17252c7..0000000 --- a/test/test_naive.jl +++ /dev/null @@ -1,156 +0,0 @@ -# Test Naive -using StaticArrays -using LinearAlgebra -using SimplexCellLists -using Random -using Test -using ArgCheck -using Rotations - -include("common.jl") - - -@testset "constructor" begin - empty_s = SimplexCellLists.Naive(0, 0, 0) - onepointgroup_s = SimplexCellLists.Naive(1, 0, 0) - onelinegroup_s = SimplexCellLists.Naive(0, 1, 0) - onetrianglegroup_s = SimplexCellLists.Naive(0, 0, 1) - oneallgroup_s = SimplexCellLists.Naive(1, 1, 1) - tenallgroup_s = SimplexCellLists.Naive(10, 10, 10) -end -@testset "setElements!" begin - empty_s = SimplexCellLists.Naive(0, 0, 0) - setElements!(empty_s, [], [], []) - - onepointgroup_s = SimplexCellLists.Naive(1, 0, 0) - setElements!(onepointgroup_s, [[[[1,2,3]],[[3,4,6]]]], [], []) - - oneallgroup_s = SimplexCellLists.Naive(1, 1, 1) - setElements!(oneallgroup_s, - [[[[1,2,3]],[[3,4,6]]]], - [[[[1,2,3],[7,2,5]],[[3,4,6],[3,9,6]]]], - [[[[1,2,3],[7,2,5],[7,9,5]],[[3,4,6],[3,9,6],[1,2,3]]]], - ) - - twoallgroup_s = SimplexCellLists.Naive(2, 2, 2) - setElements!(twoallgroup_s, - [[[[1,2,3]],[[3,4,6]]],[]], - [[],[[[1,2,3],[7,2,5]],[[3,4,6],[3,9,6]]]], - [ - [[[1,2,3],[7,2,5],[7,9,5]],[[3,4,6],[3,9,6],[1,2,3]]], - [[[1,2,3],[7,2,5],[7,9,5]],[[3,4,6],[3,9,6],[1,2,3]]], - ], - ) -end -@testset "addElement!" begin - onepointgroup_s = SimplexCellLists.Naive(1, 0, 0) - setElements!(onepointgroup_s, [[[[1,2,3]],[[3,4,6]]]], [], []) - @test 3 == addElement!(onepointgroup_s, 1, SA[SA_F32[3,4,6]]) - @test 4 == addElement!(onepointgroup_s, 1, SA[SA_F32[3,4,6]]) - - oneallgroup_s = SimplexCellLists.Naive(1, 1, 1) - setElements!(oneallgroup_s, - [[[[1,2,3]],[[3,4,6]]]], - [[[[1,2,3],[7,2,5]],[[3,4,6],[3,9,6]]]], - [[[[1,2,3],[7,2,5],[7,9,5]],[[3,4,6],[3,9,6],[1,2,3]]]], - ) - @test 3 == addElement!(oneallgroup_s, 1, SA[SA_F32[3,4,6]]) - @test 3 == addElement!(oneallgroup_s, 1, SA[SA_F32[3,4,6], SA_F32[4,4,6]]) - @test 3 == addElement!(oneallgroup_s, 1, SA[SA_F32[3,4,6], SA_F32[4,4,6], SA_F32[5,5,6]]) - - twoallgroup_s = SimplexCellLists.Naive(2, 2, 2) - setElements!(twoallgroup_s, - [[[[1,2,3]],[[3,4,6]]],[]], - [[],[[[1,2,3],[7,2,5]],[[3,4,6],[3,9,6]]]], - [[],[]], - ) - @test 1 == addElement!(twoallgroup_s, 2, SA[SA_F32[3,4,6]]) - @test 3 == addElement!(twoallgroup_s, 1, SA[SA_F32[3,4,6]]) - @test 3 == addElement!(twoallgroup_s, 2, SA[SA_F32[3,4,6], SA_F32[4,4,6]]) - @test 1 == addElement!(twoallgroup_s, 1, SA[SA_F32[3,4,6], SA_F32[4,4,6]]) - @test 1 == addElement!(twoallgroup_s, 1, SA[SA_F32[3,4,6], SA_F32[4,4,6], SA_F32[5,5,6]]) -end -@testset "mapSimplexElements" begin - @testset "empty cell list" begin - s = SimplexCellLists.Naive(2, 2, 2) - out = zeros(Int,2) - mapSimplexElements(test_f_mapSimplexElements!, out, s, SA[SA_F32[3,4,6]], 1 ,SimplexCellLists.Line, 10.0f0) - @test out == zero(out) - end - @testset "mapSimplexElements tests" begin - for trial in 1:10 - scale = Float32(exp(rand()*8.0 - 4)) - rotation = rand(RotMatrix{3,Float32}) - translation = randn(SVector{3,Float32}) * scale - function trans(in) - pts = reinterpret(SVector{3,Float32},in) - map(pts) do pt - rotation * (pt * scale) + translation - end - end - s = SimplexCellLists.Naive(2, 2, 2) - elements = makeBasicCellList!( - scale, - rotation, - translation, - s, - ) - simplex_types = (SimplexCellLists.Point, SimplexCellLists.Line, SimplexCellLists.Triangle) - group_idxs = (1,2) - i = 0 - for group_type in simplex_types - for idx in group_idxs - i+=1 - cutoff = scale*6.001f0 - f(x...) = test_f_mapSimplexElements!(x..., elements[i], cutoff) - out = mapSimplexElements( f, zeros(Int,2), s, trans(SA[SA_F32[0,0,0]]), idx ,group_type, cutoff) - @test out == [1,1] - - cutoff = scale*5.99f0 - out = mapSimplexElements( f, zeros(Int,2), s, trans(SA[SA_F32[0,0,0]]), idx ,group_type, cutoff) - @test out == [0,0] - - cutoff = scale*6.0f0 - out = mapSimplexElements( f, zeros(Int,2), s, trans(SA[SA_F32[0.01,0,0]]), idx ,group_type, cutoff) - @test out == (idx==1 ? [0,1] : [1,0]) - end - end - i = 0 - for group_type in simplex_types - for idx in group_idxs - i+=1 - cutoff = scale*6.001f0 - f(x...) = test_f_mapSimplexElements!(x..., elements[i], cutoff) - out = mapSimplexElements( f, zeros(Int,2), s, trans(SA[SA_F32[0,0,0],SA_F32[0,0,1]]), idx ,group_type, cutoff) - @test out == [1,1] - - cutoff = scale*5.99f0 - out = mapSimplexElements( f, zeros(Int,2), s, trans(SA[SA_F32[0,0,0],SA_F32[0,0,1]]), idx ,group_type, cutoff) - @test out == [0,0] - - cutoff = scale*6.0f0 - out = mapSimplexElements( f, zeros(Int,2), s, trans(SA[SA_F32[0.01,0,0],SA_F32[0,0,1]]), idx ,group_type, cutoff) - @test out == (idx==1 ? [0,1] : [1,0]) - end - end - i = 0 - for group_type in simplex_types - for idx in group_idxs - i+=1 - cutoff = scale*6.001f0 - f(x...) = test_f_mapSimplexElements!(x..., elements[i], cutoff) - out = mapSimplexElements( f, zeros(Int,2), s, trans(SA[SA_F32[0,0,0],SA_F32[0,0,1],SA_F32[0,1,1]]), idx ,group_type, cutoff) - @test out == [1,1] - - cutoff = scale*5.99f0 - out = mapSimplexElements( f, zeros(Int,2), s, trans(SA[SA_F32[0,0,0],SA_F32[0,0,1],SA_F32[0,1,1]]), idx ,group_type, cutoff) - @test out == [0,0] - - cutoff = scale*6.0f0 - out = mapSimplexElements( f, zeros(Int,2), s, trans(SA[SA_F32[0.01,0,0],SA_F32[0,0,1],SA_F32[0,1,1]]), idx ,group_type, cutoff) - @test out == (idx==1 ? [0,1] : [1,0]) - end - end - end - end -end \ No newline at end of file diff --git a/test/test_painter-basic.jl b/test/test_painter-basic.jl deleted file mode 100644 index b8cc372..0000000 --- a/test/test_painter-basic.jl +++ /dev/null @@ -1,159 +0,0 @@ -# Test Painter -using StaticArrays -using LinearAlgebra -using SimplexCellLists -using Random -using Test -using ArgCheck -using Rotations - -include("common.jl") - - -@testset "constructor" begin - kwargs = (; grid_start=SA_F32[0,0,0], grid_size=SA[4,4,4], voxel_length=1.5) - empty_s = SimplexCellLists.Painter(0, 0, 0; kwargs..., max_range=SA[Float64[],Float64[],Float64[]] ) - onepointgroup_s = SimplexCellLists.Painter(1, 0, 0; kwargs..., max_range=SA[[1.2],Float64[],Float64[]] ) - onelinegroup_s = SimplexCellLists.Painter(0, 1, 0; kwargs..., max_range=SA[Float64[],[1.2],Float64[]] ) - onetrianglegroup_s = SimplexCellLists.Painter(0, 0, 1; kwargs..., max_range=SA[Float64[],Float64[],[1.2]] ) - oneallgroup_s = SimplexCellLists.Painter(1, 1, 1; kwargs..., max_range=SA[[1.2],[1.2],[1.2]] ) - tenallgroup_s = SimplexCellLists.Painter(10, 10, 10; kwargs..., max_range=SA[fill(1.2,10),fill(1.2,10),fill(1.2,10)] ) -end -@testset "setElements!" begin - kwargs = (; grid_start=SA_F32[0,0,0], grid_size=SA[4,4,4], voxel_length=1.5) - empty_s = SimplexCellLists.Painter(0, 0, 0; kwargs..., max_range=SA[Float64[],Float64[],Float64[]]) - setElements!(empty_s, [], [], []) - - onepointgroup_s = SimplexCellLists.Painter(1, 0, 0; kwargs..., max_range=SA[Float64[1.3],Float64[],Float64[]]) - setElements!(onepointgroup_s, [[[[1,2,3]],[[3,4,6]]]], [], []) - - oneallgroup_s = SimplexCellLists.Painter(1, 1, 1; kwargs..., max_range=SA[Float64[1.2],Float64[1.5],Float64[1.3]]) - setElements!(oneallgroup_s, - [[[[1,2,3]],[[3,4,6]]]], - [[[[1,2,3],[7,2,5]],[[3,4,6],[3,9,6]]]], - [[[[1,2,3],[7,2,5],[7,9,5]],[[3,4,6],[3,9,6],[1,2,3]]]], - ) - - twoallgroup_s = SimplexCellLists.Painter(2, 2, 2; kwargs..., max_range=SA[Float64[0.3,0.7],Float64[1.0,2.2],Float64[0.5,0.7]]) - setElements!(twoallgroup_s, - [[[[1,2,3]],[[3,4,6]]],[]], - [[],[[[1,2,3],[7,2,5]],[[3,4,6],[3,9,6]]]], - [ - [[[1,2,3],[7,2,5],[7,9,5]],[[3,4,6],[3,9,6],[1,2,3]]], - [[[1,2,3],[7,2,5],[7,9,5]],[[3,4,6],[3,9,6],[1,2,3]]], - ], - ) -end -@testset "addElement!" begin - kwargs = (; grid_start=SA_F32[0,0,0], grid_size=SA[4,4,4], voxel_length=1.5) - onepointgroup_s = SimplexCellLists.Painter(1, 0, 0; kwargs..., max_range=SA[Float64[0.2],Float64[],Float64[]]) - setElements!(onepointgroup_s, [[[[1,2,3]],[[3,4,6]]]], [], []) - @test 3 == addElement!(onepointgroup_s, 1, SA[SA_F32[3,4,6]]) - @test 4 == addElement!(onepointgroup_s, 1, SA[SA_F32[3,4,6]]) - - oneallgroup_s = SimplexCellLists.Painter(1, 1, 1; kwargs..., max_range=SA[Float64[0.3],Float64[0.5],Float64[0.7]]) - setElements!(oneallgroup_s, - [[[[1,2,3]],[[3,4,6]]]], - [[[[1,2,3],[7,2,5]],[[3,4,6],[3,9,6]]]], - [[[[1,2,3],[7,2,5],[7,9,5]],[[3,4,6],[3,9,6],[1,2,3]]]], - ) - @test 3 == addElement!(oneallgroup_s, 1, SA[SA_F32[3,4,6]]) - @test 3 == addElement!(oneallgroup_s, 1, SA[SA_F32[3,4,6], SA_F32[4,4,6]]) - @test 3 == addElement!(oneallgroup_s, 1, SA[SA_F32[3,4,6], SA_F32[4,4,6], SA_F32[5,5,6]]) - - twoallgroup_s = SimplexCellLists.Painter(2, 2, 2; kwargs..., max_range=SA[Float64[0.3,0.7],Float64[1.0,2.2],Float64[0.5,0.7]]) - setElements!(twoallgroup_s, - [[[[1,2,3]],[[3,4,6]]],[]], - [[],[[[1,2,3],[7,2,5]],[[3,4,6],[3,9,6]]]], - [[],[]], - ) - @test 1 == addElement!(twoallgroup_s, 2, SA[SA_F32[3,4,6]]) - @test 3 == addElement!(twoallgroup_s, 1, SA[SA_F32[3,4,6]]) - @test 3 == addElement!(twoallgroup_s, 2, SA[SA_F32[3,4,6], SA_F32[4,4,6]]) - @test 1 == addElement!(twoallgroup_s, 1, SA[SA_F32[3,4,6], SA_F32[4,4,6]]) - @test 1 == addElement!(twoallgroup_s, 1, SA[SA_F32[3,4,6], SA_F32[4,4,6], SA_F32[5,5,6]]) -end -@testset "mapSimplexElements" begin - @testset "empty cell list" begin - s = SimplexCellLists.Painter(2, 2, 2; grid_start=SA_F32[0,0,0], grid_size=SA[4,4,4], voxel_length=1.5, max_range=SA[Float64[0.3,0.7],Float64[10.1,2.2],Float64[0.5,0.7]]) - out = zeros(Int,2) - mapSimplexElements(test_f_mapSimplexElements!, out, s, SA[SA_F32[3,4,6]], 1 ,SimplexCellLists.Line, 10.0f0) - @test out == zero(out) - end - @testset "mapSimplexElements tests" begin - for trial in 1:10 - scale = Float32(exp(rand()*8.0 - 4)) - rotation = rand(RotMatrix{3,Float32}) - translation = randn(SVector{3,Float32}) * scale - function trans(in) - pts = reinterpret(SVector{3,Float32},in) - map(pts) do pt - rotation * (pt * scale) + translation - end - end - s = SimplexCellLists.Painter(2, 2, 2; grid_start=SA_F32[-5,-5,-5], grid_size=SA[10,10,10], voxel_length=scale*rand()*10, max_range=6.1*scale) - elements = makeBasicCellList!( - scale, - rotation, - translation, - s, - ) - simplex_types = (SimplexCellLists.Point, SimplexCellLists.Line, SimplexCellLists.Triangle) - group_idxs = (1,2) - i = 0 - for group_type in simplex_types - for idx in group_idxs - i+=1 - cutoff = scale*6.001f0 - f(x...) = test_f_mapSimplexElements!(x..., elements[i], cutoff) - out = mapSimplexElements( f, zeros(Int,2), s, trans(SA[SA_F32[0,0,0]]), idx ,group_type, cutoff) - @test out == [1,1] - - cutoff = scale*5.99f0 - out = mapSimplexElements( f, zeros(Int,2), s, trans(SA[SA_F32[0,0,0]]), idx ,group_type, cutoff) - @test out == [0,0] - - cutoff = scale*6.0f0 - out = mapSimplexElements( f, zeros(Int,2), s, trans(SA[SA_F32[0.01,0,0]]), idx ,group_type, cutoff) - @test out == (idx==1 ? [0,1] : [1,0]) - end - end - i = 0 - for group_type in simplex_types - for idx in group_idxs - i+=1 - cutoff = scale*6.001f0 - f(x...) = test_f_mapSimplexElements!(x..., elements[i], cutoff) - out = mapSimplexElements( f, zeros(Int,2), s, trans(SA[SA_F32[0,0,0],SA_F32[0,0,1]]), idx ,group_type, cutoff) - @test out == [1,1] - - cutoff = scale*5.99f0 - out = mapSimplexElements( f, zeros(Int,2), s, trans(SA[SA_F32[0,0,0],SA_F32[0,0,1]]), idx ,group_type, cutoff) - @test out == [0,0] - - cutoff = scale*6.0f0 - out = mapSimplexElements( f, zeros(Int,2), s, trans(SA[SA_F32[0.01,0,0],SA_F32[0,0,1]]), idx ,group_type, cutoff) - @test out == (idx==1 ? [0,1] : [1,0]) - end - end - i = 0 - for group_type in simplex_types - for idx in group_idxs - i+=1 - cutoff = scale*6.001f0 - f(x...) = test_f_mapSimplexElements!(x..., elements[i], cutoff) - out = mapSimplexElements( f, zeros(Int,2), s, trans(SA[SA_F32[0,0,0],SA_F32[0,0,1],SA_F32[0,1,1]]), idx ,group_type, cutoff) - @test out == [1,1] - - cutoff = scale*5.99f0 - out = mapSimplexElements( f, zeros(Int,2), s, trans(SA[SA_F32[0,0,0],SA_F32[0,0,1],SA_F32[0,1,1]]), idx ,group_type, cutoff) - @test out == [0,0] - - cutoff = scale*6.0f0 - out = mapSimplexElements( f, zeros(Int,2), s, trans(SA[SA_F32[0.01,0,0],SA_F32[0,0,1],SA_F32[0,1,1]]), idx ,group_type, cutoff) - @test out == (idx==1 ? [0,1] : [1,0]) - end - end - end - end -end \ No newline at end of file diff --git a/test/test_painter.jl b/test/test_painter.jl deleted file mode 100644 index 1854299..0000000 --- a/test/test_painter.jl +++ /dev/null @@ -1,553 +0,0 @@ -# Test Painter against Naive methods. - -using StaticArrays -using LinearAlgebra -using SimplexCellLists -using Random -using Test - -include("common.jl") - - - -Random.seed!(1234) - -@testset "just points" begin - N = 100000 - points = rand(SVector{1,SVector{3,Float32}},N) - naive = SimplexCellLists.Naive(1, 0, 0) - painter = SimplexCellLists.Painter(1,0,0; - grid_start= SA[0.1,0.1,0.1], - grid_size= SA[10,10,10], - voxel_length= 1/10, - max_range= 0.1, - ) - SimplexCellLists.setElements!(naive,[points],[],[]) - SimplexCellLists.setElements!(painter,[points],[],[]) - SimplexCellLists.addElement!(naive,1,points[1]) - SimplexCellLists.addElement!(painter,1,points[1]) - delid = rand(1:(N+1)) - SimplexCellLists.deactivate!(painter, 1, delid, SimplexCellLists.Point) - SimplexCellLists.deactivate!(naive, 1, delid, SimplexCellLists.Point) - cutoff = 0.09f0 - f(x,y,i,j,d2,output) = output+((√(d2)-cutoff)^2) - naive_out = SimplexCellLists.mapSimplexElements( - f, - 0.0, - naive, - points[1], - 1, - SimplexCellLists.Point, - cutoff, - ) - painter_out = SimplexCellLists.mapSimplexElements( - f, - 0.0, - painter, - points[1], - 1, - SimplexCellLists.Point, - cutoff, - ) - @test naive_out ≈ painter_out atol=1E-6 -end - -@testset "just lines" begin - for trial in 1:100 - #@show trial - N = 1000 - lines = rand(SVector{2,SVector{3,Float32}},N) - naive = SimplexCellLists.Naive(0, 1, 0) - painter = SimplexCellLists.Painter(0, 1, 0; - grid_start= SA[0.0,0.0,0.0], - grid_size= SA[10,10,10], - voxel_length= 1/10, - max_range= 0.1, - ) - SimplexCellLists.setElements!(naive,[],[lines],[]) - SimplexCellLists.setElements!(painter,[],[lines],[]) - SimplexCellLists.addElement!(naive,1,lines[1]) - SimplexCellLists.addElement!(painter,1,lines[1]) - push!(lines,lines[1]) - delid = rand(eachindex(lines)) - SimplexCellLists.deactivate!(painter, 1, delid, SimplexCellLists.Line) - SimplexCellLists.deactivate!(naive, 1, delid, SimplexCellLists.Line) - cutoff = 0.09f0 - function f!(x,y,i,j,d2,output) - d2 = SimplexCellLists.distSqr(lines[1], lines[j]) - if √(d2) < cutoff-1E-5 - output[j] += 1 - end - output - end - naive_out = SimplexCellLists.mapSimplexElements( - f!, - zeros(length(lines)), - naive, - lines[1], - 1, - SimplexCellLists.Line, - cutoff, - ) - painter_out = SimplexCellLists.mapSimplexElements( - f!, - zeros(length(lines)), - painter, - lines[1], - 1, - SimplexCellLists.Line, - cutoff, - ) - @test naive_out == painter_out - end -end - -@testset "points and lines" begin - for trial in 1:100 - # @show trial - N = 1000 - lines = rand(SVector{2,SVector{3,Float32}},N) - points = rand(SVector{1,SVector{3,Float32}},N) - naive = SimplexCellLists.Naive(1, 1, 0) - painter = SimplexCellLists.Painter(1, 1, 0; - grid_start= SA[0.0,0.0,0.0], - grid_size= SA[10,10,10], - voxel_length= 1/10, - max_range= 0.1, - ) - SimplexCellLists.setElements!(naive,[points],[lines],[]) - SimplexCellLists.setElements!(painter,[points],[lines],[]) - SimplexCellLists.addElement!(naive,1,lines[1]) - SimplexCellLists.addElement!(painter,1,lines[1]) - SimplexCellLists.addElement!(naive,1,points[1]) - SimplexCellLists.addElement!(painter,1,points[1]) - push!(lines,lines[1]) - push!(points,points[1]) - cutoff = 0.1f0 - function pointline_f!(x,y,i,j,d2,output) - d2 = SimplexCellLists.distSqr(points[1], lines[j]) - if √(d2) < cutoff-1E-5 - output[j] += 1 - end - output - end - function linepoint_f!(x,y,i,j,d2,output) - d2 = SimplexCellLists.distSqr(lines[1], points[j]) - if √(d2) < cutoff-1E-5 - output[j] += 1 - end - output - end - pointline_naive_out = SimplexCellLists.mapSimplexElements( - pointline_f!, - zeros(length(lines)), - naive, - points[1], - 1, - SimplexCellLists.Line, - cutoff, - ) - linepoint_naive_out = SimplexCellLists.mapSimplexElements( - linepoint_f!, - zeros(length(points)), - naive, - lines[1], - 1, - SimplexCellLists.Point, - cutoff, - ) - pointline_painter_out = SimplexCellLists.mapSimplexElements( - pointline_f!, - zeros(length(lines)), - painter, - points[1], - 1, - SimplexCellLists.Line, - cutoff, - ) - linepoint_painter_out = SimplexCellLists.mapSimplexElements( - linepoint_f!, - zeros(length(points)), - painter, - lines[1], - 1, - SimplexCellLists.Point, - cutoff, - ) - @test linepoint_naive_out == linepoint_painter_out - @test pointline_naive_out == pointline_painter_out - end -end - -@testset "points and lines edge cases 1" begin - cutoff = 1f-1 - painter = SimplexCellLists.Painter(1, 0, 0; - grid_start= SA[-5.0,-5.0,-5.0], - grid_size= SA[10,10,10], - voxel_length= 1.0, - max_range= SA[Float64[0.1],Float64[],Float64[]], - ) - diag = SA_F32[1,1,1] - diag_l = norm(diag) - diag_hat = normalize(diag) - point = 1//2*diag - (SimplexCellLists.MAX_SAMPLE_SPACING + cutoff)*diag_hat - SimplexCellLists.addElement!(painter,1,SA[point]) - line_a = point + 0.9f0*cutoff*diag_hat - line_b = line_a + 2*SimplexCellLists.MAX_SAMPLE_SPACING*diag_hat - f(x,y,i,j,d2,output) = output + 1 - out = SimplexCellLists.mapSimplexElements( - f, - 0, - painter, - SA[line_a, line_b], - 1, - SimplexCellLists.Point, - cutoff, - ) - @test out == 1 -end - -@testset "points and lines edge cases 2" begin - cutoff = 1f-1 - painter = SimplexCellLists.Painter(1, 0, 0; - grid_start= SA[-5.0,-5.0,-5.0], - grid_size= SA[10,10,10], - voxel_length= 1.0, - max_range= 0.1, - ) - diag = SA_F32[1,1,1] - diag_l = norm(diag) - diag_hat = normalize(diag) - point = 1//2*diag - (SimplexCellLists.MAX_SAMPLE_SPACING-0.001f0 + cutoff-0.001f0)*diag_hat - SimplexCellLists.addElement!(painter,1,SA[point]) - line_a = 1//2*diag - (SimplexCellLists.MAX_SAMPLE_SPACING-0.001f0)*diag_hat - line_b = line_a + 1.9999f0*SimplexCellLists.MAX_SAMPLE_SPACING*diag_hat - f(x,y,i,j,d2,output) = output + 1 - out = SimplexCellLists.mapSimplexElements( - f, - 0, - painter, - SA[line_a, line_b], - 1, - SimplexCellLists.Point, - cutoff, - ) - @test out == 1 -end - -@testset "points and lines edge cases 3" begin - cutoff = 1f-1 - painter = SimplexCellLists.Painter(1, 0, 0; - grid_start= SA[-5.0,-5.0,-5.0], - grid_size= SA[10,10,10], - voxel_length= 1.0, - max_range= 0.1, - ) - diag = SA_F32[1,1,1] - diag_l = norm(diag) - diag_hat = normalize(diag) - point = 1//2*diag - (SimplexCellLists.MAX_SAMPLE_SPACING-0.001f0 + cutoff-0.001f0)*diag_hat - SimplexCellLists.addElement!(painter,1,SA[point]) - line_a = 1//2*diag - (SimplexCellLists.MAX_SAMPLE_SPACING-0.001f0)*diag_hat - line_b = line_a + 2.1f0*SimplexCellLists.MAX_SAMPLE_SPACING*diag_hat - f(x,y,i,j,d2,output) = output + 1 - out = SimplexCellLists.mapSimplexElements( - f, - 0, - painter, - SA[line_a, line_b], - 1, - SimplexCellLists.Point, - cutoff, - ) - @test out == 1 -end - -@testset "points and lines edge cases 4" begin - cutoff = 1f-1 - painter = SimplexCellLists.Painter(1, 0, 0; - grid_start= SA[-5.0,-5.0,-5.0], - grid_size= SA[10,10,10], - voxel_length= 1.0, - max_range= 0.1, - ) - diag = SA_F32[1,1,1] - diag_l = norm(diag) - diag_hat = normalize(diag) - point = 1//2*diag - (SimplexCellLists.MAX_SAMPLE_SPACING-0.001f0 + cutoff-0.001f0)*diag_hat - SimplexCellLists.addElement!(painter,1,SA[point]) - line_a = 1//2*diag - (SimplexCellLists.MAX_SAMPLE_SPACING-0.001f0)*diag_hat - line_b = line_a - 2.1f0*SimplexCellLists.MAX_SAMPLE_SPACING*diag_hat - f(x,y,i,j,d2,output) = output + 1 - out = SimplexCellLists.mapSimplexElements( - f, - 0, - painter, - SA[line_a, line_b], - 1, - SimplexCellLists.Point, - cutoff, - ) - @test out == 1 -end - -@testset "parallel lines" begin - cutoff = 2.5f0 - painter = SimplexCellLists.Painter(0, 1, 0; - grid_start= SA[-5.0,-5.0,-5.0], - grid_size= SA[10,10,10], - voxel_length= 1.0, - max_range= 2.5f0, - ) - x = SA_F32[1,0,0] - y = SA_F32[0,1,0] - z = SA_F32[0,0,1] - SimplexCellLists.addElement!(painter,1,SA[-100x, 100x]) - f(x,y,i,j,d2,output) = output + 1 - out = SimplexCellLists.mapSimplexElements( - f, - 0, - painter, - SA[-100x+y, 100x+y], - 1, - SimplexCellLists.Line, - cutoff, - ) - @test out == 1 -end - -@testset "almost parallel lines that are closest outside the grid" begin - cutoff = 2.5f0 - painter = SimplexCellLists.Painter(0, 1, 0; - grid_start= SA[-5.0,-5.0,-5.0], - grid_size= SA[10,10,10], - voxel_length= 1.0, - max_range= 2.5f0, - ) - x = SA_F32[1,0,0] - y = SA_F32[0,1,0] - z = SA_F32[0,0,1] - SimplexCellLists.addElement!(painter,1,SA[-100x, 100x]) - f(x,y,i,j,d2,output) = output + 1 - out = SimplexCellLists.mapSimplexElements( - f, - 0, - painter, - SA[-100x+y, 100x], - 1, - SimplexCellLists.Line, - cutoff, - ) - @test out == 1 -end - -@testset "points outside the grid" begin - cutoff = 1f-1 - painter = SimplexCellLists.Painter(1, 0, 0; - grid_start= SA[-5.0,-5.0,-5.0], - grid_size= SA[10,10,10], - voxel_length= 1.0, - max_range= 0.1, - ) - x = SA_F32[1,0,0] - y = SA_F32[0,1,0] - z = SA_F32[0,0,1] - SimplexCellLists.addElement!(painter,1,SA[-100x]) - f123(x,y,i,j,d2,output) = output + 1 - out = SimplexCellLists.mapSimplexElements( - f123, - 0, - painter, - SA[-100.05f0x], - 1, - SimplexCellLists.Point, - cutoff, - ) - @test out == 1 -end - -@testset "map pairs of simplex $M in the same group" for M in (1:3) - simplex_type = SimplexCellLists.Simplex{M} - for trial in 1:20 - N = 1000 - scale = Float32(exp(rand()*4.0 - 2)) - elements = scale .* rand(simplex_type, N) - naive = SimplexCellLists.Naive(1, 1, 1) - painter = SimplexCellLists.Painter(1, 1, 1; - min_point = SA[0.0,0.0,0.0], - max_point = SA[scale,scale,scale], - voxel_length= scale*1/10, - max_range= scale*0.1, - ) - total_elements = [[[]],[[]],[[]]] - total_elements[M][1] = elements - SimplexCellLists.setElements!(naive,total_elements...) - SimplexCellLists.setElements!(painter,total_elements...) - SimplexCellLists.addElement!(naive,1,elements[1]) - SimplexCellLists.addElement!(painter,1,elements[1]) - push!(elements,elements[1]) - delid = rand(eachindex(elements)) - SimplexCellLists.deactivate!(painter, 1, delid, simplex_type) - SimplexCellLists.deactivate!(naive, 1, delid, simplex_type) - cutoff = scale*0.099f0 - f!(x...) = test_f_mapPairElements!(x..., elements,cutoff) - naive_out = SimplexCellLists.mapPairElements( - f!, - zeros(length(elements),length(elements)), - naive, - 1, - simplex_type, - cutoff, - ) - painter_out = SimplexCellLists.mapPairElements( - f!, - zeros(length(elements),length(elements)), - painter, - 1, - simplex_type, - cutoff, - ) - if naive_out != painter_out - println("Simplex $M") - dif_inds = findall(naive_out .!= painter_out) - for ind in dif_inds - @show ind - @show naive_out[ind], painter_out[ind] - d2 = SimplexCellLists.distSqr(lines[ind[1]], lines[ind[2]]) - @show d2 - @show √(d2) - @show cutoff - println() - end - println() - end - @test naive_out == painter_out - end -end - -@testset "map pairs in the same group" begin - for trial in 1:20 - N = 1000 - triangles = trial .* rand(SVector{3,SVector{3,Float32}},N) - lines = trial .* rand(SVector{2,SVector{3,Float32}},N) - points = trial .* rand(SVector{1,SVector{3,Float32}},N) - naive = SimplexCellLists.Naive(1, 1, 1) - painter = SimplexCellLists.Painter(1, 1, 1; - grid_start= SA[0.0,0.0,0.0], - grid_size= SA[10,10,10], - voxel_length= trial*1/10, - max_range= trial*0.1, - ) - SimplexCellLists.setElements!(naive,[points],[lines],[triangles]) - SimplexCellLists.setElements!(painter,[points],[lines],[triangles]) - SimplexCellLists.addElement!(naive,1,triangles[1]) - SimplexCellLists.addElement!(painter,1,triangles[1]) - SimplexCellLists.addElement!(naive,1,lines[1]) - SimplexCellLists.addElement!(painter,1,lines[1]) - SimplexCellLists.addElement!(naive,1,points[1]) - SimplexCellLists.addElement!(painter,1,points[1]) - push!(triangles,triangles[1]) - push!(lines,lines[1]) - push!(points,points[1]) - delid = rand(eachindex(lines)) - SimplexCellLists.deactivate!(painter, 1, delid, SimplexCellLists.Line) - SimplexCellLists.deactivate!(naive, 1, delid, SimplexCellLists.Line) - cutoff = trial*0.099f0 - #@show trial - function LLf!(x,y,i,j,d2,output) - d2 = SimplexCellLists.distSqr(lines[i], lines[j]) - if √(d2) < cutoff-1E-3 - output[i,j] += 1 - output[j,i] += 1 - end - output - end - function LPf!(x,y,i,j,d2,output) - d2 = SimplexCellLists.distSqr(lines[i], points[j]) - if √(d2) < cutoff-1E-3 - output[i,j] += 1 - output[j,i] += 1 - end - output - end - function PPf!(x,y,i,j,d2,output) - d2 = SimplexCellLists.distSqr(points[i], points[j]) - if √(d2) < cutoff-1E-3 - output[i,j] += 1 - output[j,i] += 1 - end - output - end - naive_out_LL = SimplexCellLists.mapPairElements( - LLf!, - zeros(length(lines),length(lines)), - naive, - 1, - SimplexCellLists.Line, - cutoff, - ) - painter_out_LL = SimplexCellLists.mapPairElements( - LLf!, - zeros(length(lines),length(lines)), - painter, - 1, - SimplexCellLists.Line, - cutoff, - ) - - naive_out_PP = SimplexCellLists.mapPairElements( - PPf!, - zeros(length(points),length(points)), - naive, - 1, - SimplexCellLists.Point, - cutoff, - ) - painter_out_PP = SimplexCellLists.mapPairElements( - PPf!, - zeros(length(points),length(points)), - painter, - 1, - SimplexCellLists.Point, - cutoff, - ) - - naive_out_LP = SimplexCellLists.mapElementsElements( - LPf!, - zeros(length(lines),length(points)), - naive, - 1, - SimplexCellLists.Line, - 1, - SimplexCellLists.Point, - cutoff, - ) - painter_out_LP = SimplexCellLists.mapElementsElements( - LPf!, - zeros(length(lines),length(points)), - painter, - 1, - SimplexCellLists.Line, - 1, - SimplexCellLists.Point, - cutoff, - ) - for (painter_out, naive_out, name) in ((painter_out_LL, naive_out_LL, "Line Line"), (painter_out_LP, naive_out_LP, "Line Point"), (painter_out_PP, naive_out_PP, "Point Point")) - if naive_out != painter_out - println(name) - dif_inds = findall(naive_out .!= painter_out) - for ind in dif_inds - @show ind - @show naive_out[ind], painter_out[ind] - d2 = SimplexCellLists.distSqr(lines[ind[1]], lines[ind[2]]) - @show d2 - @show √(d2) - @show cutoff - println() - end - println() - end - @test naive_out == painter_out - end - end -end \ No newline at end of file From 9bd4361a8343dd621e493176edb011d0fa26298d Mon Sep 17 00:00:00 2001 From: nhz2 Date: Sun, 9 Aug 2026 10:40:09 +0200 Subject: [PATCH 02/14] add line seg cell list --- README.md | 156 +-------------- src/SimplexCellLists.jl | 14 +- src/distances.jl | 10 +- src/linesegcelllist.jl | 250 ++++++++++++++++++++++++ src/pointcelllist.jl | 32 ++-- src/util.jl | 11 ++ test/runtests.jl | 6 +- test/test-linesegcelllist.jl | 361 +++++++++++++++++++++++++++++++++++ test/test-pointcelllist.jl | 44 +++-- 9 files changed, 682 insertions(+), 202 deletions(-) create mode 100644 src/linesegcelllist.jl create mode 100644 test/test-linesegcelllist.jl diff --git a/README.md b/README.md index c70bc68..f5af5d9 100644 --- a/README.md +++ b/README.md @@ -3,160 +3,10 @@ [![Build Status](https://github.com/medyan-dev/SimplexCellLists.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/medyan-dev/SimplexCellLists.jl/actions/workflows/CI.yml?query=branch%3Amain) ![Julia Support](https://img.shields.io/badge/Julia-1.8-blue) -This Julia package accelerates computations on all pairs of 3D points, line segments, and triangles within a cutoff distance. +This Julia package accelerates computations on pairs of 3D points, line segments, and triangles within a cutoff distance. This package is largely inspired by [CellListMap.jl](https://github.com/m3g/CellListMap.jl). -However, there is no support for periodic boundary conditions, 2D systems, or types other than Float32. +However, there is no support for periodic boundary conditions or 2D systems. -See [CellListMap.jl](https://github.com/m3g/CellListMap.jl) if you want these features, or higher performance on systems without triangles and line segments. - - -## Setup - -- `Simplex{N}` is `SVector{N, SVector{3, Float32}}` -- `Point` is `Simplex{1}` -- `Line` is `Simplex{2}` -- `Triangle` is `Simplex{3}` - -There are multiple algorithms that implement the `SimplexCellList` abstract type interface. -Currently: -- `Naive`: A simple `SimplexCellList` with no extra options that uses double for loops. -- `Painter`: A grid is painted with element ids based on a max range. Based on the ideas in [cytosim](https://gitlab.com/f-nedelec/cytosim/-/blob/af739d2ff768628e4737d3a75457676e1a7f4287/src/sim/fiber_grid.h). - -Let `T` be a concrete subtype of `SimplexCellList` - -### Constructor -Construct `T`: - -```julia -T(numpointgroups::Integer, numlinegroups::Integer, numtrianglegroups::Integer; kwargs...)::T -``` - -Where `numpointgroups` is the number of groups of points, `numlinegroups` is the number of groups of lines, -and `numtrianglegroups` is the number of groups of triangles. - -`kwargs` are options specific for `T` - -### `setElements!` -Reset the elements stored in `s` in batch: - -```julia -setElements!(s::T, points, lines, triangles)::Nothing -``` - -Where `points`, `lines` and `triangles` are collections of collections of objects convertible to -`Point`, `Line`, and `Triangle` respectively. - -For example, each collection in `points` is a group of points that can be mapped over independently from, or together with, other groups. - -Added elements will have a group index and element index based on the order of the inputs. -The first group in each type has group index 1, and the first element in each group has element index 1. - -### `addElement!` -Add a new element to `s`, and return its element index: - -```julia -addElement!(s::T, group_idx::Integer, element::Simplex{N})::Int32 -``` -The new element will be pushed to the end of the specified group. - -### `deactivate!` -Deactivate an existing element in `s` - -```julia -deactivate!(s::T, group_idx::Integer, element_idx::Integer, element_type::Type{Simplex{N}})::Nothing -``` -Inactive elements are not mapped over. Elements are active by default. - -### `activate!` -Re-activate an existing element in `s` - -```julia -activate!(s::T, group_idx::Integer, element_idx::Integer, element_type::Type{Simplex{N}})::Nothing -``` -Inactive elements are not mapped over. Elements are active by default. - -### `isActive` -Return if an existing element in `s` is active. - -```julia -isActive(s::T, group_idx::Integer, element_idx::Integer, element_type::Type{Simplex{N}})::Bool -``` -Inactive elements are not mapped over. Elements are active by default. - -## Mapping - -The following functions allow mapping a custom function over pairs of simplexes within some cutoff. - -### Mapped function `f` - -The function f should have the same form as used in CellListMap.jl. - -`i` is the element index of simplex `x`, `j` is the element index of simplex `y`. - -`d2` is an approximate `Float32` squared distance between `x` and `y`. - -Except here `x` and `y` are `Simplex{N}`, `Simplex{M}` - -```julia - function f(x,y,i,j,d2,output) - # update output - return output - end -``` - -The order in which pairs of elements in range are mapped is implementation dependent. - -The elements passed to `f` may be slightly different from the elements added to `T` due to implementation dependent floating point rounding errors. - -If a pair distance is very near the cutoff, it is implementation dependent whether the pair gets mapped or not due to floating point rounding errors. - -Therefore, if more precision is needed, add some extra distance to the cutoff, store the elements externally in 64 bit precision, and in `f` use `i` and `j` to get the precise elements and again check distances. - -### `mapSimplexElements` - -Map `f` to all simplexes in a group close to a single simplex. - -```julia -mapSimplexElements(f, output, s::T, x::Simplex{N}, group_idx::Integer, elements_type::Type{Simplex{M}}, cutoff::Float32) where {N, M} -``` - -Apply function `f` to all elements in group `group_idx` within the cutoff range of the simplex `x`, and -return the output of the final `f` call. - -`x` is always `x` and `i` is always 0, in calls to `f`. - -### `mapPairElements` - -Map `f` to all pairs of nearby simplexes in a single group. - -```julia -mapPairElements(f, output, s::T, group_idx::Integer, elements_type::Type{Simplex{N}}, cutoff::Float32) where {N} -``` -Apply function `f` to all unordered pairs of elements in group `group_idx` within cutoff range, and return the output of the final `f` call. - -`f` is never called more than once per unordered pair. Which element is `x` and `y` in calls to `f` is implementation dependent. - - -### `mapElementsElements` - -Map `f` to all pairs of nearby simplexes between two different groups. - -```julia -mapElementsElements( - f, - output, - s::T, - x_group_idx::Integer, - x_type::Type{Simplex{N}}, - y_group_idx::Integer, - y_type::Type{Simplex{M}}, - cutoff::Float32, - ) where {N, M} -``` -Apply function `f` to each pair of elements from two different groups that are within cutoff range of each other, and return the output of the final `f` call. - -The first element has is an `x_type` in group `x_group_idx` and the second element is a `y_type` in group `y_group_idx`. - -`f` is never called more than once per pair. +See [CellListMap.jl](https://github.com/m3g/CellListMap.jl) if you want these features. diff --git a/src/SimplexCellLists.jl b/src/SimplexCellLists.jl index 3c0604b..19c0fc5 100644 --- a/src/SimplexCellLists.jl +++ b/src/SimplexCellLists.jl @@ -7,8 +7,9 @@ using ArgCheck const Vec3 = SVector{3} const Simplex{N,T} = SVector{N, Vec3{T}} const Point = Simplex{1} -const Line = Simplex{2} +const LineSeg = Simplex{2} const Triangle = Simplex{3} +public Vec3, Simplex, Point, LineSeg, Triangle include("util.jl") export ball_intersects_unit_cube @@ -23,6 +24,15 @@ export PointCellList export CellPointEntry export cell_point_add! export map_nearby_points -export cell_point_clear! +export cell_points_clear! + +include("linesegcelllist.jl") +export LineSegCellList +export CellLineSegEntry +export cell_line_seg_add! +export cell_line_seg_is_end +export cell_line_seg_tmax +export map_nearby_line_segs +export cell_line_segs_clear! end \ No newline at end of file diff --git a/src/distances.jl b/src/distances.jl index 7b2cf53..e279b24 100644 --- a/src/distances.jl +++ b/src/distances.jl @@ -51,7 +51,7 @@ function dist_sqr_r_s_t(x::Point{T}, y::Point{T}) where T d ⋅ d, d, SVector{0,T}(), SVector{0,T}() end -function dist_sqr_r_s_t(x::Point{T}, y::Line{T}) where T +function dist_sqr_r_s_t(x::Point{T}, y::LineSeg{T}) where T r = y[2] - y[1] p = x[1] - y[1] t = clamp01nan((p ⋅ r)/(r ⋅ r)) @@ -177,7 +177,7 @@ https://www.geometrictools.com/Documentation/DistanceLine3Line3.pdf // Version: 6.0.2022.01.06 Ignores the case of degenerate line segments. =# -function dist_sqr_r_s_t(x::Line{T}, y::Line{T}) where T +function dist_sqr_r_s_t(x::LineSeg{T}, y::LineSeg{T}) where T P0 = x[1] P1 = x[2] Q0 = y[1] @@ -240,7 +240,7 @@ https://www.geometrictools.com/Documentation/DistanceLine3Line3.pdf // Version: 6.0.2022.01.06 Ignores the case of degenerate line segments. =# -function dist_sqr(x::Line{T}, y::Line{T}) where T +function dist_sqr(x::LineSeg{T}, y::LineSeg{T}) where T P0 = x[1] P1 = x[2] Q0 = y[1] @@ -327,7 +327,7 @@ function moller_trumbore_intersect(o, d, a, b, c) SegTriangleIntersectResult(true, t, u, v) end -function dist_sqr_r_s_t(x::Line{T}, y::Triangle{T}) where T +function dist_sqr_r_s_t(x::LineSeg{T}, y::Triangle{T}) where T fzero = zero(T) o = x[1] d = x[2] - x[1] @@ -374,7 +374,7 @@ function dist_sqr_r_s_t(x::Line{T}, y::Triangle{T}) where T end end -function dist_sqr(x::Line{T}, y::Triangle{T})::T where T +function dist_sqr(x::LineSeg{T}, y::Triangle{T})::T where T fzero = zero(T) o = x[1] d = x[2] - x[1] diff --git a/src/linesegcelllist.jl b/src/linesegcelllist.jl new file mode 100644 index 0000000..347e3f4 --- /dev/null +++ b/src/linesegcelllist.jl @@ -0,0 +1,250 @@ + +""" + CellLineSegEntry{T,F} + +A single line segment's contribution to one voxel cell. Created by [`cell_line_seg_add!`](@ref) +and read by [`map_nearby_line_segs`](@ref). + +# Fields +- `id::T`: line segment ID. +- `p0::SVector{3, F}`: line segment start point. +- `d_hat::SVector{3, F}`: unit direction vector along the line segment. Zero for zero-length line segments. +- `tmin::F`: start of the owned parametric range along the line segment (length units from `p0`). +- `packed_tmax::F`: end of the owned parametric range (length units from `p0`). The sign bit + encodes the `is_end` flag; use [`cell_line_seg_tmax`](@ref) for the absolute value and + [`cell_line_seg_is_end`](@ref) for the flag. +""" +struct CellLineSegEntry{T,F} + id::T + p0::SVector{3, F} + d_hat::SVector{3, F} + tmin::F + packed_tmax::F +end +function CellLineSegEntry{T,F}(id::T, is_end::Bool, + p0::SVector{3, F}, d_hat::SVector{3, F}, + tmin::F, tmax::F) where {T,F} + # Pack is_end flag into sign bit of tmax (-0.0 encodes is_end when tmax==0) + packed_tmax = ifelse(is_end, -abs(tmax), abs(tmax)) + CellLineSegEntry{T,F}(id, p0, d_hat, tmin, packed_tmax) +end + +""" + cell_line_seg_is_end(e::CellLineSegEntry)::Bool + +Return true if `e` is the entry containing the end point of its line segment. +""" +cell_line_seg_is_end(e::CellLineSegEntry) = signbit(e.packed_tmax) + +""" + cell_line_seg_tmax(e::CellLineSegEntry{T,F})::F + +Return the end of the owned parametric range of `e` (length units from `e.p0`). +""" +cell_line_seg_tmax(e::CellLineSegEntry) = abs(e.packed_tmax) + +struct CellLineSeg{T,F} + list::Memory{CellLineSegEntry{T,F}} + len::Int +end +CellLineSeg{T,F}() where {T,F} = CellLineSeg{T,F}(Memory{CellLineSegEntry{T,F}}(), 0) + +""" + LineSegCellList{T,F}(size::SVector{3, Int}, grid_spacing::F) + LineSegCellList{T,F}(size::NTuple{3, Int}, grid_spacing::F) + +An origin-centered 3D voxel grid for spatial queries on line segments. The grid spans +`[-size * grid_spacing/2, size * grid_spacing/2)` in each dimension. + +Insert line segments with [`cell_line_seg_add!`](@ref) and query with +[`map_nearby_line_segs`](@ref). Use [`cell_line_segs_clear!`](@ref) to reset. + +Positions are not required to be within the grid bounds: parts of a line segment +outside the grid are stored in the nearest boundary cells, and queries account +for this. Line segments far outside the grid accumulate in boundary cells, which +can slow down queries near those boundaries. + +# Arguments +- `size`: number of cells along each axis (x, y, z). +- `grid_spacing::F`: side length of each cubic cell. +""" +struct LineSegCellList{T,F} + cells::Memory{CellLineSeg{T,F}} + size::SVector{3, Int} + inv_half_grid_spacing::F +end +function LineSegCellList{T,F}(size::SVector{3, Int}, grid_spacing::F) where {T,F} + @argcheck all(≥(0), size) + @argcheck grid_spacing > 0 + inv_half_grid_spacing = 2*inv(grid_spacing) + @argcheck isfinite(inv_half_grid_spacing) + n = prod(size) + cells = Memory{CellLineSeg{T,F}}(undef, n) + fill!(cells, CellLineSeg{T,F}()) + LineSegCellList{T,F}(cells, size, inv_half_grid_spacing) +end +LineSegCellList{T,F}(size::NTuple{3, Int}, grid_spacing::F) where {T,F} = LineSegCellList{T,F}(SVector{3,Int}(size...), grid_spacing) + +""" + cell_line_segs_clear!(scl::LineSegCellList)::Nothing + +Remove all line segments. +""" +function cell_line_segs_clear!(scl::LineSegCellList{T,F})::Nothing where {T,F} + for i in eachindex(scl.cells) + scl.cells[i] = CellLineSeg{T,F}(scl.cells[i].list, 0) + end +end + +""" + cell_line_seg_add!(scl::LineSegCellList{T,F}, p0::SVector{3, F}, p1::SVector{3, F}, id::T)::Nothing + +Insert a line segment into the cell list, adding a [`CellLineSegEntry`](@ref) to every cell the +line segment passes through. Each entry records the parametric range of the line segment owned +by its cell; the ranges partition the line segment so [`map_nearby_line_segs`](@ref) reports it +exactly once. + +Uses the Amanatides & Woo "Fast Voxel Traversal" algorithm (1987) for 3D DDA traversal. + +Positions are not required to be within the grid bounds: parts of the line segment outside the +grid are owned by the nearest boundary cells, and queries account for this. Line segments far +outside the grid accumulate in boundary cells, which can slow down queries near those boundaries. + +Throws an `ArgumentError` if the length of the line segment is not finite: for example if a +coordinate is NaN or Inf, or if the squared length overflows `F`. + +# Arguments +- `scl`: the line segment cell list to insert into. +- `p0`: line segment start point. +- `p1`: line segment end point. +- `id`: ID for this line segment. +""" +function cell_line_seg_add!(scl::LineSegCellList{T,F}, p0::SVector{3, F}, p1::SVector{3, F}, id::T)::Nothing where {T,F} + h2_inv = scl.inv_half_grid_spacing + sz = scl.size + @argcheck all(>(0), sz) + strides = SVector(1, sz[1], sz[1]*sz[2]) + d = p1 - p0 + seg_len = norm_fast(d) + @argcheck isfinite(seg_len) + invseglen = inv(seg_len) + d_hat = ifelse(isfinite(invseglen), d * invseglen, zero(d)) + # Work in doubled coordinates 2*pos/h so the origin-centering offset is + # added exactly in Int after the floor, keeping cell assignment accurate + # for any grid size. Clamp in F before converting to Int so far away + # positions don't overflow Int; the prevfloat upper bound keeps the + # resulting index ≤ size - 1. + q0 = p0 * h2_inv + i = round2grid(clamp.(q0, -F.(sz), prevfloat.(F.(sz))), sz) + step_signbit = signbit.(d) + step = ifelse.(step_signbit, -1, +1) + # tDelta: how much t to cross one cell (2 doubled units). + tDelta = 2 .* inv.(abs.(d_hat * h2_inv)) # h = |d̂ᵢ| * tΔᵢ, tΔᵢ = h/|d̂ᵢ|, tΔᵢ = 2/(|d̂ᵢ*2*inv(h)|) + # tMax: t at which the next cell boundary is crossed per axis. Boundary + # cells own everything outside the grid (matching the clamping above), so + # an axis already at the wall in its step direction never crosses again. + at_wall = ifelse.(step_signbit, i .== 0, i .== sz .- 1) + next_boundary = 2 .* (i .+ .!step_signbit) .- sz + tMax_raw = abs.(next_boundary .- q0) .* tDelta .* F(0.5) + # Replace NaN with Inf (from 0*Inf when the segment is parallel to an axis + # exactly at a cell boundary) + tMax_1, tMax_2, tMax_3 = ifelse.(at_wall .| isnan.(tMax_raw), F(Inf), tMax_raw) + # DDA loop + t_prev = zero(F) # parametric t at entry to current cell + li = i ⋅ strides + X, Y, Z = i + while true + t_exit = min(tMax_1, tMax_2, tMax_3, seg_len) + is_last = !(t_exit < seg_len) + entry = CellLineSegEntry{T,F}(id, is_last, p0, d_hat, t_prev, t_exit) + scl.cells[begin + li] = _cell_push_entry!(scl.cells[begin + li], entry) + is_last && break + # Step to next cell: advance the axis with smallest tMax. + t_prev = t_exit + if tMax_1 ≤ tMax_2 && tMax_1 ≤ tMax_3 + X += step[1] + li += step[1] * strides[1] + tMax_1 = ifelse(ifelse(step_signbit[1], X == 0, X == sz[1]-1), F(Inf), tMax_1 + tDelta[1]) + elseif tMax_2 ≤ tMax_3 + Y += step[2] + li += step[2] * strides[2] + tMax_2 = ifelse(ifelse(step_signbit[2], Y == 0, Y == sz[2]-1), F(Inf), tMax_2 + tDelta[2]) + else + Z += step[3] + li += step[3] * strides[3] + tMax_3 = ifelse(ifelse(step_signbit[3], Z == 0, Z == sz[3]-1), F(Inf), tMax_3 + tDelta[3]) + end + end + nothing +end + + +""" + map_nearby_line_segs(f, scl::LineSegCellList{T,F}, pos::SVector{3, F}, cutoff::F, out) -> out + +Call `f` for every line segment in `scl` whose closest point to `pos` is within `cutoff` distance. +Each line segment is reported exactly once (deduplicated across cells). + +# Arguments +- `f(entry::CellLineSegEntry{T,F}, out) -> (out, cont::Bool)`: callback called for each nearby line segment. + Return `(new_out, true)` to continue, or `(new_out, false)` to stop early. +- `scl`: the line segment cell list to query. +- `pos`: query point. +- `cutoff`: maximum distance. If zero, returns `out` immediately. +- `out`: initial accumulator value, threaded through `f` and returned. +""" +function map_nearby_line_segs( + f, + scl::LineSegCellList{T,F}, + pos::SVector{3, F}, + cutoff::F, + out, + ) where {T,F} + cutoff ≤ 0 && return out + sz = scl.size + all(>(0), sz) || return out + cutoff2 = cutoff * cutoff + h2_inv = scl.inv_half_grid_spacing + strides = SVector(1, sz[1], sz[1]*sz[2]) + # Clamping the query into the grid box makes the cube test below stay + # conservative given line segments can extend outside the grid. + # Clamping in F before converting to Int also keeps far away + # queries and huge cutoffs from overflowing Int. + pos_norm = clamp.(pos * h2_inv, -F.(sz), prevfloat.(F.(sz))) + r_cells = cutoff * h2_inv + lo = round2grid(max.(pos_norm .- r_cells, -F.(sz)), sz) + hi = round2grid(min.(pos_norm .+ r_cells, prevfloat.(F.(sz))), sz) + for iz in lo[3]:hi[3] + liz = iz*strides[3] + cell_center_z = 2*iz + 1 - sz[3] + for iy in lo[2]:hi[2] + liy = iy*strides[2] + cell_center_y = 2*iy + 1 - sz[2] + for ix in lo[1]:hi[1] + lix = ix + cell_center_x = 2*ix + 1 - sz[1] + li = lix + liy + liz + cell_center = SVector{3, F}(cell_center_x, cell_center_y, cell_center_z) + @inline ball_intersects_biunit_cube(pos_norm - cell_center, r_cells) || continue + cs = scl.cells[begin + li] + for j in 1:cs.len + entry = cs.list[j] + # closest point parameter on the whole line segment + tmax = cell_line_seg_tmax(entry) + pos_m_p0 = pos - entry.p0 + t = clamp(pos_m_p0 ⋅ entry.d_hat, zero(F), tmax) + # ownership check: tmin ≤ t < tmax, or is_end + t ≥ entry.tmin || continue + (t < tmax || cell_line_seg_is_end(entry)) || continue + # distance check + diff = pos_m_p0 - t * entry.d_hat + d2 = diff ⋅ diff + d2 ≤ cutoff2 || continue + out, cont = f(entry, out) + cont || return out + end + end + end + end + out +end diff --git a/src/pointcelllist.jl b/src/pointcelllist.jl index 2391680..4b4bc91 100644 --- a/src/pointcelllist.jl +++ b/src/pointcelllist.jl @@ -6,11 +6,11 @@ and read by [`map_nearby_points`](@ref). # Fields - `pos::SVector{3,F}`: point position. -- `point_id::T`: point ID. +- `id::T`: point ID. """ struct CellPointEntry{T,F} pos::SVector{3,F} - point_id::T + id::T end struct CellPoint{T,F} @@ -19,21 +19,21 @@ struct CellPoint{T,F} end CellPoint{T,F}() where{T,F} = CellPoint{T,F}(Memory{CellPointEntry{T,F}}(), 0) -function _cell_point_push_entry!(old::CellPoint{T,F}, entry::CellPointEntry{T,F}) where {T,F} +function _cell_push_entry!(old::CELLTYPE, entry::ENTRYTYPE) where {CELLTYPE,ENTRYTYPE} old_list = old.list cap = length(old_list) len = old.len list = if cap ≤ len new_cap = max(cap<<1, 1) @assert new_cap > len - new_list = Memory{CellPointEntry{T,F}}(undef, new_cap) + new_list = Memory{ENTRYTYPE}(undef, new_cap) unsafe_copyto!(new_list, 1, old_list, 1, cap) new_list else old_list end list[len+1] = entry - CellPoint{T,F}(list, len+1) + CELLTYPE(list, len+1) end """ @@ -44,7 +44,7 @@ An origin-centered 3D voxel grid for spatial queries on points. The grid spans `[-size * grid_spacing/2, size * grid_spacing/2)` in each dimension. Insert points with [`cell_point_add!`](@ref) and query with -[`map_nearby_points`](@ref). Use [`cell_point_clear!`](@ref) to reset. +[`map_nearby_points`](@ref). Use [`cell_points_clear!`](@ref) to reset. Positions are not required to be within the grid bounds: points outside the grid are stored in the nearest boundary cell, and queries account for this. @@ -73,18 +73,18 @@ end PointCellList{T,F}(size::NTuple{3, Int}, grid_spacing::F) where {T,F} = PointCellList{T,F}(SVector{3,Int}(size...), grid_spacing) """ - cell_point_clear!(pcl::PointCellList)::Nothing + cell_points_clear!(pcl::PointCellList)::Nothing Remove all points. """ -function cell_point_clear!(pcl::PointCellList{T,F})::Nothing where {T,F} +function cell_points_clear!(pcl::PointCellList{T,F})::Nothing where {T,F} for i in eachindex(pcl.cells) pcl.cells[i] = CellPoint{T,F}(pcl.cells[i].list, 0) end end """ - cell_point_add!(pcl::PointCellList{T,F}, pos::SVector{3, F}, point_id::T)::Nothing + cell_point_add!(pcl::PointCellList{T,F}, pos::SVector{3, F}, id::T)::Nothing Insert a point into the cell list, adding a [`CellPointEntry`](@ref) to the cell containing the point. If the point is outside the grid bounds, it is added to @@ -93,9 +93,9 @@ the nearest boundary cell. # Arguments - `pcl`: the point cell list to insert into. - `pos`: point position. -- `point_id`: ID for this point. +- `id`: ID for this point. """ -function cell_point_add!(pcl::PointCellList{T,F}, pos::SVector{3, F}, point_id::T)::Nothing where {T,F} +function cell_point_add!(pcl::PointCellList{T,F}, pos::SVector{3, F}, id::T)::Nothing where {T,F} h2_inv = pcl.inv_half_grid_spacing sz = pcl.size @argcheck all(>(0), sz) @@ -106,10 +106,10 @@ function cell_point_add!(pcl::PointCellList{T,F}, pos::SVector{3, F}, point_id:: # positions don't overflow Int; the prevfloat upper bound keeps the # resulting index ≤ size - 1. pos_norm = clamp.(pos*h2_inv, -F.(sz), prevfloat.(F.(sz))) - i = (floor.((Int,), pos_norm) .+ sz) .>> 1 + i = round2grid(pos_norm, sz) li = i ⋅ strides - entry = CellPointEntry{T,F}(pos, point_id) - pcl.cells[begin + li] = _cell_point_push_entry!(pcl.cells[begin + li], entry) + entry = CellPointEntry{T,F}(pos, id) + pcl.cells[begin + li] = _cell_push_entry!(pcl.cells[begin + li], entry) nothing end @@ -147,8 +147,8 @@ function map_nearby_points( # also keeps far away queries and huge cutoffs from overflowing Int. pos_norm = clamp.(pos * h2_inv, -F.(sz), prevfloat.(F.(sz))) r_cells = cutoff * h2_inv - lo = (floor.((Int,), max.(pos_norm .- r_cells, -F.(sz))) .+ sz) .>> 1 - hi = (floor.((Int,), min.(pos_norm .+ r_cells, prevfloat.(F.(sz)))) .+ sz) .>> 1 + lo = round2grid(max.(pos_norm .- r_cells, -F.(sz)), sz) + hi = round2grid(min.(pos_norm .+ r_cells, prevfloat.(F.(sz))), sz) for iz in lo[3]:hi[3] liz = iz*strides[3] cell_center_z = 2*iz + 1 - sz[3] diff --git a/src/util.jl b/src/util.jl index d77da38..ab60fba 100644 --- a/src/util.jl +++ b/src/util.jl @@ -4,6 +4,17 @@ function relu(x::T)::T where T (x > zero(x) ? x : zero(x)) end +_sqrt_fast(x::Union{Float64, Float32}) = Core.Intrinsics.sqrt_llvm(x) +_sqrt_fast(x) = sqrt(x) + +norm_fast(v) = _sqrt_fast(v ⋅ v) + +# Return the zero based XYZ grid index. +# `pos_norm` has an origin at the center of the grid +# and a scale of 2 units per grid spacing. +# `pos_norm` is expected to be inside the grid. +round2grid(pos_norm::Vec3, sz::Vec3{Int})::Vec3{Int} = (floor.((Int,), pos_norm) .+ sz) .>> 1 + """ ball_intersects_unit_cube(center, radius) -> Bool diff --git a/test/runtests.jl b/test/runtests.jl index 88eeae2..bd4ea22 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -4,9 +4,5 @@ include("test-mindistance.jl") include("test-pointcelllist.jl") -# include("test_naive.jl") - -# include("test_painter-basic.jl") - -# include("test_painter.jl") +include("test-linesegcelllist.jl") diff --git a/test/test-linesegcelllist.jl b/test/test-linesegcelllist.jl new file mode 100644 index 0000000..7c88c70 --- /dev/null +++ b/test/test-linesegcelllist.jl @@ -0,0 +1,361 @@ +using SimplexCellLists +using LinearAlgebra +using StaticArrays +using Test + +# Helper to collect all entries from a LineSegCellList as (ix,iy,iz) => Vector{CellLineSegEntry} +function collect_seg_entries(scl::LineSegCellList{T,F}) where {T,F} + result = Dict{NTuple{3,Int}, Vector{CellLineSegEntry{T,F}}}() + sz = scl.size + cells3 = reshape(scl.cells, sz[1], sz[2], sz[3]) + for iz in 0:sz[3]-1, iy in 0:sz[2]-1, ix in 0:sz[1]-1 + cs = cells3[ix+1,iy+1,iz+1] + if cs.len > 0 + result[(ix,iy,iz)] = collect(view(cs.list, 1:cs.len)) + end + end + result +end + +total_seg_entries(scl::LineSegCellList) = sum(scl.cells[i].len for i in 1:prod(scl.size); init=0) + +# All entries for one line segment id, sorted by tmin. +function seg_entries_for_id(scl::LineSegCellList{T,F}, id::T) where {T,F} + es = CellLineSegEntry{T,F}[] + for c in scl.cells + for j in 1:c.len + c.list[j].id == id && push!(es, c.list[j]) + end + end + sort(es; by=e->e.tmin) +end + +# Check that the entries of one line segment exactly partition [0, seg_len]: +# tmin of the first entry is 0, ranges chain with no gaps or overlaps, and +# only the last entry has is_end set, with tmax == seg_len. +function check_seg_partition(es, seg_len) + @test !isempty(es) + @test es[1].tmin == 0 + for k in 1:length(es)-1 + @test cell_line_seg_tmax(es[k]) == es[k+1].tmin + @test !cell_line_seg_is_end(es[k]) + end + @test cell_line_seg_is_end(es[end]) + # If more than one entry, the last entry cannot be empty + if length(es) > 1 + @test cell_line_seg_tmax(es[end]) != es[end].tmin + end + # ≈ because recomputing seg_len here can differ from the package's + # internal computation by an ulp (fma contraction). + @test cell_line_seg_tmax(es[end]) ≈ seg_len rtol=4*eps(Float32) + # all entries share the segment's geometry + @test allequal(e.p0 for e in es) + @test allequal(e.d_hat for e in es) +end + +@testset "linesegcelllist" begin + # Grid (4,4,4) with spacing 2.0 is centered at origin: + # x: [-4, 4), y: [-4, 4), z: [-4, 4) + # Cell (0,0,0) covers [-4,-2) × [-4,-2) × [-4,-2) + # Cell (2,2,2) covers [0,2) × [0,2) × [0,2) + # Cell (3,3,3) covers [2,4) × [2,4) × [2,4) + + @testset "invalid and degenerate sizes" begin + @test_throws ArgumentError LineSegCellList{Int64,Float32}((-1,4,4), 2.0f0) + # A zero-size axis makes an empty grid: queries find nothing, adds throw. + scl = LineSegCellList{Int64,Float32}((0,4,4), 2.0f0) + count = map_nearby_line_segs(scl, SA[0.5f0, 0.5f0, 0.5f0], 1.0f0, 0) do entry, out + out + 1, true + end + @test count == 0 + @test_throws ArgumentError cell_line_seg_add!(scl, SA[0.5f0, 0.5f0, 0.5f0], SA[1.0f0, 0.5f0, 0.5f0], Int64(1)) + end + + @testset "single segment inside one cell" begin + scl = LineSegCellList{Int64,Float32}((4,4,4), 2.0f0) + cell_line_seg_add!(scl, SA[0.3f0, 0.5f0, 0.5f0], SA[1.2f0, 0.5f0, 0.5f0], Int64(1)) + @test total_seg_entries(scl) == 1 + entries = collect_seg_entries(scl) + @test haskey(entries, (2,2,2)) + e = entries[(2,2,2)][1] + @test e.id == 1 + @test e.p0 == SA[0.3f0, 0.5f0, 0.5f0] + @test e.d_hat == SA[1.0f0, 0.0f0, 0.0f0] + @test e.tmin == 0 + @test cell_line_seg_tmax(e) == 1.2f0 - 0.3f0 + @test cell_line_seg_is_end(e) + end + + @testset "axis-aligned segment spanning cells" begin + scl = LineSegCellList{Int64,Float32}((4,4,4), 2.0f0) + # x from -3 to 3 crosses cell boundaries at x = -2, 0, 2 + cell_line_seg_add!(scl, SA[-3.0f0, 0.5f0, 0.5f0], SA[3.0f0, 0.5f0, 0.5f0], Int64(1)) + @test total_seg_entries(scl) == 4 + entries = collect_seg_entries(scl) + for ix in 0:3 + @test haskey(entries, (ix,2,2)) + end + es = seg_entries_for_id(scl, Int64(1)) + check_seg_partition(es, 6.0f0) + @test [e.tmin for e in es] == [0.0f0, 1.0f0, 3.0f0, 5.0f0] + @test cell_line_seg_tmax.(es) == [1.0f0, 3.0f0, 5.0f0, 6.0f0] + end + + @testset "reversed direction segment" begin + scl = LineSegCellList{Int64,Float32}((4,4,4), 2.0f0) + cell_line_seg_add!(scl, SA[3.0f0, 0.5f0, 0.5f0], SA[-3.0f0, 0.5f0, 0.5f0], Int64(1)) + @test total_seg_entries(scl) == 4 + es = seg_entries_for_id(scl, Int64(1)) + check_seg_partition(es, 6.0f0) + @test es[1].d_hat == SA[-1.0f0, 0.0f0, 0.0f0] + end + + @testset "diagonal segment" begin + scl = LineSegCellList{Int64,Float32}((4,4,4), 2.0f0) + p0 = SA[-3.9f0, -3.9f0, -3.9f0] + p1 = SA[3.9f0, 3.9f0, 3.9f0] + cell_line_seg_add!(scl, p0, p1, Int64(1)) + d = p1 - p0 + seg_len = sqrt(d ⋅ d) + es = seg_entries_for_id(scl, Int64(1)) + # crosses 3 boundaries per axis: 1 + 9 cells visited + @test length(es) == 10 + check_seg_partition(es, seg_len) + entries = collect_seg_entries(scl) + @test haskey(entries, (0,0,0)) + @test haskey(entries, (3,3,3)) + end + + @testset "zero-length segment" begin + scl = LineSegCellList{Int64,Float32}((4,4,4), 2.0f0) + pos = SA[0.5f0, 0.5f0, 0.5f0] + cell_line_seg_add!(scl, pos, pos, Int64(1)) + @test total_seg_entries(scl) == 1 + e = collect_seg_entries(scl)[(2,2,2)][1] + @test e.d_hat == zero(SVector{3,Float32}) + @test e.tmin == 0 + @test cell_line_seg_tmax(e) == 0 + @test cell_line_seg_is_end(e) + count = map_nearby_line_segs(scl, SA[0.5f0, 0.5f0, 1.0f0], 1.0f0, 0) do entry, out + out + 1, true + end + @test count == 1 + end + + @testset "non-finite segments error" begin + scl = LineSegCellList{Int64,Float32}((4,4,4), 2.0f0) + ok = SA[0.5f0, 0.5f0, 0.5f0] + nanp = SA[NaN32, 0.5f0, 0.5f0] + infp = SA[Inf32, 0.5f0, 0.5f0] + @test_throws ArgumentError cell_line_seg_add!(scl, nanp, ok, Int64(1)) + @test_throws ArgumentError cell_line_seg_add!(scl, ok, nanp, Int64(1)) + @test_throws ArgumentError cell_line_seg_add!(scl, infp, ok, Int64(1)) + @test_throws ArgumentError cell_line_seg_add!(scl, ok, infp, Int64(1)) + @test_throws ArgumentError cell_line_seg_add!(scl, infp, infp, Int64(1)) + # finite endpoints whose squared length overflows Float32 + big = SA[1.0f25, 0.5f0, 0.5f0] + @test_throws ArgumentError cell_line_seg_add!(scl, -big, big, Int64(1)) + # nothing was added + @test total_seg_entries(scl) == 0 + end + + @testset "segment along a cell boundary" begin + scl = LineSegCellList{Int64,Float32}((4,4,4), 2.0f0) + # x = 0 is the boundary between x-cells 1 and 2; half-open cells put it in cell 2. + # p1 x of -0.0 makes d_x == -0.0, exercising the 0/0 -> NaN -> Inf tMax fixup. + cell_line_seg_add!(scl, SA[0.0f0, -3.0f0, 0.5f0], SA[-0.0f0, 3.0f0, 0.5f0], Int64(1)) + es = seg_entries_for_id(scl, Int64(1)) + @test length(es) == 4 + check_seg_partition(es, 6.0f0) + entries = collect_seg_entries(scl) + for iy in 0:3 + @test haskey(entries, (2,iy,2)) + end + end + + @testset "multiple segments same position different IDs" begin + scl = LineSegCellList{Int64,Float32}((4,4,4), 2.0f0) + p0 = SA[0.5f0, 0.5f0, 0.5f0] + p1 = SA[1.5f0, 0.5f0, 0.5f0] + cell_line_seg_add!(scl, p0, p1, Int64(10)) + cell_line_seg_add!(scl, p0, p1, Int64(20)) + count = map_nearby_line_segs(scl, p0, 0.1f0, 0) do entry, out + out + 1, true + end + @test count == 2 + end + + @testset "cell_line_segs_clear!" begin + scl = LineSegCellList{Int64,Float32}((4,4,4), 2.0f0) + cell_line_seg_add!(scl, SA[-3.0f0, 0.5f0, 0.5f0], SA[3.0f0, 0.5f0, 0.5f0], Int64(1)) + @test total_seg_entries(scl) == 4 + cell_line_segs_clear!(scl) + @test total_seg_entries(scl) == 0 + end + + @testset "segments outside the grid are clamped to boundary cells" begin + @testset "fully outside" begin + scl = LineSegCellList{Int64,Float32}((4,4,4), 2.0f0) + cell_line_seg_add!(scl, SA[10.0f0, 10.0f0, 10.0f0], SA[12.0f0, 10.0f0, 10.0f0], Int64(1)) + @test total_seg_entries(scl) == 1 + e = collect_seg_entries(scl)[(3,3,3)][1] + @test e.tmin == 0 + @test cell_line_seg_tmax(e) == 2.0f0 + @test cell_line_seg_is_end(e) + end + @testset "crossing the whole grid" begin + scl = LineSegCellList{Int64,Float32}((4,4,4), 2.0f0) + cell_line_seg_add!(scl, SA[-100.0f0, 0.5f0, 0.5f0], SA[100.0f0, 0.5f0, 0.5f0], Int64(1)) + @test total_seg_entries(scl) == 4 + es = seg_entries_for_id(scl, Int64(1)) + check_seg_partition(es, 200.0f0) + # boundary cells own the out-of-grid parts: + # crossings at x = -2, 0, 2, i.e. t = 98, 100, 102 + @test [e.tmin for e in es] == [0.0f0, 98.0f0, 100.0f0, 102.0f0] + @test cell_line_seg_tmax.(es) == [98.0f0, 100.0f0, 102.0f0, 200.0f0] + end + @testset "far outside in several axes" begin + scl = LineSegCellList{Int64,Float32}((4,4,4), 2.0f0) + cell_line_seg_add!(scl, SA[1.0f10, -1.0f10, 3.0f0], SA[1.0f10, -1.0f10, 3.5f0], Int64(1)) + @test total_seg_entries(scl) == 1 + @test haskey(collect_seg_entries(scl), (3,0,3)) + end + end + + @testset "map_nearby_line_segs" begin + @testset "empty" begin + scl = LineSegCellList{Int64,Float32}((4,4,4), 2.0f0) + count = map_nearby_line_segs(scl, SA[0.5f0, 0.5f0, 0.5f0], 0.5f0, 0) do entry, out + out + 1, true + end + @test count == 0 + end + + @testset "single segment found" begin + scl = LineSegCellList{Int64,Float32}((4,4,4), 2.0f0) + cell_line_seg_add!(scl, SA[0.3f0, 0.5f0, 0.5f0], SA[1.2f0, 0.5f0, 0.5f0], Int64(1)) + count = map_nearby_line_segs(scl, SA[0.7f0, 0.9f0, 0.5f0], 0.5f0, 0) do entry, out + out + 1, true + end + @test count == 1 + end + + @testset "no matches - outside cutoff" begin + scl = LineSegCellList{Int64,Float32}((4,4,4), 2.0f0) + cell_line_seg_add!(scl, SA[0.3f0, 0.5f0, 0.5f0], SA[1.2f0, 0.5f0, 0.5f0], Int64(1)) + count = map_nearby_line_segs(scl, SA[-3.0f0, -3.0f0, -3.0f0], 0.5f0, 0) do entry, out + out + 1, true + end + @test count == 0 + end + + @testset "early exit" begin + scl = LineSegCellList{Int64,Float32}((4,4,4), 2.0f0) + cell_line_seg_add!(scl, SA[0.3f0, 0.5f0, 0.5f0], SA[1.2f0, 0.5f0, 0.5f0], Int64(1)) + cell_line_seg_add!(scl, SA[0.3f0, 0.6f0, 0.5f0], SA[1.2f0, 0.6f0, 0.5f0], Int64(2)) + count = map_nearby_line_segs(scl, SA[0.5f0, 0.5f0, 0.5f0], 1.0f0, 0) do entry, out + out + 1, false + end + @test count == 1 + end + + @testset "reported exactly once across cells" begin + scl = LineSegCellList{Int64,Float32}((4,4,4), 2.0f0) + cell_line_seg_add!(scl, SA[-3.5f0, -3.5f0, -3.5f0], SA[3.5f0, 3.5f0, 3.5f0], Int64(1)) + # cutoff big enough that every cell the segment touches is visited + ids = map_nearby_line_segs(scl, SA[0.0f0, 0.0f0, 0.0f0], 20.0f0, Int64[]) do entry, out + push!(out, entry.id) + out, true + end + @test ids == [1] + end + + @testset "segment across cell boundary within cutoff" begin + scl = LineSegCellList{Int64,Float32}((4,4,4), 2.0f0) + # Segment just inside x-cell 1, query just inside x-cell 2 + cell_line_seg_add!(scl, SA[-0.1f0, 0.5f0, 0.5f0], SA[-0.1f0, 1.5f0, 0.5f0], Int64(1)) + count = map_nearby_line_segs(scl, SA[0.1f0, 1.0f0, 0.5f0], 0.5f0, 0) do entry, out + out + 1, true + end + @test count == 1 + end + + @testset "query from outside grid finds nearby segment" begin + scl = LineSegCellList{Int64,Float32}((4,4,4), 2.0f0) + cell_line_seg_add!(scl, SA[3.5f0, 3.0f0, 3.5f0], SA[3.5f0, 3.8f0, 3.5f0], Int64(1)) + count = map_nearby_line_segs(scl, SA[4.5f0, 3.5f0, 3.5f0], 2.0f0, 0) do entry, out + out + 1, true + end + @test count == 1 + end + + @testset "query from outside grid finds segments also outside grid" begin + scl = LineSegCellList{Int64,Float32}((4,4,4), 2.0f0) + # Grid covers [-4,4). Segment and query are both beyond the +x face. + cell_line_seg_add!(scl, SA[10.0f0, 0.5f0, 0.5f0], SA[12.0f0, 0.5f0, 0.5f0], Int64(1)) + count = map_nearby_line_segs(scl, SA[11.0f0, 0.5f0, 1.5f0], 2.0f0, 0) do entry, out + out + 1, true + end + @test count == 1 + # The same query must not return an outside segment beyond the cutoff. + cell_line_seg_add!(scl, SA[10.0f0, 0.5f0, 30.0f0], SA[12.0f0, 0.5f0, 30.0f0], Int64(2)) + count = map_nearby_line_segs(scl, SA[11.0f0, 0.5f0, 1.5f0], 2.0f0, 0) do entry, out + out + 1, true + end + @test count == 1 + end + + @testset "zero and negative cutoff return out" begin + scl = LineSegCellList{Int64,Float32}((4,4,4), 2.0f0) + cell_line_seg_add!(scl, SA[0.3f0, 0.5f0, 0.5f0], SA[1.2f0, 0.5f0, 0.5f0], Int64(1)) + for cutoff in (0.0f0, -1.0f0) + result = map_nearby_line_segs(scl, SA[0.5f0, 0.5f0, 0.5f0], cutoff, 42) do entry, out + out + 1, true + end + @test result == 42 + end + end + + @testset "brute force comparison" begin + for trial in 1:5000 + scl = LineSegCellList{Int64,Float32}((10,10,10), 2.0f0) + # Grid covers [-10,10)^3, but segments and queries are sampled in + # [-12,12)^3 so some fall outside the grid and get clamped. + n_segs = 500 + segs = map(1:n_segs) do i + p0 = rand(SVector{3,Float32}) .* 24f0 .- 12f0 + p1 = if i % 20 == 0 + p0 # occasional zero-length segment + elseif i % 5 == 0 + rand(SVector{3,Float32}) .* 24f0 .- 12f0 # long segment + else + p0 + rand(SVector{3,Float32}) .* 4f0 .- 2f0 + end + SA[p0, p1] + end + for i in 1:n_segs + cell_line_seg_add!(scl, segs[i][1], segs[i][2], Int64(i)) + end + + for _ in 1:20 + cutoff = Float32(rand()) * 8f0 + cutoff2 = cutoff * cutoff + q = rand(SVector{3,Float32}) .* 24f0 .- 12f0 + cell_ids = map_nearby_line_segs(scl, q, cutoff, Int64[]) do entry, out + push!(out, entry.id) + out, true + end + # each segment reported at most once + @test allunique(cell_ids) + brute_ids = [Int64(i) for i in 1:n_segs if dist_sqr(SA[q], segs[i]) ≤ cutoff2] + # Any id in one set but not the other must be right on the cutoff boundary. + for id in symdiff(cell_ids, brute_ids) + d2 = dist_sqr(SA[q], SA[segs[id][1], segs[id][2]]) + @test d2 ≈ cutoff2 rtol=1e-4 atol=1f-4 + end + end + end + end + end +end +nothing diff --git a/test/test-pointcelllist.jl b/test/test-pointcelllist.jl index 52aa462..475f9f7 100644 --- a/test/test-pointcelllist.jl +++ b/test/test-pointcelllist.jl @@ -6,9 +6,10 @@ using Test # Helper to collect all entries from a PointCellList as (ix,iy,iz) => Vector{CellPointEntry} function collect_entries(pcl::PointCellList{T,F}) where {T,F} result = Dict{NTuple{3,Int}, Vector{CellPointEntry{T,F}}}() - for iz in 0:pcl.size[3]-1, iy in 0:pcl.size[2]-1, ix in 0:pcl.size[1]-1 - # li = SA[ix,iy,iz] ⋅ pcl.strides - cs = reshape(pcl.cells, pcl.size[1], pcl.size[2], pcl.size[3])[ix+1,iy+1,iz+1] + sz = pcl.size + cells3 = reshape(pcl.cells, sz[1], sz[2], sz[3]) + for iz in 0:sz[3]-1, iy in 0:sz[2]-1, ix in 0:sz[1]-1 + cs = cells3[ix+1,iy+1,iz+1] if cs.len > 0 result[(ix,iy,iz)] = collect(view(cs.list, 1:cs.len)) end @@ -22,18 +23,18 @@ Brute-force O(n) version of `map_nearby_points` for testing. function map_nearby_points_naive( f, positions::AbstractVector{SVector{3,Float32}}, - point_ids::AbstractVector{Int64}, + ids::AbstractVector{Int64}, pos::SVector{3,Float32}, cutoff::Float32, out, ) cutoff ≤ 0 && return out cutoff2 = cutoff * cutoff - for i in eachindex(positions, point_ids) + for i in eachindex(positions, ids) diff = pos - positions[i] dist2 = diff ⋅ diff if dist2 ≤ cutoff2 - out, cont = f(CellPointEntry{Int64,Float32}(positions[i], point_ids[i]), out) + out, cont = f(CellPointEntry{Int64,Float32}(positions[i], ids[i]), out) cont || return out end end @@ -66,7 +67,7 @@ total_entries(pcl::PointCellList) = sum(pcl.cells[i].len for i in 1:prod(pcl.siz entries = collect_entries(pcl) @test haskey(entries, (2,2,2)) e = entries[(2,2,2)][1] - @test e.point_id == 1 + @test e.id == 1 @test e.pos == SA[0.5f0, 0.5f0, 0.5f0] end @@ -77,7 +78,7 @@ total_entries(pcl::PointCellList) = sum(pcl.cells[i].len for i in 1:prod(pcl.siz entries = collect_entries(pcl) @test haskey(entries, (0,0,0)) e = entries[(0,0,0)][1] - @test e.point_id == 2 + @test e.id == 2 end @testset "point in last cell" begin @@ -108,8 +109,8 @@ total_entries(pcl::PointCellList) = sum(pcl.cells[i].len for i in 1:prod(pcl.siz entries = collect_entries(pcl) @test haskey(entries, (0,0,0)) @test haskey(entries, (3,3,3)) - @test entries[(0,0,0)][1].point_id == 1 - @test entries[(3,3,3)][1].point_id == 2 + @test entries[(0,0,0)][1].id == 1 + @test entries[(3,3,3)][1].id == 2 end @testset "point on cell boundary" begin @@ -144,7 +145,7 @@ total_entries(pcl::PointCellList) = sum(pcl.cells[i].len for i in 1:prod(pcl.siz entries = collect_entries(pcl) es = entries[(2,2,2)] @test length(es) == n - @test issetequal(map(e->e.point_id, es), 1:n) + @test issetequal(map(e->e.id, es), 1:n) @test total_entries(pcl) == n end @@ -159,12 +160,12 @@ total_entries(pcl::PointCellList) = sum(pcl.cells[i].len for i in 1:prod(pcl.siz @test count == 2 end - @testset "cell_point_clear!" begin + @testset "cell_points_clear!" begin pcl = PointCellList{Int64,Float32}((4,4,4), 2.0f0) cell_point_add!(pcl, SA[0.5f0, 0.5f0, 0.5f0], Int64(1)) cell_point_add!(pcl, SA[-3.5f0, -3.5f0, -3.5f0], Int64(2)) @test total_entries(pcl) == 2 - cell_point_clear!(pcl) + cell_points_clear!(pcl) @test total_entries(pcl) == 0 end @@ -178,9 +179,9 @@ total_entries(pcl::PointCellList) = sum(pcl.cells[i].len for i in 1:prod(pcl.siz cell_point_add!(pcl, SA[-4.0f0, 0.5f0, 0.5f0], Int64(5)) # exactly on the lower bound @test total_entries(pcl) == 5 entries = collect_entries(pcl) - @test map(e->e.point_id, entries[(3,2,2)]) == [1, 4] - @test map(e->e.point_id, entries[(0,2,2)]) == [2, 5] - @test entries[(3,0,3)][1].point_id == 3 + @test map(e->e.id, entries[(3,2,2)]) == [1, 4] + @test map(e->e.id, entries[(0,2,2)]) == [2, 5] + @test entries[(3,0,3)][1].id == 3 end @testset "map_nearby_points" begin @@ -283,21 +284,22 @@ total_entries(pcl::PointCellList) = sum(pcl.cells[i].len for i in 1:prod(pcl.siz # Grid covers [-10,10)^3, but points and queries are sampled in # [-12,12)^3 so some fall outside the grid and get clamped. n_pts = 500 - positions = [SVector{3,Float32}(rand(Float32, 3) .* 24f0 .- 12f0) for _ in 1:n_pts] + positions = [rand(SVector{3,Float32}) .* 24f0 .- 12f0 for _ in 1:n_pts] for i in 1:n_pts cell_point_add!(pcl, positions[i], Int64(i)) end function collect_id(entry, out) - push!(out, entry.point_id) + push!(out, entry.id) out, true end for _ in 1:20 cutoff = Float32(rand()) * 8f0 cutoff2 = cutoff * cutoff - q = SVector{3,Float32}(rand(Float32, 3) .* 24f0 .- 12f0) - cell_ids = map_nearby_points(collect_id, pcl, q, cutoff, Set{Int64}()) - brute_ids = map_nearby_points_naive(collect_id, positions, Int64(1):n_pts, q, cutoff, Set{Int64}()) + q = rand(SVector{3,Float32}) .* 24f0 .- 12f0 + cell_ids = map_nearby_points(collect_id, pcl, q, cutoff, Int64[]) + @test allunique(cell_ids) + brute_ids = map_nearby_points_naive(collect_id, positions, Int64(1):n_pts, q, cutoff, Int64[]) # Any id in one set but not the other must be right on the cutoff boundary. for id in symdiff(cell_ids, brute_ids) diff = q - positions[id] From e194cea6eba842738a1bc886484ad9442ea87538 Mon Sep 17 00:00:00 2001 From: nhz2 Date: Sun, 9 Aug 2026 16:11:43 +0200 Subject: [PATCH 03/14] benchmarking --- Project.toml | 2 +- benchmark/Project.toml | 6 ++ benchmark/distancesbench.jl | 26 ++--- benchmark/painterbench.jl | 53 ---------- benchmark/ring-bench.jl | 125 +++++++++++++++--------- benchmark/ring-system-line-segs-f32.bin | Bin 0 -> 128400 bytes benchmark/ringsystemsnapshot.h5 | Bin 167418 -> 0 bytes src/linesegcelllist.jl | 31 ++++-- src/pointcelllist.jl | 27 +++-- 9 files changed, 137 insertions(+), 133 deletions(-) create mode 100644 benchmark/Project.toml delete mode 100644 benchmark/painterbench.jl create mode 100644 benchmark/ring-system-line-segs-f32.bin delete mode 100644 benchmark/ringsystemsnapshot.h5 diff --git a/Project.toml b/Project.toml index aa244c1..8ef47dd 100644 --- a/Project.toml +++ b/Project.toml @@ -14,4 +14,4 @@ StaticArrays = "1" julia = "1.12" [workspace] -projects = ["test"] +projects = ["test", "benchmark"] diff --git a/benchmark/Project.toml b/benchmark/Project.toml new file mode 100644 index 0000000..4ae27d2 --- /dev/null +++ b/benchmark/Project.toml @@ -0,0 +1,6 @@ +[deps] +Chairmarks = "0ca39b1e-fe0b-4e98-acfc-b1656634c4de" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +SimplexCellLists = "fbe69cd6-f244-4ce4-a90b-8979d86c9ea4" +StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" diff --git a/benchmark/distancesbench.jl b/benchmark/distancesbench.jl index e5485ee..bb8f3d8 100644 --- a/benchmark/distancesbench.jl +++ b/benchmark/distancesbench.jl @@ -2,11 +2,11 @@ using StaticArrays using LinearAlgebra using SimplexCellLists using Random -using BenchmarkTools +using Chairmarks function dist2linelinevect!(r,a,b) @inbounds for i in eachindex(r, a, b) - @inline r[i] = SimplexCellLists.dist2LineLine(a[i],b[i]) + @inline r[i] = dist_sqr(a[i],b[i]) end end @@ -15,13 +15,13 @@ a = rand(SVector{2,SVector{3,Float32}}, N) b = rand(SVector{2,SVector{3,Float32}}, N) r = zeros(Float32, N) println("line line") -@btime dist2linelinevect!($r,$a,$b) +display(@b dist2linelinevect!($r,$a,$b)) println() function dist2pointlinevect!(r,a,b) @inbounds for i in eachindex(r, a, b) - @inline r[i] = SimplexCellLists.dist2PointLine(a[i],b[i]) + @inline r[i] = dist_sqr(a[i],b[i]) end end @@ -30,12 +30,12 @@ a = rand(SVector{1,SVector{3,Float32}}, N) b = rand(SVector{2,SVector{3,Float32}}, N) r = zeros(Float32, N) println("point line") -@btime dist2pointlinevect!($r,$a,$b) +display(@b dist2pointlinevect!($r,$a,$b)) println() function dist2pointpointvect!(r,a,b) @inbounds for i in eachindex(r, a, b) - @inline r[i] = SimplexCellLists.dist2PointPoint(a[i],b[i]) + @inline r[i] = dist_sqr(a[i],b[i]) end end @@ -44,12 +44,12 @@ a = rand(SVector{1,SVector{3,Float32}}, N) b = rand(SVector{1,SVector{3,Float32}}, N) r = zeros(Float32, N) println("point point") -@btime dist2pointpointvect!($r,$a,$b) +display(@b dist2pointpointvect!($r,$a,$b)) println() function dist2pointtrianglevect!(r,a,b) @inbounds for i in eachindex(r, a, b) - @inline r[i] = SimplexCellLists.dist2PointTriangle(a[i],b[i]) + @inline r[i] = dist_sqr(a[i],b[i]) end end @@ -58,12 +58,12 @@ a = rand(SVector{1,SVector{3,Float32}}, N) b = rand(SVector{3,SVector{3,Float32}}, N) r = zeros(Float32, N) println("point triangle") -@btime dist2pointtrianglevect!($r,$a,$b) +display(@b dist2pointtrianglevect!($r,$a,$b)) println() function dist2linetrianglevect!(r,a,b) @inbounds for i in eachindex(r, a, b) - @inline r[i] = SimplexCellLists.dist2LineTriangle(a[i],b[i]) + @inline r[i] = dist_sqr(a[i],b[i]) end end @@ -72,12 +72,12 @@ a = rand(SVector{2,SVector{3,Float32}}, N) b = rand(SVector{3,SVector{3,Float32}}, N) r = zeros(Float32, N) println("line triangle") -@btime dist2linetrianglevect!($r,$a,$b) +display(@b dist2linetrianglevect!($r,$a,$b)) println() function dist2triangletrianglevect!(r,a,b) @inbounds for i in eachindex(r, a, b) - @inline r[i] = SimplexCellLists.dist2TriangleTriangle(a[i],b[i]) + @inline r[i] = dist_sqr(a[i],b[i]) end end @@ -86,5 +86,5 @@ a = rand(SVector{3,SVector{3,Float32}}, N) b = rand(SVector{3,SVector{3,Float32}}, N) r = zeros(Float32, N) println("triangle triangle") -@btime dist2triangletrianglevect!($r,$a,$b) +display(@b dist2triangletrianglevect!($r,$a,$b)) println() \ No newline at end of file diff --git a/benchmark/painterbench.jl b/benchmark/painterbench.jl deleted file mode 100644 index 968e047..0000000 --- a/benchmark/painterbench.jl +++ /dev/null @@ -1,53 +0,0 @@ -using BenchmarkTools -using JET -using SimplexCellLists -using StaticArrays - -function run1(celllist, points) - cutoff = 0.09f0 - function f(x::SimplexCellLists.Point,y::SimplexCellLists.Point,i::Int32,j::Int32,d2::Float32,output::Float64)::Float64 - output+((√(d2)-0.09f0)^2) - end - sum(points) do point - SimplexCellLists.mapSimplexElements( - f, - 0.0, - celllist, - 1, - point, - SimplexCellLists.Point, - cutoff, - ) - end -end - -function run2(celllist, point) - cutoff = 0.09f0 - f(x,y,i,j,d2,output) = output+((√(d2)-cutoff)^2) - SimplexCellLists.mapSimplexElements( - f, - 0.0, - celllist, - 1, - point, - SimplexCellLists.Point, - cutoff, - ) -end - - -N = 100000 -M = 100 -points = rand(SVector{1,SVector{3,Float32}},N) -otherpoints = rand(SVector{1,SVector{3,Float32}},100) -naive = SimplexCellLists.Naive(1, 0) -painter = SimplexCellLists.Painter(1,0; - grid_start= SA[0.0,0.0,0.0], - grid_size= SA[10,10,10], - voxel_length= 1/10, - max_range= SA[[0.1],Float64[]], -) -SimplexCellLists.setElements!(naive,[points],[]) -SimplexCellLists.setElements!(painter,[points],[]) -@btime run1(painter, otherpoints) -@btime run2(painter, otherpoints[1]) \ No newline at end of file diff --git a/benchmark/ring-bench.jl b/benchmark/ring-bench.jl index 94324fb..9b24332 100644 --- a/benchmark/ring-bench.jl +++ b/benchmark/ring-bench.jl @@ -1,60 +1,89 @@ -using BenchmarkTools -using JET +using Chairmarks +using Random using SimplexCellLists using StaticArrays -using HDF5 -import H5Zblosc -function setup() - file = h5open(joinpath(@__DIR__, "ringsystemsnapshot.h5"), "r") +function load_cylinders() + data = read(joinpath(@__DIR__, "ring-system-line-segs-f32.bin")) + cylinders = collect(reinterpret(SimplexCellLists.LineSeg{Float32}, data)) + # center + cylinders .+= (SA[SA[-2000,-2000,-200], SA[-2000,-2000,-200]],) + cylinders +end - nodepositions = read(file["filaments/1/nodepositions"]) - numcylinders = read(file["filaments/1/num_cylinders"]) - numfilaments = length(numcylinders) +# Grid dimensions are 4000 nm by 4000 nm by 400 nm +# Benchmark building and query time with 50 nm, 100 nm, and 200 nm grid spacing and 10 nm, 50 nm, 100 nm, and 200 nm cutoff. +# Query time is measured for random points on the grid, and for cylinder end points - cylinders = SimplexCellLists.Line[] +function build!(scl::LineSegCellList{Int64,Float32}, cylinders) + cell_line_segs_clear!(scl) + for (i, cyl) in enumerate(cylinders) + cell_line_seg_add!(scl, cyl[1], cyl[2], i) + end +end - node_ptr = 0 - for filament_i in 1:numfilaments - node_ptr += 1 - for cylinder_i in 1:numcylinders[filament_i] - push!(cylinders,[nodepositions[node_ptr,:],nodepositions[node_ptr+1,:]]) - node_ptr += 1 - end +function count_nearby(scl::LineSegCellList{Int64,Float32}, points, cutoff::Float32) + total = 0 + for pos in points + total = map_nearby_line_segs((entry, out) -> (out + 1, true), scl, pos, cutoff, total) end - return cylinders + total end +function build!(pcl::PointCellList{Int64,Float32}, points) + cell_points_clear!(pcl) + for (i, pos) in enumerate(points) + cell_point_add!(pcl, pos, i) + end +end -function run1(cylinders) - output = 0 - f(x,y,i,j,d2,output) = output + 1 - painter = SimplexCellLists.Painter(0,1; - grid_start= SA[0.0,0.0,0.0], - grid_size= SA[101,101,11], - voxel_length= 40.0, - max_range= SA[Float64[],[226.0]], - ) - naive = SimplexCellLists.Naive(0, 1) - # SimplexCellLists.setElements!(naive,[cylinders],[]) - SimplexCellLists.setElements!(painter,[],[cylinders]) - SimplexCellLists.mapPairElements(f, output, painter, 1, SimplexCellLists.Line, 225.0f0) +function count_nearby(pcl::PointCellList{Int64,Float32}, points, cutoff::Float32) + total = 0 + for pos in points + total = map_nearby_points((entry, out) -> (out + 1, true), pcl, pos, cutoff, total) + end + total end -function run1(cylinders) - f(x,y,i,j,d2,output) = output + 1 - painter = SimplexCellLists.Painter(0,1; - grid_start= SA[200.0,200.0,200.0], - grid_size= SA[10,10,1], - voxel_length= 400.0, - max_range= SA[Float64[],[226.0]], - ) - #naive = SimplexCellLists.Naive(0, 1) - #SimplexCellLists.setElements!(naive,[],[cylinders]) - SimplexCellLists.setElements!( - painter, - Vector{SimplexCellLists.Point}[], - Vector{SimplexCellLists.Line}[cylinders]) - SimplexCellLists.mapPairElements(f, 0, painter, 1, SimplexCellLists.Line, 225.0f0) - #SimplexCellLists.mapPairElements(f, output, naive, 1, SimplexCellLists.Line, 225.0f0) -end \ No newline at end of file +const grid_dims = SA[4000.0f0, 4000.0f0, 400.0f0] + +cylinders = load_cylinders() +end_points = [cyl[2] for cyl in cylinders] +Random.seed!(1234) +random_points = [(rand(SimplexCellLists.Vec3{Float32}) .- 0.5f0) .* grid_dims for _ in eachindex(cylinders)] + +for grid_spacing in Float32[100, 200] + grid_size = round.(Int, grid_dims ./ grid_spacing) + scl = LineSegCellList{Int64, Float32}(Tuple(grid_size), grid_spacing) + println("$(length(cylinders)) line segments, grid spacing $grid_spacing nm, grid size $(Tuple(grid_size))") + print(" build:") + display(@b build!($scl, $cylinders) evals=1) + build!(scl, cylinders) + for cutoff in Float32[10, 50, 100] + n = count_nearby(scl, random_points, cutoff) + print(" cutoff $cutoff nm, $(length(random_points)) random points ($n hits):") + display(@b count_nearby($scl, $random_points, $cutoff) evals=1) + n = count_nearby(scl, end_points, cutoff) + print(" cutoff $cutoff nm, $(length(end_points)) end points ($n hits):") + display(@b count_nearby($scl, $end_points, $cutoff) evals=1) + end + println() +end + +for grid_spacing in Float32[100, 200] + grid_size = round.(Int, grid_dims ./ grid_spacing) + pcl = PointCellList{Int64, Float32}(Tuple(grid_size), grid_spacing) + println("$(length(end_points)) points, grid spacing $grid_spacing nm, grid size $(Tuple(grid_size))") + print(" build:") + display(@b build!($pcl, $end_points) evals=1) + build!(pcl, end_points) + for cutoff in Float32[10, 50, 100] + n = count_nearby(pcl, random_points, cutoff) + print(" cutoff $cutoff nm, $(length(random_points)) random points ($n hits):") + display(@b count_nearby($pcl, $random_points, $cutoff) evals=1) + n = count_nearby(pcl, end_points, cutoff) + print(" cutoff $cutoff nm, $(length(end_points)) end points ($n hits):") + display(@b count_nearby($pcl, $end_points, $cutoff) evals=1) + end + println() +end diff --git a/benchmark/ring-system-line-segs-f32.bin b/benchmark/ring-system-line-segs-f32.bin new file mode 100644 index 0000000000000000000000000000000000000000..2fd71b7bd7ae0c1fdc0d9f09b2b59d7312295fb2 GIT binary patch literal 128400 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%0Dokw6AP6o=`L6PkyePT|gi4clNCq<=TASOZ^#k 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 G-~a#ah3str literal 0 HcmV?d00001 diff --git a/benchmark/ringsystemsnapshot.h5 b/benchmark/ringsystemsnapshot.h5 deleted file mode 100644 index fa99bd7298cb84ba4faf71b529add8db7f98573b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 167418 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 wwRKyXn(McsmN(xh_+mtLn#Lt;Ks?FjhQlXw Date: Sun, 9 Aug 2026 18:10:43 +0200 Subject: [PATCH 04/14] swap diff --- src/linesegcelllist.jl | 2 +- src/pointcelllist.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/linesegcelllist.jl b/src/linesegcelllist.jl index edd8198..382d8bf 100644 --- a/src/linesegcelllist.jl +++ b/src/linesegcelllist.jl @@ -245,7 +245,7 @@ function map_nearby_line_segs( pos_m_p0 = pos - entry.p0 t = clamp(pos_m_p0 ⋅ entry.d_hat, zero(F), tmax) # distance check - diff = pos_m_p0 - t * entry.d_hat + diff = t * entry.d_hat - pos_m_p0 d2 = diff ⋅ diff d2 ≤ cutoff2 || continue # ownership check: tmin ≤ t < tmax, or is_end diff --git a/src/pointcelllist.jl b/src/pointcelllist.jl index 7667767..b33b435 100644 --- a/src/pointcelllist.jl +++ b/src/pointcelllist.jl @@ -175,7 +175,7 @@ function map_nearby_points( cs = @inbounds pcl.cells[begin + li] for j in 1:cs.len entry = @inbounds cs.list[j] - diff = pos - entry.pos + diff = entry.pos - pos d2 = diff ⋅ diff d2 ≤ cutoff2 || continue out, cont = f(entry, out) From df7f28c1b608b901b57420e394e0a3d7587c1e78 Mon Sep 17 00:00:00 2001 From: nhz2 Date: Sun, 9 Aug 2026 18:31:08 +0200 Subject: [PATCH 05/14] add sep arg to map function --- benchmark/ring-bench.jl | 4 ++-- src/linesegcelllist.jl | 12 ++++++---- src/pointcelllist.jl | 9 ++++---- test/test-linesegcelllist.jl | 40 ++++++++++++++++++++------------ test/test-pointcelllist.jl | 44 +++++++++++++++++++++++------------- 5 files changed, 68 insertions(+), 41 deletions(-) diff --git a/benchmark/ring-bench.jl b/benchmark/ring-bench.jl index 9b24332..101e61a 100644 --- a/benchmark/ring-bench.jl +++ b/benchmark/ring-bench.jl @@ -25,7 +25,7 @@ end function count_nearby(scl::LineSegCellList{Int64,Float32}, points, cutoff::Float32) total = 0 for pos in points - total = map_nearby_line_segs((entry, out) -> (out + 1, true), scl, pos, cutoff, total) + total = map_nearby_line_segs((entry, sep, out) -> (out + 1, true), scl, pos, cutoff, total) end total end @@ -40,7 +40,7 @@ end function count_nearby(pcl::PointCellList{Int64,Float32}, points, cutoff::Float32) total = 0 for pos in points - total = map_nearby_points((entry, out) -> (out + 1, true), pcl, pos, cutoff, total) + total = map_nearby_points((entry, sep, out) -> (out + 1, true), pcl, pos, cutoff, total) end total end diff --git a/src/linesegcelllist.jl b/src/linesegcelllist.jl index 382d8bf..09d0090 100644 --- a/src/linesegcelllist.jl +++ b/src/linesegcelllist.jl @@ -189,7 +189,9 @@ Call `f` for every line segment in `scl` whose closest point to `pos` is within Each line segment is reported exactly once (deduplicated across cells). # Arguments -- `f(entry::CellLineSegEntry{T,F}, out) -> (out, cont::Bool)`: callback called for each nearby line segment. +- `f(entry::CellLineSegEntry{T,F}, sep::SVector{3,F}, out) -> (out, cont::Bool)`: callback called + for each nearby line segment. `sep` is the separation vector from `pos` to the closest point on the + line segment. Return `(new_out, true)` to continue, or `(new_out, false)` to stop early. - `scl`: the line segment cell list to query. - `pos`: query point. @@ -244,14 +246,14 @@ function map_nearby_line_segs( tmax = cell_line_seg_tmax(entry) pos_m_p0 = pos - entry.p0 t = clamp(pos_m_p0 ⋅ entry.d_hat, zero(F), tmax) - # distance check - diff = t * entry.d_hat - pos_m_p0 - d2 = diff ⋅ diff + # distance check. + sep = t * entry.d_hat - pos_m_p0 + d2 = sep ⋅ sep d2 ≤ cutoff2 || continue # ownership check: tmin ≤ t < tmax, or is_end t ≥ entry.tmin || continue (t < tmax || cell_line_seg_is_end(entry)) || continue - out, cont = f(entry, out) + out, cont = f(entry, sep, out) cont || return out end end diff --git a/src/pointcelllist.jl b/src/pointcelllist.jl index b33b435..48f5baf 100644 --- a/src/pointcelllist.jl +++ b/src/pointcelllist.jl @@ -122,7 +122,8 @@ end Call `f` for every point in `pcl` within `cutoff` distance of `pos`. # Arguments -- `f(entry::CellPointEntry{T,F}, out) -> (out, cont::Bool)`: callback called for each nearby point. +- `f(entry::CellPointEntry{T,F}, sep::SVector{3,F}, out) -> (out, cont::Bool)`: callback called + for each nearby point. `sep` is the separation vector from `pos` to the point (`entry.pos - pos`). Return `(new_out, true)` to continue, or `(new_out, false)` to stop early. - `pcl`: the point cell list to query. - `pos`: query point. @@ -175,10 +176,10 @@ function map_nearby_points( cs = @inbounds pcl.cells[begin + li] for j in 1:cs.len entry = @inbounds cs.list[j] - diff = entry.pos - pos - d2 = diff ⋅ diff + sep = entry.pos - pos + d2 = sep ⋅ sep d2 ≤ cutoff2 || continue - out, cont = f(entry, out) + out, cont = f(entry, sep, out) cont || return out end end diff --git a/test/test-linesegcelllist.jl b/test/test-linesegcelllist.jl index 7c88c70..ff34bce 100644 --- a/test/test-linesegcelllist.jl +++ b/test/test-linesegcelllist.jl @@ -64,7 +64,7 @@ end @test_throws ArgumentError LineSegCellList{Int64,Float32}((-1,4,4), 2.0f0) # A zero-size axis makes an empty grid: queries find nothing, adds throw. scl = LineSegCellList{Int64,Float32}((0,4,4), 2.0f0) - count = map_nearby_line_segs(scl, SA[0.5f0, 0.5f0, 0.5f0], 1.0f0, 0) do entry, out + count = map_nearby_line_segs(scl, SA[0.5f0, 0.5f0, 0.5f0], 1.0f0, 0) do entry, sep, out out + 1, true end @test count == 0 @@ -136,7 +136,7 @@ end @test e.tmin == 0 @test cell_line_seg_tmax(e) == 0 @test cell_line_seg_is_end(e) - count = map_nearby_line_segs(scl, SA[0.5f0, 0.5f0, 1.0f0], 1.0f0, 0) do entry, out + count = map_nearby_line_segs(scl, SA[0.5f0, 0.5f0, 1.0f0], 1.0f0, 0) do entry, sep, out out + 1, true end @test count == 1 @@ -179,7 +179,7 @@ end p1 = SA[1.5f0, 0.5f0, 0.5f0] cell_line_seg_add!(scl, p0, p1, Int64(10)) cell_line_seg_add!(scl, p0, p1, Int64(20)) - count = map_nearby_line_segs(scl, p0, 0.1f0, 0) do entry, out + count = map_nearby_line_segs(scl, p0, 0.1f0, 0) do entry, sep, out out + 1, true end @test count == 2 @@ -225,7 +225,7 @@ end @testset "map_nearby_line_segs" begin @testset "empty" begin scl = LineSegCellList{Int64,Float32}((4,4,4), 2.0f0) - count = map_nearby_line_segs(scl, SA[0.5f0, 0.5f0, 0.5f0], 0.5f0, 0) do entry, out + count = map_nearby_line_segs(scl, SA[0.5f0, 0.5f0, 0.5f0], 0.5f0, 0) do entry, sep, out out + 1, true end @test count == 0 @@ -234,7 +234,7 @@ end @testset "single segment found" begin scl = LineSegCellList{Int64,Float32}((4,4,4), 2.0f0) cell_line_seg_add!(scl, SA[0.3f0, 0.5f0, 0.5f0], SA[1.2f0, 0.5f0, 0.5f0], Int64(1)) - count = map_nearby_line_segs(scl, SA[0.7f0, 0.9f0, 0.5f0], 0.5f0, 0) do entry, out + count = map_nearby_line_segs(scl, SA[0.7f0, 0.9f0, 0.5f0], 0.5f0, 0) do entry, sep, out out + 1, true end @test count == 1 @@ -243,7 +243,7 @@ end @testset "no matches - outside cutoff" begin scl = LineSegCellList{Int64,Float32}((4,4,4), 2.0f0) cell_line_seg_add!(scl, SA[0.3f0, 0.5f0, 0.5f0], SA[1.2f0, 0.5f0, 0.5f0], Int64(1)) - count = map_nearby_line_segs(scl, SA[-3.0f0, -3.0f0, -3.0f0], 0.5f0, 0) do entry, out + count = map_nearby_line_segs(scl, SA[-3.0f0, -3.0f0, -3.0f0], 0.5f0, 0) do entry, sep, out out + 1, true end @test count == 0 @@ -253,7 +253,7 @@ end scl = LineSegCellList{Int64,Float32}((4,4,4), 2.0f0) cell_line_seg_add!(scl, SA[0.3f0, 0.5f0, 0.5f0], SA[1.2f0, 0.5f0, 0.5f0], Int64(1)) cell_line_seg_add!(scl, SA[0.3f0, 0.6f0, 0.5f0], SA[1.2f0, 0.6f0, 0.5f0], Int64(2)) - count = map_nearby_line_segs(scl, SA[0.5f0, 0.5f0, 0.5f0], 1.0f0, 0) do entry, out + count = map_nearby_line_segs(scl, SA[0.5f0, 0.5f0, 0.5f0], 1.0f0, 0) do entry, sep, out out + 1, false end @test count == 1 @@ -263,7 +263,7 @@ end scl = LineSegCellList{Int64,Float32}((4,4,4), 2.0f0) cell_line_seg_add!(scl, SA[-3.5f0, -3.5f0, -3.5f0], SA[3.5f0, 3.5f0, 3.5f0], Int64(1)) # cutoff big enough that every cell the segment touches is visited - ids = map_nearby_line_segs(scl, SA[0.0f0, 0.0f0, 0.0f0], 20.0f0, Int64[]) do entry, out + ids = map_nearby_line_segs(scl, SA[0.0f0, 0.0f0, 0.0f0], 20.0f0, Int64[]) do entry, sep, out push!(out, entry.id) out, true end @@ -274,7 +274,7 @@ end scl = LineSegCellList{Int64,Float32}((4,4,4), 2.0f0) # Segment just inside x-cell 1, query just inside x-cell 2 cell_line_seg_add!(scl, SA[-0.1f0, 0.5f0, 0.5f0], SA[-0.1f0, 1.5f0, 0.5f0], Int64(1)) - count = map_nearby_line_segs(scl, SA[0.1f0, 1.0f0, 0.5f0], 0.5f0, 0) do entry, out + count = map_nearby_line_segs(scl, SA[0.1f0, 1.0f0, 0.5f0], 0.5f0, 0) do entry, sep, out out + 1, true end @test count == 1 @@ -283,7 +283,7 @@ end @testset "query from outside grid finds nearby segment" begin scl = LineSegCellList{Int64,Float32}((4,4,4), 2.0f0) cell_line_seg_add!(scl, SA[3.5f0, 3.0f0, 3.5f0], SA[3.5f0, 3.8f0, 3.5f0], Int64(1)) - count = map_nearby_line_segs(scl, SA[4.5f0, 3.5f0, 3.5f0], 2.0f0, 0) do entry, out + count = map_nearby_line_segs(scl, SA[4.5f0, 3.5f0, 3.5f0], 2.0f0, 0) do entry, sep, out out + 1, true end @test count == 1 @@ -293,13 +293,13 @@ end scl = LineSegCellList{Int64,Float32}((4,4,4), 2.0f0) # Grid covers [-4,4). Segment and query are both beyond the +x face. cell_line_seg_add!(scl, SA[10.0f0, 0.5f0, 0.5f0], SA[12.0f0, 0.5f0, 0.5f0], Int64(1)) - count = map_nearby_line_segs(scl, SA[11.0f0, 0.5f0, 1.5f0], 2.0f0, 0) do entry, out + count = map_nearby_line_segs(scl, SA[11.0f0, 0.5f0, 1.5f0], 2.0f0, 0) do entry, sep, out out + 1, true end @test count == 1 # The same query must not return an outside segment beyond the cutoff. cell_line_seg_add!(scl, SA[10.0f0, 0.5f0, 30.0f0], SA[12.0f0, 0.5f0, 30.0f0], Int64(2)) - count = map_nearby_line_segs(scl, SA[11.0f0, 0.5f0, 1.5f0], 2.0f0, 0) do entry, out + count = map_nearby_line_segs(scl, SA[11.0f0, 0.5f0, 1.5f0], 2.0f0, 0) do entry, sep, out out + 1, true end @test count == 1 @@ -309,13 +309,25 @@ end scl = LineSegCellList{Int64,Float32}((4,4,4), 2.0f0) cell_line_seg_add!(scl, SA[0.3f0, 0.5f0, 0.5f0], SA[1.2f0, 0.5f0, 0.5f0], Int64(1)) for cutoff in (0.0f0, -1.0f0) - result = map_nearby_line_segs(scl, SA[0.5f0, 0.5f0, 0.5f0], cutoff, 42) do entry, out + result = map_nearby_line_segs(scl, SA[0.5f0, 0.5f0, 0.5f0], cutoff, 42) do entry, sep, out out + 1, true end @test result == 42 end end + @testset "sep argument" begin + scl = LineSegCellList{Int64,Float32}((4,4,4), 2.0f0) + cell_line_seg_add!(scl, SA[-1.0f0, 0.0f0, 0.0f0], SA[1.0f0, 0.0f0, 0.0f0], Int64(1)) + # the closest point on the line segment to q is (0.5, 0, 0) + q = SA[0.5f0, 0.75f0, 0.0f0] + seps = map_nearby_line_segs(scl, q, 1.0f0, SVector{3,Float32}[]) do entry, sep, out + push!(out, sep) + out, true + end + @test seps ≈ [SA[0.0f0, -0.75f0, 0.0f0]] + end + @testset "brute force comparison" begin for trial in 1:5000 scl = LineSegCellList{Int64,Float32}((10,10,10), 2.0f0) @@ -341,7 +353,7 @@ end cutoff = Float32(rand()) * 8f0 cutoff2 = cutoff * cutoff q = rand(SVector{3,Float32}) .* 24f0 .- 12f0 - cell_ids = map_nearby_line_segs(scl, q, cutoff, Int64[]) do entry, out + cell_ids = map_nearby_line_segs(scl, q, cutoff, Int64[]) do entry, sep, out push!(out, entry.id) out, true end diff --git a/test/test-pointcelllist.jl b/test/test-pointcelllist.jl index 475f9f7..0d34ed4 100644 --- a/test/test-pointcelllist.jl +++ b/test/test-pointcelllist.jl @@ -31,10 +31,10 @@ function map_nearby_points_naive( cutoff ≤ 0 && return out cutoff2 = cutoff * cutoff for i in eachindex(positions, ids) - diff = pos - positions[i] - dist2 = diff ⋅ diff + sep = positions[i] - pos + dist2 = sep ⋅ sep if dist2 ≤ cutoff2 - out, cont = f(CellPointEntry{Int64,Float32}(positions[i], ids[i]), out) + out, cont = f(CellPointEntry{Int64,Float32}(positions[i], ids[i]), sep, out) cont || return out end end @@ -54,7 +54,7 @@ total_entries(pcl::PointCellList) = sum(pcl.cells[i].len for i in 1:prod(pcl.siz @test_throws ArgumentError PointCellList{Int64,Float32}((-1,4,4), 2.0f0) # A zero-size axis makes an empty grid: queries find nothing. pcl = PointCellList{Int64,Float32}((0,4,4), 2.0f0) - count = map_nearby_points(pcl, SA[0.5f0, 0.5f0, 0.5f0], 1.0f0, 0) do entry, out + count = map_nearby_points(pcl, SA[0.5f0, 0.5f0, 0.5f0], 1.0f0, 0) do entry, sep, out out + 1, true end @test count == 0 @@ -154,7 +154,7 @@ total_entries(pcl::PointCellList) = sum(pcl.cells[i].len for i in 1:prod(pcl.siz pos = SA[0.5f0, 0.5f0, 0.5f0] cell_point_add!(pcl, pos, Int64(10)) cell_point_add!(pcl, pos, Int64(20)) - count = map_nearby_points(pcl, pos, 0.1f0, 0) do entry, out + count = map_nearby_points(pcl, pos, 0.1f0, 0) do entry, sep, out out + 1, true end @test count == 2 @@ -187,7 +187,7 @@ total_entries(pcl::PointCellList) = sum(pcl.cells[i].len for i in 1:prod(pcl.siz @testset "map_nearby_points" begin @testset "empty" begin pcl = PointCellList{Int64,Float32}((4,4,4), 2.0f0) - count = map_nearby_points(pcl, SA[0.5f0, 0.5f0, 0.5f0], 0.5f0, 0) do entry, out + count = map_nearby_points(pcl, SA[0.5f0, 0.5f0, 0.5f0], 0.5f0, 0) do entry, sep, out out + 1, true end @test count == 0 @@ -196,7 +196,7 @@ total_entries(pcl::PointCellList) = sum(pcl.cells[i].len for i in 1:prod(pcl.siz @testset "single point found" begin pcl = PointCellList{Int64,Float32}((4,4,4), 2.0f0) cell_point_add!(pcl, SA[0.5f0, 0.5f0, 0.5f0], Int64(1)) - count = map_nearby_points(pcl, SA[0.5f0, 0.5f0, 0.5f0], 0.5f0, 0) do entry, out + count = map_nearby_points(pcl, SA[0.5f0, 0.5f0, 0.5f0], 0.5f0, 0) do entry, sep, out out + 1, true end @test count == 1 @@ -205,7 +205,7 @@ total_entries(pcl::PointCellList) = sum(pcl.cells[i].len for i in 1:prod(pcl.siz @testset "no matches - outside cutoff" begin pcl = PointCellList{Int64,Float32}((4,4,4), 2.0f0) cell_point_add!(pcl, SA[0.5f0, 0.5f0, 0.5f0], Int64(1)) - count = map_nearby_points(pcl, SA[-3.0f0, -3.0f0, -3.0f0], 0.5f0, 0) do entry, out + count = map_nearby_points(pcl, SA[-3.0f0, -3.0f0, -3.0f0], 0.5f0, 0) do entry, sep, out out + 1, true end @test count == 0 @@ -215,7 +215,7 @@ total_entries(pcl::PointCellList) = sum(pcl.cells[i].len for i in 1:prod(pcl.siz pcl = PointCellList{Int64,Float32}((4,4,4), 2.0f0) cell_point_add!(pcl, SA[0.5f0, 0.5f0, 0.5f0], Int64(1)) cell_point_add!(pcl, SA[0.6f0, 0.5f0, 0.5f0], Int64(2)) - count = map_nearby_points(pcl, SA[0.5f0, 0.5f0, 0.5f0], 1.0f0, 0) do entry, out + count = map_nearby_points(pcl, SA[0.5f0, 0.5f0, 0.5f0], 1.0f0, 0) do entry, sep, out out + 1, false end @test count == 1 @@ -225,7 +225,7 @@ total_entries(pcl::PointCellList) = sum(pcl.cells[i].len for i in 1:prod(pcl.siz pcl = PointCellList{Int64,Float32}((4,4,4), 2.0f0) # Point just inside cell (1,2,2), query just inside cell (2,2,2) cell_point_add!(pcl, SA[-0.1f0, 0.5f0, 0.5f0], Int64(1)) - count = map_nearby_points(pcl, SA[0.1f0, 0.5f0, 0.5f0], 0.5f0, 0) do entry, out + count = map_nearby_points(pcl, SA[0.1f0, 0.5f0, 0.5f0], 0.5f0, 0) do entry, sep, out out + 1, true end @test count == 1 @@ -236,7 +236,7 @@ total_entries(pcl::PointCellList) = sum(pcl.cells[i].len for i in 1:prod(pcl.siz # Grid covers [-4,4). Point near edge. cell_point_add!(pcl, SA[3.5f0, 3.5f0, 3.5f0], Int64(1)) # Query from outside grid, distance = 1.0 - count = map_nearby_points(pcl, SA[4.5f0, 3.5f0, 3.5f0], 2.0f0, 0) do entry, out + count = map_nearby_points(pcl, SA[4.5f0, 3.5f0, 3.5f0], 2.0f0, 0) do entry, sep, out out + 1, true end @test count == 1 @@ -246,13 +246,13 @@ total_entries(pcl::PointCellList) = sum(pcl.cells[i].len for i in 1:prod(pcl.siz pcl = PointCellList{Int64,Float32}((4,4,4), 2.0f0) # Grid covers [-4,4). Point and query are both beyond the +x face. cell_point_add!(pcl, SA[10.0f0, 0.5f0, 0.5f0], Int64(1)) - count = map_nearby_points(pcl, SA[11.0f0, 0.5f0, 0.5f0], 2.0f0, 0) do entry, out + count = map_nearby_points(pcl, SA[11.0f0, 0.5f0, 0.5f0], 2.0f0, 0) do entry, sep, out out + 1, true end @test count == 1 # The same query must not return an outside point beyond the cutoff. cell_point_add!(pcl, SA[11.0f0, 0.5f0, 30.0f0], Int64(2)) - count = map_nearby_points(pcl, SA[11.0f0, 0.5f0, 0.5f0], 2.0f0, 0) do entry, out + count = map_nearby_points(pcl, SA[11.0f0, 0.5f0, 0.5f0], 2.0f0, 0) do entry, sep, out out + 1, true end @test count == 1 @@ -263,7 +263,7 @@ total_entries(pcl::PointCellList) = sum(pcl.cells[i].len for i in 1:prod(pcl.siz # Points near the low-x boundary of the grid cell_point_add!(pcl, SA[-3.5f0, 0.5f0, 0.5f0], Int64(1)) # Query with large cutoff that extends past grid boundary on -x side - count = map_nearby_points(pcl, SA[-2.5f0, 0.5f0, 0.5f0], 3.0f0, 0) do entry, out + count = map_nearby_points(pcl, SA[-2.5f0, 0.5f0, 0.5f0], 3.0f0, 0) do entry, sep, out out + 1, true end @test count == 1 @@ -272,12 +272,24 @@ total_entries(pcl::PointCellList) = sum(pcl.cells[i].len for i in 1:prod(pcl.siz @testset "zero cutoff returns out" begin pcl = PointCellList{Int64,Float32}((4,4,4), 2.0f0) cell_point_add!(pcl, SA[0.5f0, 0.5f0, 0.5f0], Int64(1)) - result = map_nearby_points(pcl, SA[0.5f0, 0.5f0, 0.5f0], 0.0f0, 42) do entry, out + result = map_nearby_points(pcl, SA[0.5f0, 0.5f0, 0.5f0], 0.0f0, 42) do entry, sep, out out + 1, true end @test result == 42 end + @testset "sep argument" begin + pcl = PointCellList{Int64,Float32}((4,4,4), 2.0f0) + p = SA[0.5f0, 0.25f0, -0.75f0] + cell_point_add!(pcl, p, Int64(1)) + q = SA[0.25f0, 0.5f0, -0.5f0] + seps = map_nearby_points(pcl, q, 1.0f0, SVector{3,Float32}[]) do entry, sep, out + push!(out, sep) + out, true + end + @test seps == [p - q] + end + @testset "brute force comparison" begin for trial in 1:10000 pcl = PointCellList{Int64,Float32}((10,10,10), 2.0f0) @@ -289,7 +301,7 @@ total_entries(pcl::PointCellList) = sum(pcl.cells[i].len for i in 1:prod(pcl.siz cell_point_add!(pcl, positions[i], Int64(i)) end - function collect_id(entry, out) + function collect_id(entry, sep, out) push!(out, entry.id) out, true end From 1d9f8d67514e30870429f467b50b5662f4e8b825 Mon Sep 17 00:00:00 2001 From: nhz2 Date: Sun, 9 Aug 2026 18:40:45 +0200 Subject: [PATCH 06/14] add precompile statements --- src/SimplexCellLists.jl | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/SimplexCellLists.jl b/src/SimplexCellLists.jl index 19c0fc5..b60616b 100644 --- a/src/SimplexCellLists.jl +++ b/src/SimplexCellLists.jl @@ -35,4 +35,17 @@ export cell_line_seg_tmax export map_nearby_line_segs export cell_line_segs_clear! +# The mapping functions are not precompiled because they must be specialized +# on the caller's callback function anyway. +let T = Int64, F = Float32 + precompile(PointCellList{T,F}, (SVector{3,Int}, F)) + precompile(PointCellList{T,F}, (NTuple{3,Int}, F)) + precompile(cell_points_clear!, (PointCellList{T,F},)) + precompile(cell_point_add!, (PointCellList{T,F}, SVector{3,F}, T)) + precompile(LineSegCellList{T,F}, (SVector{3,Int}, F)) + precompile(LineSegCellList{T,F}, (NTuple{3,Int}, F)) + precompile(cell_line_segs_clear!, (LineSegCellList{T,F},)) + precompile(cell_line_seg_add!, (LineSegCellList{T,F}, SVector{3,F}, SVector{3,F}, T)) +end + end \ No newline at end of file From 585fb75c7a89a5f5caf8c70657210b899a24d624 Mon Sep 17 00:00:00 2001 From: nhz2 Date: Sun, 9 Aug 2026 18:53:40 +0200 Subject: [PATCH 07/14] remove ball_intersects_unit_cube and ball_intersects_biunit_cube --- Project.toml | 1 + src/SimplexCellLists.jl | 2 - src/util.jl | 26 ------- test/Project.toml | 1 + test/common.jl | 103 --------------------------- test/runtests.jl | 4 +- test/test-util.jl | 151 ---------------------------------------- 7 files changed, 5 insertions(+), 283 deletions(-) delete mode 100644 test/common.jl delete mode 100644 test/test-util.jl diff --git a/Project.toml b/Project.toml index 8ef47dd..d3190bc 100644 --- a/Project.toml +++ b/Project.toml @@ -10,6 +10,7 @@ StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" [compat] ArgCheck = "2" +LinearAlgebra = "1" StaticArrays = "1" julia = "1.12" diff --git a/src/SimplexCellLists.jl b/src/SimplexCellLists.jl index b60616b..2845075 100644 --- a/src/SimplexCellLists.jl +++ b/src/SimplexCellLists.jl @@ -12,8 +12,6 @@ const Triangle = Simplex{3} public Vec3, Simplex, Point, LineSeg, Triangle include("util.jl") -export ball_intersects_unit_cube -export ball_intersects_biunit_cube include("distances.jl") export dist_sqr diff --git a/src/util.jl b/src/util.jl index ab60fba..9dc4410 100644 --- a/src/util.jl +++ b/src/util.jl @@ -14,29 +14,3 @@ norm_fast(v) = _sqrt_fast(v ⋅ v) # and a scale of 2 units per grid spacing. # `pos_norm` is expected to be inside the grid. round2grid(pos_norm::Vec3, sz::Vec3{Int})::Vec3{Int} = (floor.((Int,), pos_norm) .+ sz) .>> 1 - -""" - ball_intersects_unit_cube(center, radius) -> Bool - -Check if a ball (sphere) intersects with the unit cube centered at the origin, -extending from -0.5 to 0.5 in each axis. -""" -function ball_intersects_unit_cube(center, radius) - # Squared distance from sphere center to nearest point on [-0.5, 0.5]^3 - h = oftype(radius, 1//2) - d2 = relu(abs(center[1]) - h)^2 + relu(abs(center[2]) - h)^2 + relu(abs(center[3]) - h)^2 - d2 <= radius * radius -end - -""" - ball_intersects_biunit_cube(center, radius) -> Bool - -Check if a ball (sphere) intersects with the biunit cube centered at the origin, -extending from -1 to 1 in each axis. -""" -function ball_intersects_biunit_cube(center, radius) - # Squared distance from sphere center to nearest point on [-1, 1]^3 - h = oneunit(radius) - d2 = relu(abs(center[1]) - h)^2 + relu(abs(center[2]) - h)^2 + relu(abs(center[3]) - h)^2 - d2 <= radius * radius -end diff --git a/test/Project.toml b/test/Project.toml index 0482baf..33140f6 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,4 +1,5 @@ [deps] +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" ArgCheck = "dce04be8-c92d-5529-be00-80e4d2c0e197" Clarabel = "61c947e1-3e6d-4ee4-985a-eec8c727bd6e" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" diff --git a/test/common.jl b/test/common.jl deleted file mode 100644 index 0c32be7..0000000 --- a/test/common.jl +++ /dev/null @@ -1,103 +0,0 @@ -# Common functions used in tests - -using StaticArrays -using LinearAlgebra -using SimplexCellLists -using Random -using Test -using ArgCheck -using Rotations - -trials = 1 - -Random.seed!(1234) - -function test_f_mapSimplexElements!(x,y,i,j,d2,output, real_ys, real_cutoff) - real_d2 = SimplexCellLists.distSqr(x, real_ys[j]) - @argcheck d2 ≥ 0 - @argcheck real_d2 ≥ 0 - @argcheck isapprox(real_d2, d2; atol=1E-5, rtol=1E-3) - @argcheck d2 - real_cutoff^2 ≤ 1E-5 - if √(d2) < real_cutoff-1E-5 - output[j] += 1 - end - output -end - -function test_f_mapPairElements!(x,y,i,j,d2,output, real_xs, real_cutoff) - real_d2 = SimplexCellLists.distSqr(real_xs[i], real_xs[j]) - @argcheck d2 ≥ 0 - @argcheck real_d2 ≥ 0 - @argcheck isapprox(real_d2, d2; atol = 1E-4, rtol=1E-3) - @argcheck d2 - real_cutoff^2 ≤ 1E-5 - if √(real_d2) < real_cutoff-1E-5 - output[i,j] += 1 - output[j,i] += 1 - end - output -end - -function test_f_mapElementsElements!(x,y,i,j,d2,output, real_xs, real_ys, real_cutoff) - real_d2 = SimplexCellLists.distSqr(real_xs[i], real_ys[j]) - @argcheck d2 ≥ 0 - @argcheck real_d2 ≥ 0 - @argcheck isapprox(real_d2, d2; atol = 1E-4, rtol=1E-3) - @argcheck d2 - real_cutoff^2 ≤ 1E-5 - if √(real_d2) < real_cutoff-1E-5 - output[i,j] += 1 - end - output -end - - -function makeBasicCellList!( - scale, - rotation, - translation, - emptycelllist::SimplexCellLists.SimplexCellList, - ) - function transform(in) - pts = reinterpret(SVector{3,Float32},in) - outpts = map(pts) do pt - rotation * (pt * scale) + translation - end - copy(reinterpret(eltype(in),outpts)) - end - points1 = transform([ - SA[SA_F32[-6,0,0]], - SA[SA_F32[6,0,0]], - ]) - reinterpret - points2 = reverse(points1) - lines1 = transform([ - SA[SA_F32[-6,0,0],SA_F32[-6,2,0]], - SA[SA_F32[6,0,0],SA_F32[6,2,0]], - ]) - lines2 = reverse(lines1) - triangles1 = transform([ - SA[SA_F32[-6,0,0],SA_F32[-6,2,0],SA_F32[-9,1,0]], - SA[SA_F32[6,0,0],SA_F32[6,2,0],SA_F32[9,1,0]], - ]) - triangles2 = reverse(triangles1) - # now try and set some elements - # some get added in - np1 = rand(0:2) - np2 = rand(0:2) - nl1 = rand(0:2) - nl2 = rand(0:2) - nt1 = rand(0:2) - nt2 = rand(0:2) - setElements!(emptycelllist, - [points1[1:np1],points2[1:np2]], - [lines1[1:nl1],lines2[1:nl2]], - [triangles1[1:nt1],triangles2[1:nt2]], - ) - addElement!.(Ref(emptycelllist),1,points1[np1+1:end]) - addElement!.(Ref(emptycelllist),2,points2[np2+1:end]) - addElement!.(Ref(emptycelllist),1,lines1[nl1+1:end]) - addElement!.(Ref(emptycelllist),2,lines2[nl2+1:end]) - addElement!.(Ref(emptycelllist),1,triangles1[nt1+1:end]) - addElement!.(Ref(emptycelllist),2,triangles2[nt2+1:end]) - points1, points2, lines1, lines2, triangles1, triangles2 -end - diff --git a/test/runtests.jl b/test/runtests.jl index bd4ea22..902c4a8 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,4 +1,6 @@ -include("test-util.jl") +using Aqua: Aqua +using SimplexCellLists: SimplexCellLists +Aqua.test_all(SimplexCellLists) include("test-mindistance.jl") diff --git a/test/test-util.jl b/test/test-util.jl deleted file mode 100644 index 282c430..0000000 --- a/test/test-util.jl +++ /dev/null @@ -1,151 +0,0 @@ -using StaticArrays -using LinearAlgebra -using SimplexCellLists -using Test - -@testset "ball_intersects_unit_cube" begin - buc = SimplexCellLists.ball_intersects_unit_cube - @testset "center at origin" begin - # Any positive radius intersects - @test buc(SA[0.0, 0.0, 0.0], 0.0) - @test buc(SA[0.0, 0.0, 0.0], 0.1) - @test buc(SA[0.0, 0.0, 0.0], 10.0) - end - @testset "center inside cube" begin - @test buc(SA[0.4, 0.4, 0.4], 0.0) - @test buc(SA[-0.3, 0.2, -0.1], 0.0) - @test buc(SA[0.5, 0.5, 0.5], 0.0) # on corner, still touching - @test buc(SA[0.5, 0.0, 0.0], 0.0) # on face, still touching - end - @testset "face separation" begin - # Ball center along +x axis, only x exceeds cube - @test buc(SA[0.7, 0.0, 0.0], 0.21) # distance = 0.2, radius > distance - @test !buc(SA[0.7, 0.0, 0.0], 0.19) # distance = 0.2, radius < distance - # Along -y axis - @test buc(SA[0.0, -1.0, 0.0], 0.51) - @test !buc(SA[0.0, -1.0, 0.0], 0.49) - # Along +z axis - @test buc(SA[0.0, 0.0, 0.8], 0.31) - @test !buc(SA[0.0, 0.0, 0.8], 0.29) - end - @testset "edge separation" begin - # Center near an edge: two coordinates exceed cube - # e.g. (+x, +y) edge, center at (1, 1, 0) - # nearest point on cube is (0.5, 0.5, 0), dist = sqrt(0.25 + 0.25) = sqrt(0.5) ≈ 0.7071 - @test buc(SA[1.0, 1.0, 0.0], 0.71) - @test !buc(SA[1.0, 1.0, 0.0], 0.70) - # (-x, +z) edge - @test buc(SA[-1.0, 0.0, 1.0], 0.71) - @test !buc(SA[-1.0, 0.0, 1.0], 0.70) - end - @testset "vertex separation" begin - # Center near vertex: all three coordinates exceed cube - # center at (1, 1, 1), nearest vertex is (0.5, 0.5, 0.5), dist = sqrt(0.75) ≈ 0.8660 - @test buc(SA[1.0, 1.0, 1.0], 0.87) - @test !buc(SA[1.0, 1.0, 1.0], 0.86) - # Negative vertex - @test buc(SA[-1.0, -1.0, -1.0], 0.87) - @test !buc(SA[-1.0, -1.0, -1.0], 0.86) - end - @testset "exact boundary" begin - # Exactly touching: d == r, should intersect (<=) - @test buc(SA[1.0, 0.0, 0.0], 0.5) # face touch - @test buc(SA[1.0, 1.0, 0.0], sqrt(0.5)) # edge touch - # vertex touch: sqrt(0.75)^2 != 0.75 due to fp rounding, - # so test just inside and just outside instead - @test buc(SA[1.0, 1.0, 1.0], sqrt(0.75) + eps()) - @test !buc(SA[1.0, 1.0, 1.0], sqrt(0.75) - 1e-10) - end - @testset "large radius" begin - @test buc(SA[100.0, 100.0, 100.0], 200.0) - @test !buc(SA[100.0, 100.0, 100.0], 170.0) - end - @testset "symmetry" begin - # All 8 octant corners should give same result - for sx in (-1, 1), sy in (-1, 1), sz in (-1, 1) - c = SA[sx * 0.8, sy * 0.9, sz * 0.7] - @test buc(c, 0.5) == buc(abs.(c), 0.5) - end - end - @testset "Float32" begin - @test buc(SA[0.7f0, 0.0f0, 0.0f0], 0.21f0) - @test !buc(SA[0.7f0, 0.0f0, 0.0f0], 0.19f0) - end -end - -@testset "ball_intersects_biunit_cube" begin - bbc = SimplexCellLists.ball_intersects_biunit_cube - @testset "center at origin" begin - # Any positive radius intersects - @test bbc(SA[0.0, 0.0, 0.0], 0.0) - @test bbc(SA[0.0, 0.0, 0.0], 0.1) - @test bbc(SA[0.0, 0.0, 0.0], 10.0) - end - @testset "center inside cube" begin - @test bbc(SA[0.8, 0.8, 0.8], 0.0) - @test bbc(SA[-0.6, 0.4, -0.2], 0.0) - @test bbc(SA[1.0, 1.0, 1.0], 0.0) # on corner, still touching - @test bbc(SA[1.0, 0.0, 0.0], 0.0) # on face, still touching - end - @testset "face separation" begin - # Ball center along +x axis, only x exceeds cube - @test bbc(SA[1.4, 0.0, 0.0], 0.41) # distance = 0.4, radius > distance - @test !bbc(SA[1.4, 0.0, 0.0], 0.39) # distance = 0.4, radius < distance - # Along -y axis - @test bbc(SA[0.0, -2.0, 0.0], 1.01) - @test !bbc(SA[0.0, -2.0, 0.0], 0.99) - # Along +z axis - @test bbc(SA[0.0, 0.0, 1.6], 0.61) - @test !bbc(SA[0.0, 0.0, 1.6], 0.59) - end - @testset "edge separation" begin - # Center near an edge: two coordinates exceed cube - # e.g. (+x, +y) edge, center at (2, 2, 0) - # nearest point on cube is (1, 1, 0), dist = sqrt(1 + 1) = sqrt(2) ≈ 1.4142 - @test bbc(SA[2.0, 2.0, 0.0], 1.42) - @test !bbc(SA[2.0, 2.0, 0.0], 1.41) - # (-x, +z) edge - @test bbc(SA[-2.0, 0.0, 2.0], 1.42) - @test !bbc(SA[-2.0, 0.0, 2.0], 1.41) - end - @testset "vertex separation" begin - # Center near vertex: all three coordinates exceed cube - # center at (2, 2, 2), nearest vertex is (1, 1, 1), dist = sqrt(3) ≈ 1.7321 - @test bbc(SA[2.0, 2.0, 2.0], 1.74) - @test !bbc(SA[2.0, 2.0, 2.0], 1.73) - # Negative vertex - @test bbc(SA[-2.0, -2.0, -2.0], 1.74) - @test !bbc(SA[-2.0, -2.0, -2.0], 1.73) - end - @testset "exact boundary" begin - # Exactly touching: d == r, should intersect (<=) - @test bbc(SA[2.0, 0.0, 0.0], 1.0) # face touch - @test bbc(SA[2.0, 2.0, 0.0], sqrt(2.0)) # edge touch - # vertex touch: sqrt(3)^2 != 3 due to fp rounding, - # so test just inside and just outside instead - @test bbc(SA[2.0, 2.0, 2.0], sqrt(3.0) + 2*eps()) - @test !bbc(SA[2.0, 2.0, 2.0], sqrt(3.0) - 1e-10) - end - @testset "large radius" begin - @test bbc(SA[100.0, 100.0, 100.0], 200.0) - @test !bbc(SA[100.0, 100.0, 100.0], 170.0) - end - @testset "symmetry" begin - # All 8 octant corners should give same result - for sx in (-1, 1), sy in (-1, 1), sz in (-1, 1) - c = SA[sx * 1.6, sy * 1.8, sz * 1.4] - @test bbc(c, 1.0) == bbc(abs.(c), 1.0) - end - end - @testset "Float32" begin - @test bbc(SA[1.4f0, 0.0f0, 0.0f0], 0.41f0) - @test !bbc(SA[1.4f0, 0.0f0, 0.0f0], 0.39f0) - end - @testset "agrees with unit cube version at half scale" begin - for trial in 1:1000 - c = SVector{3,Float64}(randn(3)) * 2.0 - r = abs(randn()) * 1.5 - @test bbc(c, r) == SimplexCellLists.ball_intersects_unit_cube(c/2, r/2) - end - end -end From b057a91cb4c46e14a6a4e3078da7167469d005cc Mon Sep 17 00:00:00 2001 From: nhz2 Date: Sun, 9 Aug 2026 18:56:59 +0200 Subject: [PATCH 08/14] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f5af5d9..a37f52a 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Build Status](https://github.com/medyan-dev/SimplexCellLists.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/medyan-dev/SimplexCellLists.jl/actions/workflows/CI.yml?query=branch%3Amain) ![Julia Support](https://img.shields.io/badge/Julia-1.8-blue) -This Julia package accelerates computations on pairs of 3D points, line segments, and triangles within a cutoff distance. +This Julia package contains data structures and algorithms for doing computations on pairs of 3D points, line segments, and triangles within a cutoff distance. This package is largely inspired by [CellListMap.jl](https://github.com/m3g/CellListMap.jl). From 66900796c731dace1e59477cbf34943dbced3a5c Mon Sep 17 00:00:00 2001 From: nhz2 Date: Sun, 9 Aug 2026 19:12:56 +0200 Subject: [PATCH 09/14] fix CI --- .github/workflows/CI.yml | 37 ++++++++---------------------- .github/workflows/CompatHelper.yml | 16 ------------- README.md | 1 - 3 files changed, 9 insertions(+), 45 deletions(-) delete mode 100644 .github/workflows/CompatHelper.yml diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 2a75fcb..f1a40b5 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -18,39 +18,20 @@ jobs: fail-fast: false matrix: version: - - '1.8' - - '1.9' - - 'nightly' + - 'pre' os: - ubuntu-latest - - windows-latest - - macos-latest arch: - - x64 + - 'default' steps: - - uses: actions/checkout@v2 - - uses: julia-actions/setup-julia@v1 + - uses: actions/checkout@v7 + - uses: julia-actions/setup-julia@v3 with: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - - uses: julia-actions/cache@v1 - - name: Develop packages - run: | - julia --project=test -e ' - using Pkg - Pkg.develop([PackageSpec(path=pwd())]) - Pkg.instantiate()' - - uses: julia-actions/julia-buildpkg@v1 + show-versioninfo: true + - uses: julia-actions/cache@v2 + - uses: julia-actions/julia-runtest@v1 with: - project: test - - name: Print package status - run: | - julia --project=test -e ' - using InteractiveUtils - versioninfo() - using Pkg - Pkg.status(;mode=Pkg.PKGMODE_MANIFEST)' - - name: Run tests - run: | - cd test && - julia --project --color=yes --depwarn=yes --warn-overwrite=yes --warn-scope=yes --check-bounds=yes --compiled-modules=no runtests.jl + coverage: false + checkbounds: true diff --git a/.github/workflows/CompatHelper.yml b/.github/workflows/CompatHelper.yml deleted file mode 100644 index cba9134..0000000 --- a/.github/workflows/CompatHelper.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: CompatHelper -on: - schedule: - - cron: 0 0 * * * - workflow_dispatch: -jobs: - CompatHelper: - runs-on: ubuntu-latest - steps: - - name: Pkg.add("CompatHelper") - run: julia -e 'using Pkg; Pkg.add("CompatHelper")' - - name: CompatHelper.main() - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - COMPATHELPER_PRIV: ${{ secrets.DOCUMENTER_KEY }} - run: julia -e 'using CompatHelper; CompatHelper.main()' diff --git a/README.md b/README.md index a37f52a..b4ab7ec 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # SimplexCellLists WIP [![Build Status](https://github.com/medyan-dev/SimplexCellLists.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/medyan-dev/SimplexCellLists.jl/actions/workflows/CI.yml?query=branch%3Amain) -![Julia Support](https://img.shields.io/badge/Julia-1.8-blue) This Julia package contains data structures and algorithms for doing computations on pairs of 3D points, line segments, and triangles within a cutoff distance. From ca6d3f24a505272acd380c42838a8f31c9f549a2 Mon Sep 17 00:00:00 2001 From: nhz2 Date: Sun, 9 Aug 2026 19:20:37 +0200 Subject: [PATCH 10/14] update readme --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index b4ab7ec..0ec8e24 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,28 @@ This Julia package contains data structures and algorithms for doing computations on pairs of 3D points, line segments, and triangles within a cutoff distance. +It provides cell lists for points ([`PointCellList`](src/pointcelllist.jl)) and line segments ([`LineSegCellList`](src/linesegcelllist.jl)) with fast nearby-neighbor mapping, and squared-distance functions (`dist_sqr`) for all pairs of points, line segments, and triangles. + This package is largely inspired by [CellListMap.jl](https://github.com/m3g/CellListMap.jl). However, there is no support for periodic boundary conditions or 2D systems. See [CellListMap.jl](https://github.com/m3g/CellListMap.jl) if you want these features. + +## Quickstart + +```julia +using SimplexCellLists, StaticArrays + +# 10×10×10 grid of cells, each 1.0 wide, centered on the origin +pcl = PointCellList{Int64,Float32}((10, 10, 10), 1.0f0) +cell_point_add!(pcl, SA[0.1f0, 0.2f0, 0.3f0], 1) +cell_point_add!(pcl, SA[0.3f0, 0.2f0, 0.3f0], 2) + +# Count the points within 0.5 of the origin +n = map_nearby_points(pcl, SA[0.0f0, 0.0f0, 0.0f0], 0.5f0, 0) do entry, sep, count + (count + 1, true) +end +``` + +`LineSegCellList`, `cell_line_seg_add!`, and `map_nearby_line_segs` are the line segment analogs, and `cell_points_clear!`/`cell_line_segs_clear!` reset a list for reuse. From 2803ded805dcd0f2f6bd61561979ef59c88ca35a Mon Sep 17 00:00:00 2001 From: nhz2 Date: Sun, 9 Aug 2026 19:21:14 +0200 Subject: [PATCH 11/14] update version --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index d3190bc..deb3eed 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "SimplexCellLists" uuid = "fbe69cd6-f244-4ce4-a90b-8979d86c9ea4" authors = ["Nathan Zimmerberg"] -version = "0.2.0-dev" +version = "0.2.0" [deps] ArgCheck = "dce04be8-c92d-5529-be00-80e4d2c0e197" From 084de5039d7c3bf885acff513180ba27507c57c6 Mon Sep 17 00:00:00 2001 From: nhz2 Date: Sun, 9 Aug 2026 19:36:33 +0200 Subject: [PATCH 12/14] fix CI --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index f1a40b5..ce12e4b 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -34,4 +34,4 @@ jobs: - uses: julia-actions/julia-runtest@v1 with: coverage: false - checkbounds: true + check_bounds: true From 03d464237b05dcedea18c9e1cda74bbe14d9630d Mon Sep 17 00:00:00 2001 From: nhz2 Date: Sun, 9 Aug 2026 19:37:14 +0200 Subject: [PATCH 13/14] test min --- .github/workflows/CI.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index ce12e4b..eb1cd24 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -19,6 +19,7 @@ jobs: matrix: version: - 'pre' + - 'min' os: - ubuntu-latest arch: From a7f5a3298cb43e02474a0ff46d94bbb3cfbbf3d6 Mon Sep 17 00:00:00 2001 From: nhz2 Date: Sun, 9 Aug 2026 19:39:10 +0200 Subject: [PATCH 14/14] fix CI --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index eb1cd24..94dac63 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -35,4 +35,4 @@ jobs: - uses: julia-actions/julia-runtest@v1 with: coverage: false - check_bounds: true + check_bounds: "yes"