Skip to content

Commit

Permalink
Generalize faster find_intersections Algorithm (#220)
Browse files Browse the repository at this point in the history
* Set project version to 1.10.0

---------

Co-authored-by: David F Little <david.frank.little@gmail.com>
  • Loading branch information
omus and haberdashPI authored Jun 14, 2023
1 parent 4b9f831 commit 966f3e9
Show file tree
Hide file tree
Showing 2 changed files with 26 additions and 66 deletions.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name = "Intervals"
uuid = "d8418881-c3e1-53bb-8760-2df7ec849ed5"
license = "MIT"
authors = ["Invenia Technical Computing"]
version = "1.9.0"
version = "1.10.0"

[deps]
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
Expand Down
90 changes: 25 additions & 65 deletions src/interval_sets.jl
Original file line number Diff line number Diff line change
Expand Up @@ -481,95 +481,55 @@ find_intersections(x, y) = find_intersections(vcat(x), vcat(y))
function find_intersections(x::AbstractVector{<:AbstractInterval}, y::AbstractVector{<:AbstractInterval})
tracking = endpoint_tracking(x, y)
lt = intersection_isless_fn(tracking)
x_endpoints = unbunch(enumerate(x), tracking; lt)
y_endpoints = unbunch(enumerate(y), tracking; lt)
result = [Vector{Int}() for _ in 1:length(x)]

return find_intersections_helper!(result, x_endpoints, y_endpoints, lt)
end

function find_intersections_helper!(result, x, y, lt)
active_xs = Set{Int}()
active_ys = Set{Int}()
while !isempty(x)
xᵢ, yᵢ = first_endpoint(x), first_endpoint(y)
x_less = lt(xᵢ, yᵢ)
y_less = lt(yᵢ, xᵢ)

if !y_less
if isleft(xᵢ)
push!(active_xs, first(first(x)))
else
delete!(active_xs, first(first(x)))
end
x = @view x[2:end]
end

if !x_less
if isleft(yᵢ)
push!(active_ys, first(first(y)))
else
delete!(active_ys, first(first(y)))
end
y = @view y[2:end]
end

for i in active_xs
append!(result[i], active_ys)
end
end
results = Vector{Vector{Int}}(undef, length(x))

return unique!.(result)
return find_intersections_helper!(results, x, y, lt)
end

function find_intersections(
x::AbstractVector{<:AbstractInterval{T1,Closed,Closed}},
y::AbstractVector{<:AbstractInterval{T2,Closed,Closed}},
) where {T1,T2}
function find_intersections_helper!(results, x, y, lt)
# Strategy:
# two binary searches per interval `I` in `x`
# * identify the set of intervals in `y` that start during-or-after `I`
# * identify the set of intervals in `y` that stop before-or-during `I`
# * intersect them
starts = first.(y)
starts_perm = sortperm(starts)
starts_sorted = starts[starts_perm]
lefts = LeftEndpoint.(y)
lefts_order = sortperm(lefts; lt)
lefts_sorted = lefts[lefts_order]

# Sneaky performance optimization (makes a huge difference!)
# Rather than sorting `stops` relative to `y`, we sort it relative to `starts`.
# This allows us to work in the `starts` frame of reference until the very end.
# In particular, when we intersect the sets of intervals obtained from starts and from stops,
# the `starts` set can be kept as a `UnitRange`, making the intersection *much* faster.
stops = last.(y[starts_perm])
stops_perm = sortperm(stops)
stops_sorted = stops[stops_perm]
len = length(stops_sorted)

results = Vector{Vector{Int}}(undef, length(x))
# Rather than sorting `rights` relative to `y`, we sort it relative to `lefts`.
# This allows us to work in the `lefts` frame of reference until the very end.
# In particular, when we intersect the sets of intervals obtained from lefts and from rights,
# the `lefts` set can be kept as a `UnitRange`, making the intersection *much* faster.
rights = RightEndpoint.(y)[lefts_order]
rights_order = sortperm(rights; lt)
rights_sorted = rights[rights_order]

y_len = length(y)
for (i, I) in enumerate(x)
# find all the starts which occur before or at the end of `I`
idx_first = searchsortedlast(starts_sorted, last(I); lt=(<))
# find all the starts which occur before or on the right endpoint of `I`
idx_first = searchsortedlast(lefts_sorted, RightEndpoint(I); lt)
if idx_first < 1
results[i] = Int[]
continue
end

# find all the stops which occur at or after the start of `I`
idx_last = searchsortedfirst(stops_sorted, first(I); lt=(<))
if idx_last > len
# find all the stops which occur on or after the left endpoint of `I`
idx_last = searchsortedfirst(rights_sorted, LeftEndpoint(I); lt)
if idx_last > y_len
results[i] = Int[]
continue
end

# Working in "starts" frame of reference
starts_before_or_during = 1:idx_first
stops_during_or_after = @view stops_perm[idx_last:end]
# Working in "lefts" frame of reference
lefts_before_or_during = 1:idx_first
rights_during_or_after = @view rights_order[idx_last:end]

# Intersect them
r = intersect(starts_before_or_during, stops_during_or_after)
r = intersect(lefts_before_or_during, rights_during_or_after)

# *Now* go back to y's sorting order, post-intersection.
results[i] = starts_perm[r]
results[i] = lefts_order[r]
end
return results
end

2 comments on commit 966f3e9

@omus
Copy link
Collaborator Author

@omus omus commented on 966f3e9 Jun 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/85622

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v1.10.0 -m "<description of version>" 966f3e9ba600bb4f80711227bad392a9e4f0137d
git push origin v1.10.0

Please sign in to comment.