Skip to content

Helper function to find a perpendicular vector to a given vector This is a generic implementation that works with any vector type

julia
function find_orthogonal(v::AbstractVector)

Choose the smallest component to zero out

julia
    x, y, z = v[1], v[2], v[3]
    ax, ay, az = abs(x), abs(y), abs(z)

    if ax <= ay && ax <= az

x is smallest, zero it out

julia
        return UnitSphericalPoint(0.0, -z, y)
    elseif ay <= ax && ay <= az

y is smallest, zero it out

julia
        return UnitSphericalPoint(-z, 0.0, x)
    else

z is smallest, zero it out

julia
        return UnitSphericalPoint(-y, x, 0.0)
    end
end

"""
    isUnitLength(v::AbstractVector)

Check if a vector has unit length within a small tolerance.

Returns true if the vector's magnitude is approximately 1.0.

The tolerance adapts to the element type of the vector to handle both
Float64 (high precision) and Float32 (lower precision, e.g., from GeoJSON) inputs.
Uses `16 * eps(T)` which provides adequate margin for unit vectors constructed
from trigonometric functions while still being tight enough to catch errors.
"""
function isUnitLength(v::AbstractVector)
    T = eltype(v)

Use type-appropriate tolerance: 16*eps gives good margin for trig-constructed vectors For Float64: ~3.5e-15, for Float32: ~1.9e-6

julia
    tol = 16 * eps(T <: Integer ? Float64 : T)
    return isapprox(sum(abs2, v), 1.0, rtol=tol)
end

"""
    isNormalizable(v::AbstractVector)

Returns true if the given vector's magnitude is large enough such that
the angle to another vector of the same magnitude can be measured using
angle calculations without loss of precision due to floating-point underflow.

This matches S2's IsNormalizable function.
"""
function isNormalizable(v::AbstractVector)

Same threshold as in S2 - the largest component should be at least 2^-242 This ensures we can normalize without precision loss

julia
    return maximum(abs.(v)) >= ldexp(1.0, -242)
end

"""
    normalization_needed(v::AbstractVector)

Determines if a vector's magnitude is too small for reliable normalization.
Returns true if the vector needs special handling to avoid numerical issues.

This is essentially the opposite of isNormalizable.
"""
function normalization_needed(v::AbstractVector)

The threshold is 1e-14 squared, approximately 1e-28

julia
    norm_v² = sum(abs2, v)
    return norm_v² < 1e-28
end

"""
    ensureNormalizable(p::AbstractVector)

Scales a 3-vector as necessary to ensure that the result can be normalized
without loss of precision due to floating-point underflow.

This matches S2's EnsureNormalizable function.
"""
function ensureNormalizable(p::AbstractVector)
    if p == zeros(eltype(p), 3)
        error("Vector must be non-zero")
    end

    if !isNormalizable(p)

Scale so that the largest component has magnitude in [1, 2)

julia
        p_max = maximum(abs.(p))

Scale by 2^(-1-exponent) to achieve this range

julia
        return ldexp(2.0, -1 - exponent(Float64(p_max))) * p
    end

    return p
end

"""
    normalizableFromExact(xf::Vector{BigFloat})

Converts a BigFloat vector to a double-precision vector, scaling the
result as necessary to ensure that the result can be normalized without
loss of precision due to floating-point underflow.

This matches S2's NormalizableFromExact function.
"""
function normalizableFromExact(xf::AbstractVector{BigFloat})

Mirrors S2's NormalizableFromExact at s2edge_crossings.cc:318-336.

First try a simple conversion to Float64; if that already has a component >= 2^-242 it needs no rescaling. Otherwise we must rescale in BigFloat (or equivalently compute the shift from BigFloat exponents) — we cannot convert to Float64 first, because components like 5e-324 would flush to the subnormal range or zero, destroying the axis information before the scaling multiply can fix it. (Bug fixed at src/utils/UnitSpherical/robustcrossproduct/utils.jl:102.)

julia
    x = Float64.(xf)

    if isNormalizable(x)
        return x
    end

Find the largest BigFloat exponent among nonzero components.

julia
    found = false
    max_exp = 0
    for i in 1:3
        if !iszero(xf[i])
            e = exponent(xf[i])
            if !found || e > max_exp
                max_exp = e
                found = true
            end
        end
    end

    if !found
        return zero(x)  # The exact result is (0, 0, 0).
    end

Scale in BigFloat so the largest component is in [0.5, 1), then convert. This matches S2's ldexp(xf[i], -exp) inside ExactFloat.

julia
    return UnitSphericalPoint(
        Float64(ldexp(xf[1], -max_exp)),
        Float64(ldexp(xf[2], -max_exp)),
        Float64(ldexp(xf[3], -max_exp)),
    )
end


"""
    isless_vector(a::AbstractVector, b::AbstractVector)

Lexicographic comparison of vectors.
This is used to establish a consistent order for symbolic perturbations.

Returns true if a comes before b in lexicographic order.
"""
function isless_vector(a::AbstractVector, b::AbstractVector)

First compare x coordinates

julia
    a[1] != b[1] && return a[1] < b[1]

If x coordinates are equal, compare y coordinates

julia
    a[2] != b[2] && return a[2] < b[2]

If both x and y are equal, compare z coordinates

julia
    return a[3] < b[3]
end

Use the Base.< function for UnitSphericalPoint to delegate to our isless_vector function TODO: this is sailing the high seas!

julia
function Base.:<(a::UnitSphericalPoint, b::UnitSphericalPoint)
    return isless_vector(a, b)
end

This page was generated using Literate.jl.