|
| 1 | +#= |
| 2 | +# Geometry providers |
| 3 | +
|
| 4 | +This file benchmarks GeometryOps methods on every GeoInterface.jl implementation we can find, in order to test: |
| 5 | +a. genericness, i.e., does GeometryOps work correctly with all GeoInterface.jl implementations? |
| 6 | +b. performance, i.e., how does GeometryOps compare to the native implementation? |
| 7 | +c. performance issues in the packages' implementations of GeoInterface |
| 8 | +=# |
| 9 | + |
| 10 | +# First, we import the providers: |
| 11 | +using ArchGDAL, LibGEOS, Shapefile, GeoJSON, WellKnownGeometry, GeometryBasics, GeoInterface, GeoFormatTypes |
| 12 | +PROVIDERS = (ArchGDAL, LibGEOS, GeometryBasics, GI.Wrappers) |
| 13 | +# Now, we import GeoInterface and GeometryOps, |
| 14 | +import GeometryOps as GO, GeoInterface as GI |
| 15 | +# Finally, we import some utility benchmarking, plotting and data munging packages! |
| 16 | +using BenchmarkTools, Chairmarks, CairoMakie, MakieThemes, DataFrames, Proj |
| 17 | +using CoordinateTransformations, Rotations |
| 18 | + |
| 19 | + |
| 20 | +# Polylabel.jl is a package that finds the "pole of inaccessibility" of a polygon, |
| 21 | +# i.e., the point within it that is furthest away from its boundaries. |
| 22 | + |
| 23 | +# It depends on GeometryOps, but in this instance, we'll grab some of its test geometries |
| 24 | +# to use. |
| 25 | +import Polylabel |
| 26 | + |
| 27 | +# TODO: the reason we change to LibGEOS intermediately here is so that the |
| 28 | +# linear rings of the WKG polygons are interpreted correctly. Unfortunately |
| 29 | +# that doesn't work when read, which there's an issue up for. |
| 30 | +water1 = GeoFormatTypes.WellKnownText(GeoFormatTypes.Geom(), readchomp(joinpath(dirname(dirname(pathof(Polylabel))), "test", "data", "water1.wkt")) |> String) |> x -> GI.convert(LibGEOS, x) |> GO.tuples |
| 31 | +water2 = GeoFormatTypes.WellKnownText(GeoFormatTypes.Geom(), readchomp(joinpath(dirname(dirname(pathof(Polylabel))), "test", "data", "water2.wkt")) |> String) |> x -> GI.convert(LibGEOS, x) |> GO.tuples |
| 32 | +# To fix these polygons is a complicated task, and even then LibGEOS gets it wrong: |
| 33 | +# water1 |> x -> LibGEOS.makeValid(GI.convert(LibGEOS, x)) |> GI.getgeom |> collect |> x -> filter(y -> GI.trait(y) isa Union{GI.PolygonTrait, GI.MultiPolygonTrait}, x) |> first |> GO.tuples # hide |
| 34 | + |
| 35 | +f, a, p = poly(water1; axis = (; title = "water1")); poly(f[1, 2], water2; axis = (; title = "water2")); f |
| 36 | +# Now, we rotate the `water1` polygon about its centroid, so we can use it to |
| 37 | +# test the time it takes to intersect complex polygons: |
| 38 | +water1r = GO.transform( |
| 39 | + Translation(GO.centroid(water1)) ∘ LinearMap(Makie.rotmatrix2d(π/2)) ∘ Translation((-).(GO.centroid(water1))), |
| 40 | + water1 |
| 41 | +) |
| 42 | +f, a, p = poly(water1; label = "Original") |
| 43 | +poly!(water1r; label = "Rotated") |
| 44 | +axislegend(a) |
| 45 | +f |
| 46 | +# WARNING: does not work |
| 47 | +@b GO.union($(water1), $(water1r); target = GI.PolygonTrait()) seconds=3 |
| 48 | +@b LibGEOS.union($(GI.convert(LibGEOS, water1)), $(GI.convert(LibGEOS, water1r))) seconds=3 |
| 49 | +@b ArchGDAL.union($(GI.convert(ArchGDAL, water1)), $(GI.convert(ArchGDAL, water1r))) seconds=3 |
| 50 | + |
| 51 | +poly(GO.union(w1g, w1rg; target = GI.PolygonTrait())) |
| 52 | + |
| 53 | +GI.getgeom(water1, 3) |> GI.trait |
| 54 | + |
| 55 | +# We can benchmark each provider and see if any of them have glaring issues. |
| 56 | + |
| 57 | +water1_centroid_suite = BenchmarkGroup() |
| 58 | + |
| 59 | +for provider in PROVIDERS |
| 60 | + @info "Benchmarking $provider" |
| 61 | + geom = GI.convert(provider, water1) |
| 62 | + water1_centroid_suite[string(provider)] = @be GO.centroid($geom) seconds=3 |
| 63 | +end |
| 64 | + |
| 65 | + |
| 66 | +# ## Tables.jl performance in `apply` |
| 67 | +#= |
| 68 | +This code checks how Tables.jl performs when using `apply`. |
| 69 | +We use two sources for this: `Shapefile.jl` and `DataFrames.jl`. |
| 70 | +More will be coming in the future! |
| 71 | +=# |
| 72 | +shp_file = "/Users/anshul/Downloads/ne_10m_admin_0_countries (1)/ne_10m_admin_0_countries.shp" |
| 73 | +table = Shapefile.Table(shp_file) |
| 74 | +go_df = DataFrame(table) |
| 75 | +go_df.geometry = GO.tuples(go_df.geometry); |
| 76 | + |
| 77 | +table_suite = BenchmarkGroup() |
| 78 | + |
| 79 | + |
| 80 | +ll2moll = Proj.Transformation("+proj=longlat +datum=WGS84", "+proj=moll") |
| 81 | + |
| 82 | +# First, we try reprojecting the geometries using Proj, |
| 83 | +reproject_suite = table_suite["reproject"] = BenchmarkGroup(["title:Reproject", "subtitle:All country borders from Natural Earth, 1:10m res."]) |
| 84 | + |
| 85 | +reproject_suite["Shapefile.Table"] = @be GO.reproject($table, $ll2moll) seconds=3 |
| 86 | +reproject_suite["DataFrame (Shapefile)"] = @be GO.reproject($(DataFrame(table)), $ll2moll) seconds=3 |
| 87 | +reproject_suite["DataFrame (GO)"] = @be GO.reproject($(go_df), $ll2moll) seconds=3 |
| 88 | +reproject_suite["Shapefile geoms"] = @be GO.reproject($(table.geometry), $ll2moll) seconds=3 |
| 89 | +reproject_suite["GeometryOps geoms"] = @be GO.reproject($(GO.tuples(table.geometry)), $ll2moll) seconds=3 |
| 90 | + |
| 91 | +# then transforming, just to see the difference in runtime |
| 92 | +# between calling out to C vs pure Julia, |
| 93 | +function _scaleby5(x) |
| 94 | + return x .* 5 |
| 95 | +end |
| 96 | + |
| 97 | +transform_suite = table_suite["transform"] = BenchmarkGroup(["title:Transform", "subtitle:All country borders from Natural Earth, 1:10m res."]) |
| 98 | +transform_suite["Shapefile.Table"] = @be GO.transform($_scaleby5, $table) seconds=3 |
| 99 | +transform_suite["DataFrame (Shapefile)"] = @be GO.transform($_scaleby5, $(DataFrame(table))) seconds=3 |
| 100 | +transform_suite["DataFrame (GO)"] = @be GO.transform($_scaleby5, $(go_df)) seconds=3 |
| 101 | +transform_suite["Shapefile geoms"] = @be GO.transform($_scaleby5, $(table.geometry)) seconds=3 |
| 102 | +transform_suite["GeometryOps geoms"] = @be GO.transform($_scaleby5, $(GO.tuples(table.geometry))) seconds=3 |
| 103 | + |
| 104 | +# and finally, calling `applyreduce` to find the area of each |
| 105 | +# polygon. |
| 106 | +area_suite = table_suite["area"] = BenchmarkGroup(["title:Area", "subtitle:All country borders from Natural Earth, 1:10m res."]) |
| 107 | + |
| 108 | +area_suite["Shapefile.Table"] = @be GO.area($(table)) seconds=3 |
| 109 | +area_suite["DataFrame (Shapefile)"] = @be GO.area($(DataFrame(table))) seconds=3 |
| 110 | +area_suite["DataFrame (GO)"] = @be GO.area($(go_df)) seconds=3 |
| 111 | +area_suite["Shapefile geoms"] = @be GO.area($(table.geometry)) seconds=3 |
| 112 | +area_suite["GeometryOps geoms"] = @be GO.area($(GO.tuples(table.geometry))) seconds=3 |
| 113 | + |
| 114 | +ts = getproperty.(area_suite["Shapefile.Table"].samples, :time) |
| 115 | +boxplot(ones(length(ts)), ts) |
| 116 | +violin(ones(length(ts)), ts; npoints = 3500, axis = (; yscale = log10,)) |
| 117 | + |
| 118 | + |
| 119 | +# ## Plotting |
| 120 | +function Makie.convert_arguments(::Makie.PointBased, xs, bs::AbstractVector{<: Chairmarks.Benchmark}) |
| 121 | + ts = getproperty.(Statistics.mean.(bs), :time) |
| 122 | + return (xs, ts) |
| 123 | +end |
| 124 | + |
| 125 | +function Makie.convert_arguments(::Makie.PointBased, bs::AbstractVector{<: Chairmarks.Benchmark}) |
| 126 | + ts = getproperty.(Statistics.mean.(bs), :time) |
| 127 | + return (1:length(bs), ts) |
| 128 | +end |
| 129 | + |
| 130 | +function Makie.convert_arguments(::Makie.SampleBased, b::Chairmarks.Benchmark) |
| 131 | + ts = getproperty.(b.samples, :time) |
| 132 | + return (ones(length(ts)), ts) |
| 133 | +end |
| 134 | + |
| 135 | +function Makie.convert_arguments(::Makie.SampleBased, n::Number, b::Chairmarks.Benchmark) |
| 136 | + ts = getproperty.(b.samples, :time) |
| 137 | + return (fill(n, length(ts)), ts) |
| 138 | +end |
| 139 | + |
| 140 | +function Makie.convert_arguments(::Makie.SampleBased, labels::AbstractVector{<: AbstractString}, bs::AbstractVector{<: Chairmarks.Benchmark}) |
| 141 | + ts = map(b -> getproperty.(b.samples, :time), bs) |
| 142 | + labels = |
| 143 | + return flatten |
| 144 | +end |
| 145 | + |
| 146 | +function Makie.convert_arguments(::Type{Makie.Errorbars}, xs, bs::AbstractVector{<: Chairmarks.Benchmark}) |
| 147 | + ts = map(b -> getproperty.(b.samples, :time), bs) |
| 148 | + means = map(Statistics.mean, ts) |
| 149 | + stds = map(Statistics.std, ts) |
| 150 | + return (xs, ts) |
| 151 | +end |
| 152 | + |
| 153 | +ks = keys(area_suite) |> collect .|> identity |
| 154 | + |
| 155 | +bs = getindex.((area_suite,), ks) |
| 156 | +b_lengths = length.(getproperty.(bs, :samples)) |
| 157 | +b_timing_flattened = collect(Iterators.flatten(Iterators.map(b -> getproperty.(b.samples, :time), bs))) |
| 158 | +k_strings = Iterators.flatten((fill(k, bl) for (k, bl) in zip(ks, b_lengths))) |> collect |
| 159 | + |
| 160 | +f = Figure() |
| 161 | +ax = Axis(f[1, 1]; |
| 162 | + convert_dim_1=Makie.CategoricalConversion(; sortby=nothing), |
| 163 | +) |
| 164 | +violin!(ax, k_strings, b_timing_flattened .|> log10) |
| 165 | +f |
| 166 | +ax.yscale = log10 |
| 167 | +ax.xticklabelrotation = π/12 |
| 168 | +f |
| 169 | + |
| 170 | + |
| 171 | +bs = values(area_suite) |> collect .|> identity |
| 172 | +labels = ["ST", "DS", "DG", "SG", "GG"] |
| 173 | + |
| 174 | + |
| 175 | +using AlgebraOfGraphics |
| 176 | + |
| 177 | +boxplot(b1) |
| 178 | +boxplot!.(1:5, values(area_suite) |> collect .|> identity) |
| 179 | +Makie.current_figure() |
| 180 | +Makie.current_axis().yscale = log10 |
| 181 | + |
| 182 | +data((; x = labels, y = bs)) * mapping(:y => verbatim, :x, :y) * visual(BoxPlot) |> draw |
0 commit comments