Skip to content

Commit 87c2c7b

Browse files
authored
[Core] Add an algorithm interface + StableTasks (#269)
This was factored out of the "dev branch" #259 and contains the subset of changes that apply to GeometryOpsCore, for easier review. Child PRs: #271 (TGGeometry) -> #275 (AdaptivePredicates) -> #273 (clipping algorithm type) -> #274 (trees) - Use [StableTasks.jl](https://github.com/JuliaFolds2/StableTasks.jl) in apply and applyreduce - its type-stable tasks save us some allocations! - Remove `Base.@assume_effects` on the low level functions, which caused issues on Julia v1.11 and was probably incorrect anyway - Add an algorithm interface with an abstract supertype `Algorithm{M <: Manifold}`, as discussed in #247. Also adds an abstract Operator supertype and some discussion in code comments, but no implementation or interface surface there yet. - Split out `types.jl` into a directory `types` with a bunch of files in it, for ease of readability / docs / use. - (out of context change): refactor CI a bit for cleanliness. TODOs for later (not this PR): - [ ] Add a `format` method that takes in an incompletely specified algorithm and some geometry as input, and returns a completely specified algorithm. What does this mean? Imagine I call `GO.intersection(FosterHormannClipping(), geom1, geom2)`. That `FosterHormannClipping()` should get expanded to `FosterHormannClipping(AutoAlgorithm(), AutoAccelerator())`. Then, `format` will take `format(alg, args...)` and: - get the `crstrait` of the two geometries, scan for incompatibilities, assign the correct manifold to the algorithm (maybe warn or emit debug info) - if no geometries available, get the manifold via `best_manifold(::Algorithm)`. - maybe inflate the accelerator by checking `npoint` and later preparations to see what's most efficient, maybe not - depends on what we want!
1 parent 18eb5e9 commit 87c2c7b

21 files changed

+542
-201
lines changed

.github/workflows/CI.yml

+19-4
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
fail-fast: false
1919
matrix:
2020
version:
21-
- '1.9'
21+
- '1.10'
2222
- '1'
2323
- 'nightly'
2424
os:
@@ -33,8 +33,8 @@ jobs:
3333
version: ${{ matrix.version }}
3434
arch: ${{ matrix.arch }}
3535
- uses: julia-actions/cache@v1
36-
- name: Dev GeometryOpsCore`
37-
run: julia --project=. -e 'using Pkg; Pkg.develop(; path = joinpath(".", "GeometryOpsCore"))'
36+
- name: Dev GeometryOpsCore and add other packages
37+
run: julia --project=. -e 'using Pkg; Pkg.develop(; path = joinpath(".", "GeometryOpsCore"));'
3838
- uses: julia-actions/julia-buildpkg@v1
3939
- uses: julia-actions/julia-runtest@v1
4040
- uses: julia-actions/julia-processcoverage@v1
@@ -60,13 +60,28 @@ jobs:
6060
with:
6161
version: '1'
6262
- name: Build and add versions
63-
run: julia --project=docs -e 'using Pkg; Pkg.develop([PackageSpec(path = "."), PackageSpec(path = joinpath(".", "GeometryOpsCore"))]); Pkg.add([PackageSpec(name = "GeoMakie", rev = "master"), PackageSpec(name="GeoInterface", rev="bugfix_vars")])'
63+
run: julia --project=docs -e 'using Pkg; Pkg.add([PackageSpec(name = "GeoMakie", rev = "master")])'
6464
- uses: julia-actions/julia-docdeploy@v1
6565
with:
6666
install-package: false
6767
env:
6868
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
6969
DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }}
70+
doctests:
71+
name: Doctests
72+
runs-on: ubuntu-latest
73+
permissions:
74+
contents: write
75+
statuses: write
76+
actions: write
77+
steps:
78+
- uses: actions/checkout@v2
79+
- uses: julia-actions/cache@v1
80+
- uses: julia-actions/setup-julia@v1
81+
with:
82+
version: '1'
83+
- name: Build and add versions
84+
run: julia --project=docs -e 'using Pkg; Pkg.add([PackageSpec(name = "GeoMakie", rev = "master")])'
7085
- run: |
7186
julia --project=docs -e '
7287
using Documenter: DocMeta, doctest

.gitignore

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,6 @@
33
/docs/build/
44
/docs/src/source/
55
.vscode/
6-
.DS_Store
6+
.DS_Store
7+
8+
benchmarks/Manifest.toml

GeometryOpsCore/Project.toml

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
name = "GeometryOpsCore"
22
uuid = "05efe853-fabf-41c8-927e-7063c8b9f013"
33
authors = ["Anshul Singhvi <[email protected]>", "Rafael Schouten <[email protected]>", "Skylar Gering <[email protected]>", "and contributors"]
4-
version = "0.1.2"
4+
version = "0.1.3"
55

66
[deps]
77
DataAPI = "9a962f9c-6df0-11e9-0e5d-c546b8b5ee8a"
88
GeoInterface = "cf35fbd7-0cd7-5166-be24-54bfbe79505f"
9+
StableTasks = "91464d47-22a1-43fe-8b7f-2d57ee82463f"
910
Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"
1011

1112
[compat]
12-
julia = "1.9"
1313
DataAPI = "1"
1414
GeoInterface = "1.2"
15+
StableTasks = "0.1.5"
1516
Tables = "1"
17+
julia = "1.9"

GeometryOpsCore/README.md

+7-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22

33
This is a "core" package for [GeometryOps.jl](https://github.com/JuliaGeo/GeometryOps.jl), that defines some basic primitive functions and types for GeometryOps.
44

5-
Generally, you would depend on this to use either the GeometryOps types (like `Linear`, `Spherical`, etc) or the primitive functions like `apply`, `applyreduce`, `flatten`, etc.
5+
It defines, all in all:
6+
- Manifolds and the manifold interface
7+
- The Algorithm type and the algorithm interface
8+
- Low level functions like apply, applyreduce, flatten, etc.
9+
- Common methods that should work across all geometries!
10+
11+
Generally, you would depend on this to use either the GeometryOps types (like `Planar`, `Spherical`, etc) or the primitive functions like `apply`, `applyreduce`, `flatten`, etc.
612
All of these are also accessible from GeometryOps, so it's preferable that you use GeometryOps directly.
713

814
Tests are in the main GeometryOps tests, we don't have separate tests for GeometryOpsCore since it's in a monorepo structure.

GeometryOpsCore/src/GeometryOpsCore.jl

+10-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import GeoInterface
66
import GeoInterface as GI
77
import GeoInterface: Extents
88

9-
# Import all names from GeoInterface and Extents, so users can do `GO.extent` or `GO.trait`.
9+
# Import all exported names from GeoInterface and Extents, so users can do `GO.extent` or `GO.trait`.
1010
for name in names(GeoInterface)
1111
@eval using GeoInterface: $name
1212
end
@@ -16,9 +16,17 @@ end
1616

1717
using Tables
1818
using DataAPI
19+
import StableTasks
1920

2021
include("keyword_docs.jl")
21-
include("types.jl")
22+
include("constants.jl")
23+
24+
include("types/manifold.jl")
25+
include("types/algorithm.jl")
26+
include("types/operation.jl")
27+
include("types/exceptions.jl")
28+
include("types/booltypes.jl")
29+
include("types/traittarget.jl")
2230

2331
include("apply.jl")
2432
include("applyreduce.jl")

GeometryOpsCore/src/apply.jl

+31-20
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,11 @@ Functions like [`flip`](@ref), [`reproject`](@ref), [`transform`](@ref), even [`
3030
using the `apply` framework. Similarly, [`centroid`](@ref), [`area`](@ref) and [`distance`](@ref) have been implemented using the
3131
[`applyreduce`](@ref) framework.
3232
33-
## Docstrings
34-
35-
### Functions
36-
3733
```@docs; collapse=true, canonical=false
3834
apply
39-
applyreduce
4035
```
4136
42-
=#
4337
44-
#=
4538
## What is `apply`?
4639
4740
`apply` applies some function to every geometry matching the `Target`
@@ -69,7 +62,7 @@ Be careful making a union across "levels" of nesting, e.g.
6962
`Union{FeatureTrait,PolygonTrait}`, as `_apply` will just never reach
7063
`PolygonTrait` when all the polygons are wrapped in a `FeatureTrait` object.
7164
72-
## Embedding:
65+
### Embedding
7366
7467
`extent` and `crs` can be embedded in all geometries, features, and
7568
feature collections as part of `apply`. Geometries deeper than `Target`
@@ -78,14 +71,30 @@ will of course not have new `extent` or `crs` embedded.
7871
- `calc_extent` signals to recalculate an `Extent` and embed it.
7972
- `crs` will be embedded as-is
8073
81-
## Threading
74+
### Threading
8275
8376
Threading is used at the outermost level possible - over
8477
an array, feature collection, or e.g. a MultiPolygonTrait where
8578
each `PolygonTrait` sub-geometry may be calculated on a different thread.
8679
8780
Currently, threading defaults to `false` for all objects, but can be turned on
8881
by passing the keyword argument `threaded=true` to `apply`.
82+
83+
Threading uses [StableTasks.jl](https://github.com/JuliaFolds2/StableTasks.jl) to provide
84+
type-stable tasks (base Julia `Threads.@spawn` is not type stable). This is completely cost-free
85+
and saves some allocations when running multithreaded.
86+
87+
The current strategy is to launch 2 tasks for each CPU thread, to provide load balancing. We
88+
assume Julia will manage these tasks efficiently, and we don't want to run too many tasks
89+
since each task does have some overhead when it's created. This may need revisiting in the future,
90+
but it's a pretty easy heuristic to use.
91+
92+
## Implementation
93+
94+
Literate.jl source code is below.
95+
96+
***
97+
8998
=#
9099

91100
"""
@@ -319,6 +328,18 @@ end
319328

320329
using Base.Threads: nthreads, @threads, @spawn
321330

331+
#=
332+
Here we used to use the compiler directive `@assume_effects :foldable` to force the compiler
333+
to lookup through the closure. This alone makes e.g. `flip` 2.5x faster!
334+
335+
But it caused inference to fail, so we've removed it. No effect on runtime so far as we can tell,
336+
at least in Julia 1.11.
337+
=#
338+
@inline function _maptasks(f::F, taskrange, threaded::False)::Vector where F
339+
map(f, taskrange)
340+
end
341+
342+
322343
# Threading utility, modified Mason Protters threading PSA
323344
# run `f` over ntasks, where f receives an AbstractArray/range
324345
# of linear indices
@@ -333,22 +354,12 @@ using Base.Threads: nthreads, @threads, @spawn
333354
# Map over the chunks
334355
tasks = map(task_chunks) do chunk
335356
# Spawn a task to process this chunk
336-
@spawn begin
357+
StableTasks.@spawn begin
337358
# Where we map `f` over the chunk indices
338359
map(f, chunk)
339360
end
340361
end
341362

342363
# Finally we join the results into a new vector
343364
return mapreduce(fetch, vcat, tasks)
344-
end
345-
#=
346-
Here we used to use the compiler directive `@assume_effects :foldable` to force the compiler
347-
to lookup through the closure. This alone makes e.g. `flip` 2.5x faster!
348-
349-
But it caused inference to fail, so we've removed it. No effect on runtime so far as we can tell,
350-
at least in Julia 1.11.
351-
=#
352-
@inline function _maptasks(f::F, taskrange, threaded::False)::Vector where F
353-
map(f, taskrange)
354365
end

GeometryOpsCore/src/applyreduce.jl

+33-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,36 @@ and perform some operation on it.
1818
1919
[`centroid`](@ref), [`area`](@ref) and [`distance`](@ref) have been implemented using the
2020
[`applyreduce`](@ref) framework.
21+
22+
```@docs
23+
applyreduce
24+
```
25+
26+
27+
### Threading
28+
29+
Threading is used at the outermost level possible - over
30+
an array, feature collection, or e.g. a MultiPolygonTrait where
31+
each `PolygonTrait` sub-geometry may be calculated on a different thread.
32+
33+
Currently, threading defaults to `false` for all objects, but can be turned on
34+
by passing the keyword argument `threaded=true` to `apply`.
35+
36+
Threading uses [StableTasks.jl](https://github.com/JuliaFolds2/StableTasks.jl) to provide
37+
type-stable tasks (base Julia `Threads.@spawn` is not type stable). This is completely cost-free
38+
and saves some allocations when running multithreaded.
39+
40+
The current strategy is to launch 2 tasks for each CPU thread, to provide load balancing. We
41+
assume Julia will manage these tasks efficiently, and we don't want to run too many tasks
42+
since each task does have some overhead when it's created. This may need revisiting in the future,
43+
but it's a pretty easy heuristic to use.
44+
45+
## Implementation
46+
47+
Literate.jl source code is below.
48+
49+
***
50+
2151
=#
2252

2353
"""
@@ -135,7 +165,7 @@ import Base.Threads: nthreads, @threads, @spawn
135165
# Map over the chunks
136166
tasks = map(task_chunks) do chunk
137167
# Spawn a task to process this chunk
138-
@spawn begin
168+
StableTasks.@spawn begin
139169
# Where we map `f` over the chunk indices
140170
mapreduce(f, op, chunk; init)
141171
end
@@ -144,6 +174,7 @@ import Base.Threads: nthreads, @threads, @spawn
144174
# Finally we join the results into a new vector
145175
return mapreduce(fetch, op, tasks; init)
146176
end
147-
Base.@assume_effects :foldable function _mapreducetasks(f::F, op, taskrange, threaded::False; init) where F
177+
178+
function _mapreducetasks(f::F, op, taskrange, threaded::False; init) where F
148179
mapreduce(f, op, taskrange; init)
149180
end

GeometryOpsCore/src/constants.jl

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"The semi-major axis of the WGS84 ellipsoid"
2+
const WGS84_EARTH_SEMI_MAJOR_RADIUS = 6378137.0
3+
4+
"The inverse flattening of the WGS84 ellipsoid"
5+
const WGS84_EARTH_INV_FLATTENING = 298.257223563
6+
7+
"The mean radius of the WGS84 ellipsoid, used for spherical manifold default"
8+
const WGS84_EARTH_MEAN_RADIUS = 6371008.8

0 commit comments

Comments
 (0)