Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add a kerneldensity method #584

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Setfield = "efcf1570-3423-57d1-acb7-fd33fddbac46"
ArchGDAL = "c9ce4bd3-c3d5-55b8-8973-c0e20141b8c3"
CoordinateTransformations = "150eb455-5306-5404-9cee-2592286d6298"
HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f"
KernelDensity = "5ab0869b-81aa-558d-bb23-cbf5423bbe9b"
Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a"
NCDatasets = "85f8d34a-cbdd-5861-8df4-14fed0d494ab"
RasterDataSources = "3cb90ccd-e1b6-4867-9617-4276c8b2ca36"
Expand All @@ -36,6 +37,7 @@ RasterDataSources = "3cb90ccd-e1b6-4867-9617-4276c8b2ca36"
RastersArchGDALExt = "ArchGDAL"
RastersCoordinateTransformationsExt = "CoordinateTransformations"
RastersHDF5Ext = "HDF5"
RastersKernelDensityExt = "KernelDensity"
RastersMakieExt = "Makie"
RastersNCDatasetsExt = "NCDatasets"
RastersRasterDataSourcesExt = "RasterDataSources"
Expand All @@ -54,6 +56,7 @@ Flatten = "0.4"
GeoFormatTypes = "0.4"
GeoInterface = "1"
HDF5 = "0.14, 0.15, 0.16"
KernelDensity = "0.6"
Makie = "0.19, 0.20"
Missings = "0.4, 1"
NCDatasets = "0.10, 0.11, 0.12, 0.13"
Expand All @@ -74,6 +77,7 @@ CoordinateTransformations = "150eb455-5306-5404-9cee-2592286d6298"
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326"
HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f"
KernelDensity = "5ab0869b-81aa-558d-bb23-cbf5423bbe9b"
Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a"
NCDatasets = "85f8d34a-cbdd-5861-8df4-14fed0d494ab"
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
Expand All @@ -84,4 +88,4 @@ Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Aqua", "ArchGDAL", "CFTime", "CoordinateTransformations", "DataFrames", "GeometryBasics", "HDF5", "NCDatasets", "Plots", "RasterDataSources", "SafeTestsets", "Shapefile", "Statistics", "Test"]
test = ["Aqua", "ArchGDAL", "CFTime", "CoordinateTransformations", "DataFrames", "GeometryBasics", "HDF5", "KernelDensity", "NCDatasets", "Plots", "RasterDataSources", "SafeTestsets", "Shapefile", "Statistics", "Test"]
82 changes: 82 additions & 0 deletions ext/RastersKernelDensityExt/RastersKernelDensityExt.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
module RastersKernelDensityExt

using Extents,
GeoInterface,
KernelDensity,
Rasters

using Rasters: Tables, maybeshiftlocus, Center, lookup, _extent2dims

const GI = GeoInterface

const VectorTuple = Tuple{<:AbstractVector{<:Number},<:AbstractVector{<:Number}}

function Rasters.kerneldensity(geom; geomcolumn=nothing, kw...)
if Tables.istable(typeof(geom))
if geomcolumn isa Tuple
# Use the columns directly and skip _to_vectors
data = (Tables.getcolumn(geom, geomcolumn[1]), Tables.getcolumn(geom, geomcolumn[2]))
return _kerneldensity(data; kw...)
elseif isnothing(geomcolumn)
geomcolumn = GI.geometrycolumns(geom)[1]
end
geom = GI.getcolumn(geom, geomcolumn)
end
return _kerneldensity(_to_vectors(geom); kw...)
end
# Also accept the regular KernelDensity.jl data formats
Rasters.kerneldensity(data::VectorTuple; kw...) = _kerneldensity(data; kw...)
Rasters.kerneldensity(data::AbstractMatrix{<:Number}; kw...) =
_kerneldensity((data[:, 1], data[:, 2]); kw...)

function _kerneldensity(data::VectorTuple;
to=nothing, size=nothing, res=nothing, crs=nothing, kw...
)
# Get the extent from `data` if there is no `to` keyword
if isnothing(to)
x, y = map(extrema, data)
to = Extents.Extent(; X=x, Y=y)
end
# Convert input keywords to Dimensions
ds = _extent2dims(to; size, res, crs)

# KernelDensity needs midpoints
midpoint_ranges = map(parent ∘ lookup, maybeshiftlocus(Center(), ds))

# Create a bivariate kernel density estimator
bkde = KernelDensity.kde(data, midpoint_ranges; kw...)

# Return a raster
return Raster(bkde.density, ds)
end

_to_vectors(geoms) = _to_vectors(GI.trait(geoms), geoms)
function _to_vectors(::Nothing, geoms::AbstractArray)
if all(GI.geomtrait(g) isa PointTrait for g in geoms)
return _fill_vectors(geoms, length(geoms))
elseif all(GI.geomtrait(g) isa MultiPointTrait for g in geoms)
n = sum(GI.npoint(g) for g in geoms)
points = Iterators.flatten(GI.getpoint(g) for g in geoms)
return _fill_vectors(points, n)
end
end
function _to_vectors(::GI.MultiPointTrait, geom)
n = GI.npoint(geom)
points = Iterators.flatten(GI.getpoint(g) for g in geoms)
return _fill_vectors(points, n)
end
_to_vectors(::PointTrait, geom) = [GI.x(geom), GI.y(geom)]
function _to_vectors(trait, geom)
throw(ArgumentError("$trait not supported: only `MultiPointTrait` `PointTrait` and `MultiPointTrait` are supported"))
end

function _fill_vectors(points, n)
vec1 = Vector{Float64}(undef, n)
vec2 = Vector{Float64}(undef, n)
for (i, p) in enumerate(points)
vec1[i], vec2[i] = GI.x(p), GI.y(p)
end
return (vec1, vec2)
end

end
2 changes: 1 addition & 1 deletion src/Rasters.jl
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export missingval, boolmask, missingmask, replace_missing, replace_missing!,
aggregate, aggregate!, disaggregate, disaggregate!, mask, mask!,
resample, warp, zonal, crop, extend, trim, slice, combine, points,
classify, classify!, mosaic, mosaic!, extract, rasterize, rasterize!,
coverage, coverage!, setcrs, setmappedcrs, smapseries, cellsize
coverage, coverage!, setcrs, setmappedcrs, smapseries, cellsize, kerneldensity
export crs, mappedcrs, mappedindex, mappedbounds, projectedindex, projectedbounds
export reproject, convertlookup

Expand Down
35 changes: 35 additions & 0 deletions src/extensions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,41 @@ $EXPERIMENTAL
"""
cellsize(args...; kw...) = throw_extension_error(cellsize, "ArchGDAL", :RastersArchGDALExt, args)


"""
kerneldensity(geoms; kw...)

Calculate the kernel density of the points in `geoms` using KernelDensity.jl.

Returns a `Raster`.

## Keywords

- `geoms`: an iterable of GeoInterface.jl compatible points or multipoints,
or a Tables.jl compatible table with a geometry column. KernelDensity.jl
compatible 2-column `Matrix` or `Tuple` of `Vectors` are also supported.
$TO_KEYWORD
$RES_KEYWORD
$SIZE_KEYWORD
$CRS_KEYWORD
$GEOMCOLUMN_KEYWORD
- `kernel`: a distribution from Distributions.jl, default is `Normal()`.
- `bandwidth` bandwidth to be passed to the kernel density estimator.
- `weights`: weights to be passed to the kernel density estimator.

## Example

```julia
using Rasters, KernelDensity
points = zip(rand(Normal(), 100), rand(Normal(), 100))

kerneldensity(points; res=0.1, weights=rand(100))
```

$EXPERIMENTAL
"""
kerneldensity(args...; kw...) = throw_extension_error(warp, "KernelDensity", :RastersKernelDensityExt, args)

# Other shared stubs
function layerkeys end
function smapseries end
Expand Down
1 change: 1 addition & 0 deletions src/methods/rasterize.jl
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,7 @@ These are detected automatically from `data` where possible.

$GEOM_KEYWORDS
$RASTERIZE_KEYWORDS
$GEOMCOLUMN_KEYWORD
$FILENAME_KEYWORD
$SUFFIX_KEYWORD

Expand Down
6 changes: 6 additions & 0 deletions src/methods/shared_docstrings.jl
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,9 @@ const PROGRESS_KEYWORD = """
const VERBOSE_KEYWORD = """
- `vebose`: whether to print messages about potential problems. `true` by default.
"""

const GEOMCOLUMN_KEYWORD = """
- `geomcolumn`: `Symbol` to manually select the column the geometries are in
when `data` is a Tables.jl compatible table, or a tuple of `Symbol` for columns
of point coordinates.
"""
17 changes: 8 additions & 9 deletions src/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -128,15 +128,14 @@ function _without_mapped_crs(f, st::AbstractRasterStack, mappedcrs::GeoFormat)
return x
end

function _extent2dims(to; size=nothing, res=nothing, crs=nothing, kw...)
_extent2dims(to, size, res, crs)
end
function _extent2dims(to::Extents.Extent, size::Nothing, res::Nothing, crs)
isnothing(res) && throw(ArgumentError("Pass either `size` or `res` keywords or a `Tuple` of `Dimension`s for `to`."))
end
function _extent2dims(to::Extents.Extent, size, res, crs)
isnothing(res) || _size_and_res_error()
end
_extent2dims(to; size=nothing, res=nothing, crs=nothing, kw...) = _extent2dims(to, size, res, crs)
_extent2dims(to::DimTuple, size::Nothing, res::Nothing, crs) = setcrs(to, crs)
_extent2dims(to::DimTuple, size::Nothing, res::Nothing, crs::Nothing) = to
_extent2dims(to, size::Nothing, res::Nothing, crs) = _extent2dims(dims(to), size, res, crs)
_extent2dims(to, size, res, crs) = _extent2dims(Extents.extent(to), size, res, crs)
_extent2dims(to::Extents.Extent, size, res, crs) = _size_and_res_error()
_extent2dims(to::Union{Nothing,Extents.Extent}, size::Nothing, res::Nothing, crs) =
throw(ArgumentError("Pass either `size` or `res` keywords or a `Tuple` of `Dimension`s for `to`."))
function _extent2dims(to::Extents.Extent{K}, size::Nothing, res::Real, crs) where K
tuple_res = ntuple(_ -> res, length(K))
_extent2dims(to, size, tuple_res, crs)
Expand Down
36 changes: 36 additions & 0 deletions test/kerneldensity.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Test, Rasters, KernelDensity, Extents

points = collect(zip(rand(100) .* 10, rand(100)))
# Using just res, detecting extent from points
res_rast = kerneldensity(points; res=(0.1, 0.01))
# This doesn't exactly match, someting is wrong with `_extent2dims`
@test_broken map(step, dims(res_rast)) == (0.1, 0.01)
# Using just size, detecting extent from points
size_rast = kerneldensity(points; size=250)
@test size(size_rast) == (250, 250)
# Using a defined extent with res
to = Extents.Extent(X=(0, 10), Y=(0, 5))
ext_res_rast = kerneldensity(points; to, res=0.05)
@test size(ext_rast) == (200, 100)
@test map(step, dims(ext_rast)) == (0.05, 0.05)
# Using a defined extent with size
to = Extents.Extent(X=(0, 10), Y=(0, 5))
ext_res_rast = kerneldensity(points; to, size=(200, 100))
@test size(ext_rast) == (200, 100)
@test map(step, dims(ext_rast)) == (0.05, 0.05)
# Using a defined DimArray
to = rand(X(0:0.01:10), Y(0:0.001:1))
template_rast = kerneldensity(points; to)
@test size(to) == size(template_rast)
@test map(step, dims(template_rast)) == (0.01, 0.001)

# Test plots
# using GLMakie
# p = Makie.heatmap(res_rast)
# Makie.scatter!(points)
# p = Makie.heatmap(size_rast)
# Makie.scatter!(points)
# p = Makie.heatmap(ext_rast)
# Makie.scatter!(points)
# p = Makie.heatmap(template_rast)
# Makie.scatter!(points)
2 changes: 1 addition & 1 deletion test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ end
@time @safetestset "reproject" begin include("reproject.jl") end
@time @safetestset "warp" begin include("warp.jl") end
@time @safetestset "resample" begin include("resample.jl") end

@time @safetestset "kernel density" begin include("kerneldensity.jl") end

# Only test SMAP locally for now, also RasterDataSources because CI dowloads keep breaking
@time @safetestset "ncdatasets" begin include("sources/ncdatasets.jl") end
Expand Down
Loading