diff --git a/Project.toml b/Project.toml index 5bd70e181..e66cb778b 100644 --- a/Project.toml +++ b/Project.toml @@ -48,7 +48,7 @@ AlgebraOfGraphics = "0.8, 0.9, 0.10, 0.11" Aqua = "0.8" ArrayInterface = "7" BenchmarkTools = "1" -CairoMakie = "0.10, 0.11, 0.12" +CairoMakie = "0.10, 0.11, 0.12, 0.13" CategoricalArrays = "0.10" ColorTypes = "0.11" Combinatorics = "1" diff --git a/src/Dimensions/Dimensions.jl b/src/Dimensions/Dimensions.jl index aec431d40..f9392e1b2 100644 --- a/src/Dimensions/Dimensions.jl +++ b/src/Dimensions/Dimensions.jl @@ -24,7 +24,7 @@ const LU = Lookups const LookupArrays = Lookups import .Lookups: rebuild, order, span, sampling, locus, val, index, set, _set, - metadata, bounds, intervalbounds, units, basetypeof, unwrap, selectindices, hasselection, + metadata, bounds, intervalbounds, units, basetypeof, unwrap, selectindices, hasselection, hasmultipledimensions, shiftlocus, maybeshiftlocus, ordered_first, ordered_last, ordered_firstindex, ordered_lastindex, promote_first, _remove using .Lookups: StandardIndices, SelTuple, CategoricalEltypes, diff --git a/src/Dimensions/dimension.jl b/src/Dimensions/dimension.jl index f5e2b2866..d2d5e358c 100644 --- a/src/Dimensions/dimension.jl +++ b/src/Dimensions/dimension.jl @@ -300,22 +300,25 @@ Base.size(dims::DimTuple) = map(length, dims) Base.CartesianIndices(dims::DimTuple) = CartesianIndices(map(d -> axes(d, 1), dims)) # Extents.jl +#= function Extents.extent(ds::DimTuple, args...) extent_dims = _astuple(dims(ds, args...)) extent_bounds = bounds(extent_dims) return Extents.Extent{name(extent_dims)}(extent_bounds) end +=# -function _experimental_extent(ds::DimTuple) - regulardims = dims(ds, x -> !(lookup(x) isa MultiDimensionalLookup)) - regular_bounds = bounds.(regulardims) +function Extents.extent(ids::DimTuple, args...) + ds = _astuple(dims(ids, args...)) + regulardims = dims(ds, x -> !hasmultipledimensions(lookup(x))) + regular_bounds = map(bounds, regulardims) regular_bounds_nt = NamedTuple{map(name, regulardims)}(regular_bounds) multidims = otherdims(ds, regulardims) - multidim_raw_bounds = bounds.(multidims) # we trust that bounds will give us a tuple of bounds one for each enclosed dimension + multidim_raw_bounds = map(bounds, multidims) # we trust that bounds will give us a tuple of bounds one for each enclosed dimension multidim_dims = combinedims(map(dims, multidims)...; length = false) multidim_bounds = map(multidim_dims) do outdim - foldl(zip(multidims, multidim_raw_bounds); init = (nothing, nothing)) do (minval, maxval), (dim, bounds) + foldl(map(tuple, multidims, multidim_raw_bounds); init = (nothing, nothing)) do (minval, maxval), (dim, bounds) if hasdim(dim, outdim) if isnothing(minval) && isnothing(maxval) bounds[dimnum(dim, outdim)] @@ -329,7 +332,7 @@ function _experimental_extent(ds::DimTuple) end end multidim_bounds_nt = NamedTuple{map(name, multidim_dims)}(multidim_bounds) - return merge(regular_bounds_nt, multidim_bounds_nt) + return Extents.Extent(merge(regular_bounds_nt, multidim_bounds_nt)) end dims(extent::Extents.Extent{K}) where K = map(rebuild, name2dim(K), values(extent)) diff --git a/src/Dimensions/indexing.jl b/src/Dimensions/indexing.jl index 930dcf7bf..f51fa3cac 100644 --- a/src/Dimensions/indexing.jl +++ b/src/Dimensions/indexing.jl @@ -47,7 +47,7 @@ Convert a `Dimension` or `Selector` `I` to indices of `Int`, `AbstractArray` or @inline function dims2indices(dims::DimTuple, I::DimTuple) extradims = otherdims(I, dims) # extra dims in the query, I # Extract "multi dimensional" lookups like MergedLookup or Rasters' GeometryLookup - multidims = Dimensions.dims(otherdims(dims, I), x -> lookup(x) isa MultiDimensionalLookup && !isempty(Dimensions.dims(x, I))) + multidims = Dimensions.dims(otherdims(dims, I), x -> hasmultipledimensions(lookup(x)) && !isempty(Dimensions.dims(x, I))) # Warn if any dims from I were not picked up by multidims actuallyextradims = otherdims(extradims, x -> any(y -> hasdim(y, x), multidims)) # one way setdiff(extradims, multidims) essentially length(actuallyextradims) > 0 && _extradimswarn(actuallyextradims) diff --git a/src/Dimensions/merged.jl b/src/Dimensions/merged.jl index a2f385f05..18a842e3d 100644 --- a/src/Dimensions/merged.jl +++ b/src/Dimensions/merged.jl @@ -1,4 +1,5 @@ abstract type MultiDimensionalLookup{T} <: Lookup{T,1} end +hasmultipledimensions(::MultiDimensionalLookup) = true """ MergedLookup <: MultiDimensionalLookup <: Lookup @@ -63,6 +64,7 @@ struct MergedLookup{T,A<:AbstractVector{T},D,Me} <: MultiDimensionalLookup{T} end MergedLookup(data, dims; metadata=NoMetadata()) = MergedLookup(data, dims, metadata) +hasmultipledimensions(::MergedLookup) = true order(m::MergedLookup) = Unordered() dims(m::MergedLookup) = m.dims dims(d::Dimension{<:MergedLookup}) = dims(val(d)) diff --git a/src/Lookups/Lookups.jl b/src/Lookups/Lookups.jl index a1dee6e06..566dbb756 100644 --- a/src/Lookups/Lookups.jl +++ b/src/Lookups/Lookups.jl @@ -36,7 +36,7 @@ export reducelookup, shiftlocus, maybeshiftlocus, promote_first export index export issampled, iscategorical, iscyclic, isnolookup, isintervals, ispoints, isregular, - isexplicit, isstart, iscenter, isend, isordered, isforward, isreverse + isexplicit, isstart, iscenter, isend, isordered, isforward, isreverse, hasmultipledimensions export Selector export At, Between, Touches, Contains, Near, Where, All diff --git a/src/Lookups/lookup_arrays.jl b/src/Lookups/lookup_arrays.jl index 99e8a2049..5d218f020 100644 --- a/src/Lookups/lookup_arrays.jl +++ b/src/Lookups/lookup_arrays.jl @@ -88,6 +88,8 @@ metadata(lookup::AutoLookup) = hasproperty(lookup.kw, :metadata) ? lookup.kw.met Base.step(lookup::AutoLookup) = Base.step(parent(lookup)) bounds(lookup::Lookup) = _bounds(order(lookup), lookup) +# Fallback for raw arrays (e.g., when lookup returns a raw range) +bounds(x::AbstractArray) = (first(x), last(x)) _bounds(::ForwardOrdered, l::Lookup) = first(l), last(l) _bounds(::ReverseOrdered, l::Lookup) = last(l), first(l) diff --git a/src/Lookups/predicates.jl b/src/Lookups/predicates.jl index 8c42e4248..9f6459030 100644 --- a/src/Lookups/predicates.jl +++ b/src/Lookups/predicates.jl @@ -29,6 +29,8 @@ isintervals(::Intervals) = true isintervals(::Points) = false ispoints(::Points) = true ispoints(::Intervals) = false +hasmultipledimensions(::Lookup) = false +hasmultipledimensions(::Any) = false # Fallback for non-Lookup types (e.g., raw arrays) # Forward them from lookups for f in (:isregular, :isexplicit) diff --git a/test/merged.jl b/test/merged.jl index 1ef581b7f..bcf93064e 100644 --- a/test/merged.jl +++ b/test/merged.jl @@ -1,4 +1,4 @@ -using DimensionalData, Test, Unitful +using DimensionalData, Test, Unitful, Extents using DimensionalData.Lookups, DimensionalData.Dimensions using Statistics: mean using DimensionalData.Dimensions: SelOrStandard @@ -38,7 +38,7 @@ end @test index(da[Coord(:, Between(1, 2), :)], Coord) == [(1.0,1.0,1.0), (1.0,2.0,2.0)] -@test bounds(da) == (((1.0, 3.0), (1.0, 4.0), (1.0, 4.0)),) +@test DimensionalData.bounds(da) == (((1.0, 3.0), (1.0, 4.0), (1.0, 4.0)),) @testset "merged named reduction" begin m = mean(da2; dims=Coord) @@ -107,4 +107,110 @@ end da = ones(X(1:10), Y(1:10), Dim{:random}(1:10)) merged = mergedims(da, (X, Y) => :space) @test_warn "Z" merged[Z(1)] +end + +@testset "hasmultipledimensions trait for PR #991" begin + # Test default behavior for regular lookups + @testset "Regular lookups return false" begin + @test hasmultipledimensions(NoLookup()) == false + @test hasmultipledimensions(Sampled(1:10)) == false + @test hasmultipledimensions(Categorical([:a, :b, :c])) == false + @test hasmultipledimensions(Cyclic(1:12; cycle=12)) == false + @test hasmultipledimensions(Sampled(1:10; sampling=Points())) == false + @test hasmultipledimensions(Sampled(1:10; sampling=Intervals())) == false + end + + @testset "MergedLookup returns true" begin + x_dim = X(1:3) + y_dim = Y(10:10:30) + merged_data = vec(DimPoints((x_dim, y_dim))) + merged_lookup = Dimensions.MergedLookup(merged_data, (x_dim, y_dim)) + @test hasmultipledimensions(merged_lookup) == true + end + + @testset "Extent passthrough for MergedLookup" begin + # Basic extent with MergedLookup + x_vals = 1.0:3.0 + y_vals = 10.0:10.0:30.0 + x_dim = X(x_vals) + y_dim = Y(y_vals) + + merged_dims = mergedims((x_dim, y_dim) => :space) + ext = Extents.extent((merged_dims,)) + + @test haskey(ext, :X) + @test haskey(ext, :Y) + @test ext.X == (1.0, 3.0) + @test ext.Y == (10.0, 30.0) + + # Mixed regular and merged dimensions + t_dim = Ti(1:5) + z_dim = Z(100:100:300) + all_dims = (t_dim, merged_dims, z_dim) + ext2 = Extents.extent(all_dims) + + @test ext2.Ti == (1, 5) + @test ext2.Z == (100, 300) + @test ext2.X == (1.0, 3.0) + @test ext2.Y == (10.0, 30.0) + end + + @testset "Multiple merged dimensions extent" begin + x1_dim = X(1:3) + y1_dim = Y(10:10:30) + t_dim = Ti(0.0:0.5:1.0) + z_dim = Z(-5:5) + + merged_space = mergedims((x1_dim, y1_dim) => :space) + merged_tz = mergedims((t_dim, z_dim) => :timez) + all_dims = (merged_space, merged_tz) + + ext = Extents.extent(all_dims) + @test ext.X == (1, 3) + @test ext.Y == (10, 30) + @test ext.Ti == (0.0, 1.0) + @test ext.Z == (-5, 5) + end + + @testset "Extent with subset of dimensions" begin + x_dim = X(1:5) + y_dim = Y(10:10:50) + z_dim = Z(100:100:300) + + merged = mergedims((x_dim, y_dim) => :space) + all_dims = (merged, z_dim) + + # Get extent for just the Z dimension + ext_z = Extents.extent(all_dims, Z) + @test ext_z.Z == (100, 300) + @test !haskey(ext_z, :X) + @test !haskey(ext_z, :Y) + + # Get extent for merged dimension by name + ext_space = Extents.extent(all_dims, :space) + @test ext_space.X == (1, 5) + @test ext_space.Y == (10, 50) + @test !haskey(ext_space, :Z) + end + + @testset "Operations preserve hasmultipledimensions" begin + x = X(1:3) + y = Y(10:10:30) + data = rand(3, 3) + + da = DimArray(data, (x, y)) + merged_da = mergedims(da, (X, Y) => :space) + + # Broadcasting preserves trait + result = merged_da .+ 1 + @test hasmultipledimensions(lookup(dims(result, :space))) + + # Slicing with additional dimension preserves trait + z = Z(1:3) + data3d = rand(3, 3, 3) + da3d = DimArray(data3d, (x, y, z)) + merged_da3d = mergedims(da3d, (X, Y) => :space) + sliced = merged_da3d[Z(At(2))] + @test hasmultipledimensions(lookup(dims(sliced, :space))) + end end \ No newline at end of file