Skip to content

Support heterogeneous features #74

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

Merged
merged 28 commits into from
Aug 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
98f8f5b
Merge pull request #3 from JuliaGeometry/master
Sov-trotter Jul 3, 2020
f0fd7e3
Test out meta
Sov-trotter Jul 27, 2020
cc44ac0
Add new implementation
Sov-trotter Jul 28, 2020
0aa4ae1
Sov-trotter Jul 28, 2020
bd792e8
Delete metatest.jl
Sov-trotter Jul 28, 2020
c2119c4
Sov-trotter Jul 29, 2020
82d2577
Sov-trotter Jul 29, 2020
cd8d0af
Add tests
Sov-trotter Jul 29, 2020
4e0efc7
Sov-trotter Jul 29, 2020
0b4e78a
Add meta(), metafree() methods
Sov-trotter Jul 29, 2020
46220f8
Sov-trotter Jul 29, 2020
d0ea0a0
Minor Refactor
Sov-trotter Jul 29, 2020
4076f51
Sov-trotter Jul 30, 2020
3003c07
Not replace meta
Sov-trotter Aug 3, 2020
b726fd1
Merge pull request #5 from JuliaGeometry/master
Sov-trotter Aug 3, 2020
1212783
Minor refactoring
Sov-trotter Aug 4, 2020
007471a
Merge remote-tracking branch 'origin/meta_exp' into meta_exp
Sov-trotter Aug 4, 2020
f92fe69
Sov-trotter Aug 4, 2020
a29b9a9
End docstrings in old meta methods
Sov-trotter Aug 7, 2020
7394b6a
use variable name x
Sov-trotter Aug 7, 2020
9828a5a
Docstring changes; refactor getproperty; use getfield in metafree
Sov-trotter Aug 7, 2020
9dfd758
Sov-trotter Aug 7, 2020
630b9c1
Test for createinstance
Sov-trotter Aug 7, 2020
e6cd046
Sov-trotter Aug 7, 2020
196c162
Use meta_table instead of collect_metat
Sov-trotter Aug 8, 2020
6e8a93e
Field names rather than numbers in getfield
Sov-trotter Aug 8, 2020
170b2ac
Refactor function signature
Sov-trotter Aug 8, 2020
51a2ddf
Merge pull request #7 from JuliaGeometry/master
Sov-trotter Aug 14, 2020
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
3 changes: 2 additions & 1 deletion src/GeometryBasics.jl
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ module GeometryBasics
export GLTriangleFace, GLNormalMesh3D, GLPlainTriangleMesh, GLUVMesh3D, GLUVNormalMesh3D
export AbstractMesh, Mesh, TriangleMesh
export GLNormalMesh2D, PlainTriangleMesh

export MetaT, meta_table

# all the different predefined mesh types
# Note: meshes can contain arbitrary meta information,
export AbstractMesh, TriangleMesh, PlainMesh, GLPlainMesh, GLPlainMesh2D, GLPlainMesh3D
Expand Down
98 changes: 98 additions & 0 deletions src/metadata.jl
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Returns the Meta Type corresponding to `T`
E.g:
```julia
MetaType(Point) == PointMeta
```
"""
MetaType(::Type{T}) where T = error("No Meta Type for $T")

Expand All @@ -47,6 +48,7 @@ Returns the original type containing no metadata for `T`
E.g:
```julia
MetaFree(PointMeta) == Point
```
"""
MetaFree(::Type{T}) where T = error("No meta free Type for $T")

Expand Down Expand Up @@ -190,3 +192,99 @@ Base.size(x::MultiPolygonMeta) = size(metafree(x))
@meta_type(Mesh, mesh, AbstractMesh, Element <: Polytope)
Base.getindex(x::MeshMeta, idx::Int) = getindex(metafree(x), idx)
Base.size(x::MeshMeta) = size(metafree(x))


"""

MetaT(geometry, meta::NamedTuple)
MetaT(geometry; meta...)

Returns a `MetaT` that holds a geometry and its metadata

`MetaT` acts the same as `Meta` method.
The difference lies in the fact that it is designed to handle
geometries and metadata of different/heterogeneous types.

eg: While a Point MetaGeometry is a `PointMeta`, the MetaT representation is `MetaT{Point}`
The downside being it's not subtyped to `AbstractPoint` like a `PointMeta` is.

Example:
```julia
julia> MetaT(Point(1, 2), city = "Mumbai")
MetaT{Point{2,Int64},(:city,),Tuple{String}}([1, 2], (city = "Mumbai",))
```
"""
struct MetaT{T, Names, Types}
main::T
meta::NamedTuple{Names, Types}
end

MetaT(x; kwargs...) = MetaT(x, values(kwargs))

"""

metafree(x::MetaT)
metafree(x::Array{MetaT})

Free the MetaT from metadata
i.e. returns the geometry/array of geometries
"""
function metafree(x::MetaT)
getfield(x, :main)
end
metafree(x::AbstractVector{<: MetaT}) = map(metafree, x)

"""

meta(x::MetaT)
meta(x::Array{MetaT})

Returns the metadata of a `MetaT`
"""
function meta(x::MetaT)
getfield(x, :meta)
end
meta(x::AbstractVector{<: MetaT}) = map(meta, x)

# helper methods
function Base.getproperty(x::MetaT, field::Symbol)
if field == :main
metafree(x)
elseif field == :meta
meta(x)
else
getproperty(meta(x), field)
end
end

Base.propertynames(x::MetaT) = (:main, propertynames(meta(x))...)
getnamestypes(::Type{MetaT{T, Names, Types}}) where {T, Names, Types} = (T, Names, Types)

# explicitly give the "schema" of the object to StructArrays
function StructArrays.staticschema(::Type{F}) where {F<:MetaT}
T, names, types = getnamestypes(F)
NamedTuple{(:main, names...), Base.tuple_type_cons(T, types)}
end

# generate an instance of MetaT type
function StructArrays.createinstance(::Type{F}, x, args...) where {F<:MetaT}
T , names, types = getnamestypes(F)
MetaT(x, NamedTuple{names, types}(args))
end

"""
Puts an iterable of MetaT's into a StructArray
"""
function meta_table(iter)
cols = Tables.columntable(iter)
meta_table(first(cols), Base.tail(cols))
end

function meta_table(main, meta::NamedTuple{names, types}) where {names, types}
F = MetaT{eltype(main), names, StructArrays.eltypes(types)}
return StructArray{F}(; main=main, meta...)
end

Base.getindex(x::MetaT, idx::Int) = getindex(metafree(x), idx)
Base.size(x::MetaT) = size(metafree(x))

130 changes: 130 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,105 @@ using GeometryBasics: attributes
end
end

@testset "embedding MetaT" begin
@testset "MetaT{Polygon}" begin
polys = [Polygon(rand(Point{2, Float32}, 20)) for i in 1:10]
multipol = MultiPolygon(polys)
pnames = [randstring(4) for i in 1:10]
numbers = LinRange(0.0, 1.0, 10)
bin = rand(Bool, 10)
# create a polygon
poly = MetaT(polys[1], name = pnames[1], value = numbers[1], category = bin[1])
# create a MultiPolygon with the right type & meta information!
multipoly = MetaT(multipol, name = pnames, value = numbers, category = bin)
@test multipoly isa MetaT
@test poly isa MetaT

@test GeometryBasics.getcolumn(poly, :name) == pnames[1]
@test GeometryBasics.getcolumn(multipoly, :name) == pnames

meta_p = MetaT(polys[1], boundingbox=Rect(0, 0, 2, 2))
@test meta_p.boundingbox === Rect(0, 0, 2, 2)
@test GeometryBasics.metafree(meta_p) == polys[1]
@test GeometryBasics.metafree(poly) == polys[1]
@test GeometryBasics.metafree(multipoly) == multipol
@test GeometryBasics.meta(meta_p) == (boundingbox = GeometryBasics.HyperRectangle{2,Int64}([0, 0], [2, 2]),)
@test GeometryBasics.meta(poly) == (name = pnames[1], value = 0.0, category = bin[1])
@test GeometryBasics.meta(multipoly) == (name = pnames, value = numbers, category = bin)
end

@testset "MetaT{Point}" begin
p = Point(1.1, 2.2)
@test p isa AbstractVector{Float64}
pm = MetaT(Point(1.1, 2.2); a=1, b=2)
p1 = Point(2.2, 3.6)
p2 = [p, p1]
@test coordinates(p2) == p2
@test pm.meta === (a=1, b=2)
@test pm.main === p
@test propertynames(pm) == (:main, :a, :b)
@test GeometryBasics.metafree(pm) == p
@test GeometryBasics.meta(pm) == (a = 1, b = 2)
end

@testset "MetaT{MultiPoint}" begin
p = collect(Point{2, Float64}(x, x+1) for x in 1:5)
@test p isa AbstractVector
mpm = MetaT(MultiPoint(p); a=1, b=2)
@test coordinates(mpm.main) == Point{2, Float64}[(x, x+1) for x in 1:5]
@test mpm.meta === (a=1, b=2)
@test mpm.main == p
@test propertynames(mpm) == (:main, :a, :b)
@test GeometryBasics.metafree(mpm) == p
@test GeometryBasics.meta(mpm) == (a = 1, b = 2)
end

@testset "MetaT{LineString}" begin
linestring = MetaT(LineString(Point{2, Int}[(10, 10), (20, 20), (10, 40)]), a = 1, b = 2)
@test linestring isa MetaT
@test linestring.meta === (a = 1, b = 2)
@test propertynames(linestring) == (:main, :a, :b)
@test GeometryBasics.metafree(linestring) == LineString(Point{2, Int}[(10, 10), (20, 20), (10, 40)])
@test GeometryBasics.meta(linestring) == (a = 1, b = 2)
end

@testset "MetaT{MultiLineString}" begin
linestring1 = LineString(Point{2, Int}[(10, 10), (20, 20), (10, 40)])
linestring2 = LineString(Point{2, Int}[(40, 40), (30, 30), (40, 20), (30, 10)])
multilinestring = MultiLineString([linestring1, linestring2])
multilinestringmeta = MetaT(MultiLineString([linestring1, linestring2]); boundingbox = Rect(1.0, 1.0, 2.0, 2.0))
@test multilinestringmeta isa MetaT
@test multilinestringmeta.meta === (boundingbox = Rect(1.0, 1.0, 2.0, 2.0),)
@test multilinestringmeta.main == multilinestring
@test propertynames(multilinestringmeta) == (:main, :boundingbox)
@test GeometryBasics.metafree(multilinestringmeta) == multilinestring
@test GeometryBasics.meta(multilinestringmeta) == (boundingbox = GeometryBasics.HyperRectangle{2,Float64}([1.0, 1.0], [2.0, 2.0]),)
end

#=
So mesh works differently for MetaT
since `MetaT{Point}` not subtyped to `AbstractPoint`
=#

@testset "MetaT{Mesh}" begin
@testset "per vertex attributes" begin
points = rand(Point{3, Float64}, 8)
tfaces = TetrahedronFace{Int}[(1, 2, 3, 4), (5, 6, 7, 8)]
normals = rand(SVector{3, Float64}, 8)
stress = LinRange(0, 1, 8)
mesh_nometa = Mesh(points, tfaces)
mesh = MetaT(mesh_nometa, normals = normals, stress = stress)

@test hasproperty(mesh, :stress)
@test hasproperty(mesh, :normals)
@test mesh.stress == stress
@test mesh.normals == normals
@test GeometryBasics.faces(mesh.main) == tfaces
@test propertynames(mesh) == (:main, :normals, :stress)
end
end
end

@testset "view" begin
@testset "TupleView" begin
x = [1, 2, 3, 4, 5, 6]
Expand Down Expand Up @@ -505,6 +604,37 @@ end
@test <(x, x1)
end

@testset "MetaT and heterogeneous data" begin
ls = [LineString([Point(i, (i+1)^2/6), Point(i*0.86,i+5), Point(i/3, i/7)]) for i in 1:10]
mls = MultiLineString([LineString([Point(i+1, (i)^2/6), Point(i*0.75,i+8), Point(i/2.5, i/6.79)]) for i in 5:10])
poly = Polygon(Point{2, Int}[(40, 40), (20, 45), (45, 30), (40, 40)])
geom = [ls..., mls, poly]
prop = Any[(country_states = "India$(i)", rainfall = (i*9)/2) for i in 1:11]
push!(prop, (country_states = 12, rainfall = 1000)) # a pinch of heterogeneity

feat = [MetaT(i, j) for (i,j) = zip(geom, prop)]
sa = meta_table(feat)

@test nameof(eltype(feat)) == :MetaT
@test eltype(sa) === MetaT{Any,(:country_states, :rainfall),Tuple{Any,Float64}}
@test propertynames(sa) === (:main, :country_states, :rainfall)
@test getproperty(sa, :country_states) isa Array{Any}
@test getproperty(sa, :main) == geom

@test GeometryBasics.getnamestypes(typeof(feat[1])) ==
(LineString{2,Float64,Point{2,Float64},Base.ReinterpretArray{GeometryBasics.Ngon{2,Float64,2,Point{2,Float64}},1,Tuple{Point{2,Float64},Point{2,Float64}},TupleView{Tuple{Point{2,Float64},Point{2,Float64}},2,1,Array{Point{2,Float64},1}}}},
(:country_states, :rainfall), Tuple{String,Float64})

@test StructArrays.staticschema(typeof(feat[1])) ==
NamedTuple{(:main, :country_states, :rainfall),Tuple{LineString{2,Float64,Point{2,Float64},Base.ReinterpretArray{GeometryBasics.Ngon{2,Float64,2,Point{2,Float64}},1,Tuple{Point{2,Float64},Point{2,Float64}},TupleView{Tuple{Point{2,Float64},Point{2,Float64}},2,1,Array{Point{2,Float64},1}}}},
String,Float64}}

@test StructArrays.createinstance(typeof(feat[1]), LineString([Point(1, (2)^2/6), Point(1*0.86,6), Point(1/3, 1/7)]), "Mumbai", 100) isa typeof(feat[1])

@test Base.getindex(feat[1], 1) isa Line
@test Base.size(feat[1]) == (2,)
end

@testset "Tests from GeometryTypes" begin
include("geometrytypes.jl")
end
Expand Down