diff --git a/Project.toml b/Project.toml index c8aaa45..aa4bbd2 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "LocalMath" uuid = "82808884-8acc-4a5c-a056-be440a0fbea1" -version = "0.2.0-rc1" +version = "0.2.0-rc2" authors = ["Praneeth Merugu "] [deps] @@ -8,6 +8,7 @@ Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" Atomix = "a9b6321e-bd34-4604-b9c9-b65b8de01458" KernelAbstractions = "63c18a36-062a-441e-b654-da1e3ab1ce7c" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" +Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" StructArrays = "09ab397b-f2b6-538f-b94a-2f83cf4a842a" UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" @@ -16,6 +17,7 @@ Adapt = "4.7" Atomix = "1.1" KernelAbstractions = "0.9.42" StaticArrays = "1" +Statistics = "1.11" StructArrays = "0.7" UUIDs = "1.11" julia = "1.12" diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 4b26827..7fab50a 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,12 @@ +# LocalMath 0.2.0-rc2 + +- Replaces callable-first bounded-fold authoring with `LocalMath.fold(values; + ...)`, while retaining the checked `BoundedFold` compiler law. +- Adds familiar `sum`, `minimum`, `maximum`, `Statistics.mean`, and qualified + `LocalMath.geometric_mean` methods for bounded relation and Collection views. +- Gives all bounded reductions explicit result-type, empty-input, absence, and + canonical-order semantics on the existing transaction-aware executor. + # LocalMath 0.2.0-rc1 - First independently versioned release candidate of the typed local scientific-computation language. diff --git a/docs/src/api/localmath.md b/docs/src/api/localmath.md index f37a60b..97bcbe0 100644 --- a/docs/src/api/localmath.md +++ b/docs/src/api/localmath.md @@ -121,6 +121,24 @@ closes the map, accumulator, and result types before planning. second executor; the canonical implementation remains valid. Bounded scalar folding is distinct from the `OrderedFold` publication law for evolving state. +Common reductions use ordinary Julia vocabulary on those same bounded views: + +```julia +total = sum(values) +smallest = minimum(values) +largest = maximum(values) +average = Statistics.mean(values) +geometric = LocalMath.geometric_mean(values) +``` + +Canonical relation or Collection order is retained and repeated endpoints +participate repeatedly. Absent lanes do not participate. `sum` returns the +typed additive identity on empty input; `minimum` and `maximum` reject an empty +transaction; `Statistics.mean` returns the ordinary correctly typed `NaN` for +empty input. Present `NaN`, infinity, and signed zero follow Julia's ordinary +numeric operations. `LocalMath.geometric_mean` accepts concrete floating-point +values and rejects empty input or a nonpositive present value. + ## Inspection `LocalMath.inspect` is qualified because it is tooling rather than ordinary diff --git a/src/LocalMath.jl b/src/LocalMath.jl index 6102ce5..000121c 100644 --- a/src/LocalMath.jl +++ b/src/LocalMath.jl @@ -39,7 +39,7 @@ public ArgMin, ArgMax, CanonicalSourceLaneTie, TieMin, TieMax public RejectOverflow, EmptyCollection public FoldComponent, InitializedState, BoundedWrites, FoldStep public initialized_state -public BoundedFold, fold, Where +public BoundedFold, fold, geometric_mean, Where public RejectInvalid, SkipInvalid, FillInvalid, RejectEmpty public RelaxedAssociative public BoundedFoldOutcome, evaluate_bounded @@ -51,6 +51,7 @@ import Adapt import Atomix import KernelAbstractions import StaticArrays +import Statistics import StructArrays import UUIDs using KernelAbstractions: @index, @kernel, @localmem, @synchronize @@ -77,6 +78,7 @@ include("stage_planning.jl") include("execution/relation_preparation.jl") include("execution/stage_preparation.jl") include("execution/bounded_fold_support.jl") +include("bounded_reductions.jl") include("execution/workspace_support.jl") include("execution/candidate_grouping.jl") include("execution/candidate_stage.jl") diff --git a/src/authoring.jl b/src/authoring.jl index 2e0c0fc..4f050f3 100644 --- a/src/authoring.jl +++ b/src/authoring.jl @@ -3,10 +3,10 @@ include("authoring/preparation_syntax.jl") # Evaluator-side facades. Their only payload is the existing bounded read # capability, so they add no semantic or executable representation. -struct _AuthoringValues{R} +struct _AuthoringValues{T,R} read::R end -struct _AuthoringSamples{R} +struct _AuthoringSamples{T,R} read::R end struct _AuthoringIndices{R} @@ -30,8 +30,6 @@ _device_evaluator_capture(operation::_ValidationAuthoringEvaluator) = _contains_bounded_fold_type( ::Type{<:_ValidationAuthoringEvaluator}, seen = IdSet{Any}()) = true -@inline _authoring_values(read) = _AuthoringValues(read) -@inline _authoring_samples(read) = _AuthoringSamples(read) @inline _authoring_indices(read) = _AuthoringIndices(read) @inline Base.length(view::Union{ diff --git a/src/authoring/syntax.jl b/src/authoring/syntax.jl index dd28d4d..3af5cf1 100644 --- a/src/authoring/syntax.jl +++ b/src/authoring/syntax.jl @@ -1152,14 +1152,18 @@ function _lm_emit_stage( end function _lm_contains_fold_call(value) - value isa GlobalRef && - return value.mod === LocalMath && value.name === :fold + fold_names = (:fold, :minimum, :maximum, :mean, :geometric_mean) + value isa GlobalRef && return value.name in fold_names value isa QuoteNode && return false value isa Expr || return false + if value.head === :call && !isempty(value.args) + callee = first(value.args) + callee isa Symbol && callee in fold_names && return true + end if value.head === :. && length(value.args) == 2 name = value.args[2] name = name isa QuoteNode ? name.value : name - name === :fold && return true + name in fold_names && return true end return any(_lm_contains_fold_call, value.args) end diff --git a/src/bounded_reductions.jl b/src/bounded_reductions.jl new file mode 100644 index 0000000..4745984 --- /dev/null +++ b/src/bounded_reductions.jl @@ -0,0 +1,150 @@ +const _BoundedPrimitiveNumber = Union{ + Bool, + Int8, Int16, Int32, Int64, + UInt8, UInt16, UInt32, UInt64, + Float16, Float32, Float64, +} + +struct _BoundedConvert{T} end +@inline (::_BoundedConvert{T})(value) where {T} = convert(T, value) + +struct _BoundedResult end +@inline (::_BoundedResult)(accumulator, ::Int32) = accumulator + +struct _BoundedMeanResult{T} end +@inline (::_BoundedMeanResult{T})(accumulator, count::Int32) where {T} = + accumulator / T(count) + +struct _BoundedGeometricMap end +@inline (::_BoundedGeometricMap)(value) = log(value) + +struct _BoundedGeometricResult{T} end +@inline (::_BoundedGeometricResult{T})(accumulator, count::Int32) where {T} = + exp(accumulator / T(count)) + +struct _BoundedPositive end +@inline (::_BoundedPositive)(value) = value > zero(value) + +_bounded_sum_type(::Type{Bool}) = Int +_bounded_sum_type(::Type{T}) where {T<:Signed} = promote_type(Int, T) +_bounded_sum_type(::Type{T}) where {T<:Unsigned} = promote_type(UInt, T) +_bounded_sum_type(::Type{T}) where {T<:AbstractFloat} = T + +_bounded_mean_type(::Type{T}) where {T<:Integer} = Float64 +_bounded_mean_type(::Type{T}) where {T<:AbstractFloat} = T + +function _unsupported_bounded_reduction(name::Symbol, ::Type{T}) where {T} + throw(LocalMathValidationError( + "$(name) does not support this bounded value type"; + stage = :construct, + contract = :bounded_reduction_element_type, + expected = _BoundedPrimitiveNumber, + actual = T, + )) +end + +@inline function _bounded_sum_typed(values, ::Type{T}, ::Type{R}) where {T,R} + operation = _bounded_fold( + _BoundedConvert{R}(), +, zero(R), _BoundedResult(), + _AllBoundedValues(), RejectInvalid(), FillEmpty(zero(R)), + CanonicalLeftFold(), + ) + return operation(values) +end + +@inline function _bounded_minimum_typed(values, ::Type{T}) where {T} + operation = _bounded_fold( + identity, min, typemax(T), _BoundedResult(), + _AllBoundedValues(), RejectInvalid(), RejectEmpty(), + CanonicalLeftFold(), + ) + return operation(values) +end + +@inline function _bounded_maximum_typed(values, ::Type{T}) where {T} + operation = _bounded_fold( + identity, max, typemin(T), _BoundedResult(), + _AllBoundedValues(), RejectInvalid(), RejectEmpty(), + CanonicalLeftFold(), + ) + return operation(values) +end + +@inline function _bounded_mean_typed(values, ::Type{T}, ::Type{R}) where {T,R} + operation = _bounded_fold( + _BoundedConvert{R}(), +, zero(R), _BoundedMeanResult{R}(), + _AllBoundedValues(), RejectInvalid(), FillEmpty(R(NaN)), + CanonicalLeftFold(), + ) + return operation(values) +end + +@inline function _bounded_geometric_mean_typed(values, ::Type{T}) where {T} + operation = _bounded_fold( + _BoundedGeometricMap(), +, zero(T), _BoundedGeometricResult{T}(), + Where(_CONSTRUCTION_TOKEN, _BoundedPositive()), RejectInvalid(), + RejectEmpty(), CanonicalLeftFold(), + ) + return operation(values) +end + +@inline _bounded_sum(values, ::Type{T}) where {T<:_BoundedPrimitiveNumber} = + _bounded_sum_typed(values, T, _bounded_sum_type(T)) +@inline _bounded_sum(values, ::Type{T}) where {T} = + _unsupported_bounded_reduction(:sum, T) + +@inline _bounded_minimum(values, ::Type{T}) where {T<:_BoundedPrimitiveNumber} = + _bounded_minimum_typed(values, T) +@inline _bounded_minimum(values, ::Type{T}) where {T} = + _unsupported_bounded_reduction(:minimum, T) + +@inline _bounded_maximum(values, ::Type{T}) where {T<:_BoundedPrimitiveNumber} = + _bounded_maximum_typed(values, T) +@inline _bounded_maximum(values, ::Type{T}) where {T} = + _unsupported_bounded_reduction(:maximum, T) + +@inline _bounded_mean(values, ::Type{T}) where {T<:_BoundedPrimitiveNumber} = + _bounded_mean_typed(values, T, _bounded_mean_type(T)) +@inline _bounded_mean(values, ::Type{T}) where {T} = + _unsupported_bounded_reduction(:mean, T) + +for (operation, helper) in ( + (:sum, :_bounded_sum), + (:minimum, :_bounded_minimum), + (:maximum, :_bounded_maximum), + (:mean, :_bounded_mean), + ) + owner = operation === :mean ? Statistics : Base + @eval begin + @inline $owner.$operation(values::_StageRead{T}) where {T} = + $helper(values, T) + @inline $owner.$operation(values::_AuthoringValues{T}) where {T} = + $helper(values, T) + @inline $owner.$operation(values::_AuthoringSamples{T}) where {T} = + $helper(values, T) + @inline $owner.$operation(values::BoundedGroupView{K,T}) where {K,T} = + $helper(values, T) + end +end + +@inline _bounded_geometric_mean( + values, ::Type{T}) where {T<:Union{Float16,Float32,Float64}} = + _bounded_geometric_mean_typed(values, T) +@inline _bounded_geometric_mean(values, ::Type{T}) where {T} = + _unsupported_bounded_reduction(:geometric_mean, T) + +""" + LocalMath.geometric_mean(values) + +Compute the geometric mean of a bounded floating-point gather or Collection +group in canonical lane order. Absent lanes do not participate. A nonpositive +present value or empty input rejects the containing transaction. +""" +@inline geometric_mean(values::_StageRead{T}) where {T} = + _bounded_geometric_mean(values, T) +@inline geometric_mean(values::_AuthoringValues{T}) where {T} = + _bounded_geometric_mean(values, T) +@inline geometric_mean(values::_AuthoringSamples{T}) where {T} = + _bounded_geometric_mean(values, T) +@inline geometric_mean(values::BoundedGroupView{K,T}) where {K,T} = + _bounded_geometric_mean(values, T) diff --git a/src/execution/candidate_stage.jl b/src/execution/candidate_stage.jl index 1c6e5dd..3f5d547 100644 --- a/src/execution/candidate_stage.jl +++ b/src/execution/candidate_stage.jl @@ -571,11 +571,22 @@ end result, fields, item, port + 1, execution) end -@inline _stage_read(stage, access::_PreparedStageAccess, item::Int32) = - _StageRead(stage.fields, access.relation, item, _NoEvaluationValidation()) -@inline _stage_read(stage, access::_PreparedStageAccess, item::Int32, - validation) = - _StageRead(stage.fields, access.relation, item, validation) +@inline function _stage_read( + stage, access::_PreparedStageAccess{T}, item::Int32) where {T} + fields = stage.fields + relation = access.relation + validation = _NoEvaluationValidation() + return _StageRead{T,typeof(fields),typeof(relation),typeof(validation)}( + fields, relation, item, validation) +end +@inline function _stage_read( + stage, access::_PreparedStageAccess{T}, item::Int32, + validation) where {T} + fields = stage.fields + relation = access.relation + return _StageRead{T,typeof(fields),typeof(relation),typeof(validation)}( + fields, relation, item, validation) +end @inline function _stage_read(stage, access::_PreparedCollectionAccess{<:_BoundedGroup{K}}, item::Int32 ) where {K} diff --git a/src/execution/stage_preparation.jl b/src/execution/stage_preparation.jl index ec16766..394471c 100644 --- a/src/execution/stage_preparation.jl +++ b/src/execution/stage_preparation.jl @@ -1,12 +1,17 @@ # The one bridge from a cold Stage projection to the descriptor-free warm ABI. -struct _StageRead{F,R,V} +struct _StageRead{T,F,R,V} fields::F relation::R item::Int32 validation::V end +@inline _authoring_values(read::_StageRead{T}) where {T} = + _AuthoringValues{T,typeof(read)}(read) +@inline _authoring_samples(read::_StageRead{T}) where {T} = + _AuthoringSamples{T,typeof(read)}(read) + # Effect admission is intentionally performed against a host-compilable # surrogate signature. Array storage can be nested below the package-owned # bounded-read capability (rather than appearing as a top-level argument), so @@ -45,8 +50,9 @@ end _pointwise_surrogate_type(::Type{T}) where {T<:_StageSurrogateContainer} = _stage_surrogate_container_type(T) -function _pointwise_surrogate_type(::Type{_StageRead{F,R,V}}) where {F,R,V} +function _pointwise_surrogate_type(::Type{_StageRead{T,F,R,V}}) where {T,F,R,V} return _StageRead{ + T, _pointwise_surrogate_type(F), _pointwise_surrogate_type(R), V, @@ -209,7 +215,7 @@ function _validate_stage_read_methods(signature) return all(_package_owned_stage_read_protocol, reads.parameters) end -struct _PreparedStageAccess{R}; relation::R; end +struct _PreparedStageAccess{T,R}; relation::R; end struct _PreparedCollectionAccess{L,S}; law::L; storage::S; end struct _PreparedStageComponent{R}; relation::R; end struct _PreparedStageCollection{S}; storage::S; end @@ -280,7 +286,10 @@ struct _PreparedFieldGate{S<:_PreparedFieldSlot}; slot::S; end struct _PreparedStageControl{P,M,S,G} prefix::P; mask::M; subset::S; gate::G end -Adapt.@adapt_structure _PreparedStageAccess +function Adapt.adapt_structure(to, access::_PreparedStageAccess{T}) where {T} + relation = Adapt.adapt(to, access.relation) + return _PreparedStageAccess{T,typeof(relation)}(relation) +end Adapt.@adapt_structure _PreparedCollectionAccess Adapt.@adapt_structure _PreparedStageComponent Adapt.@adapt_structure _PreparedStageCollection @@ -495,8 +504,13 @@ function _projected_relation_view(validated::_ValidatedStructuralBinding, return _PreparedRelationUse(view, binding.generation, binding.status) end -_prepare_stage_access(validated, layout, use::_ProjectedRelationUse) = - _PreparedStageAccess(_projected_relation_view(validated, layout, use)) +function _prepare_stage_access(validated, layout, + use::_ProjectedRelationUse{R,<:_PreparedFieldSlot{I}}) where {R,I} + field_slot = getfield(layout.fields, I) + T = eltype(_validated_field_binding(validated, field_slot).storage) + relation = _projected_relation_view(validated, layout, use) + return _PreparedStageAccess{T,typeof(relation)}(relation) +end _prepare_stage_access(validated, layout, use::_ProjectedCollectionAccess) = _PreparedCollectionAccess(use.law, _collection_binding(validated, use.slot).storage) @@ -957,8 +971,8 @@ Base.@nospecializeinfer Base.@noinline function _stage_draft_from_projection( end -_stage_read_type(fields::Tuple, access::_PreparedStageAccess) = - Core.apply_type(_StageRead, typeof(fields), typeof(access.relation), +_stage_read_type(fields::Tuple, access::_PreparedStageAccess{T}) where {T} = + Core.apply_type(_StageRead, T, typeof(fields), typeof(access.relation), _NoEvaluationValidation) function _stage_read_type(fields::Tuple, access::_PreparedCollectionAccess{<:_BoundedGroup{K}}) where {K} diff --git a/src/execution/stage_program_kernelabstractions.jl b/src/execution/stage_program_kernelabstractions.jl index dce16fc..a8e6922 100644 --- a/src/execution/stage_program_kernelabstractions.jl +++ b/src/execution/stage_program_kernelabstractions.jl @@ -312,6 +312,11 @@ function _pointwise_source_safe(value) # optimized typed IR below must inline them to the same closed set of # load-only primitives; an opaque user invoke remains rejected. binding isa Function && return true + # Hygienic authored code retains qualified calls such as + # `Statistics.mean`. Admit the constant module name only at this + # source pass; the typed-IR pass must still resolve the selected + # function to the same closed, load-only call graph. + binding isa Module && return true binding isa Type && parentmodule(binding) in (Base, Core) && return true binding isa DataType && isconcretetype(binding) && isbitstype(binding) && _storage_free_type(binding) && return true @@ -421,8 +426,9 @@ function _pointwise_method_instance_safe( # contract makes these two compiler entry points an explicit cold-boundary # dependency. Runtime execution never consumes this inspection result. depth < _POINTWISE_CALL_DEPTH_LIMIT || return false - haskey(analysis.memo, method_instance) && + if haskey(analysis.memo, method_instance) return analysis.memo[method_instance] + end method_instance in analysis.active && return false spec = Base.unwrap_unionall(method_instance.specTypes) spec isa DataType && spec <: Tuple || return false diff --git a/test/metal/localmath_authoring.jl b/test/metal/localmath_authoring.jl index d0a468f..c09d535 100644 --- a/test/metal/localmath_authoring.jl +++ b/test/metal/localmath_authoring.jl @@ -2,6 +2,7 @@ using Test using Metal using LocalMath using StaticArrays +using Statistics struct LocalMathMetalNode end @@ -151,6 +152,66 @@ struct LocalMathMetalNode end @test Array(LocalMath.storage(rejected_fold, fold_output)) == Float32[-1, -1] + sum_output = LocalMath.Field(fold_sources, Float32) + minimum_output = LocalMath.Field(fold_sources, Float32) + maximum_output = LocalMath.Field(fold_sources, Float32) + mean_output = LocalMath.Field(fold_sources, Float32) + geometric_output = LocalMath.Field(fold_sources, Float32) + sum_law = LocalMath.@localmath item ∈ fold_sources begin + values = fold_values[neighborhoods(item)] + sum_output[item] = sum(values) + end + minimum_law = LocalMath.@localmath item ∈ fold_sources begin + values = fold_values[neighborhoods(item)] + minimum_output[item] = minimum(values) + end + maximum_law = LocalMath.@localmath item ∈ fold_sources begin + values = fold_values[neighborhoods(item)] + maximum_output[item] = maximum(values) + end + mean_law = LocalMath.@localmath item ∈ fold_sources begin + values = fold_values[neighborhoods(item)] + mean_output[item] = Statistics.mean(values) + end + geometric_law = LocalMath.@localmath item ∈ fold_sources begin + values = fold_values[neighborhoods(item)] + geometric_output[item] = LocalMath.geometric_mean(values) + end + reductions = LocalMath.sequence( + sum_law, minimum_law, maximum_law, mean_law, geometric_law) + reductions_prepared = LocalMath.prepare(reductions, + fold_values => LocalMath.Allocate(Float32[1, 4, 4, 16]), + sum_output => LocalMath.Allocate(undef), + minimum_output => LocalMath.Allocate(undef), + maximum_output => LocalMath.Allocate(undef), + mean_output => LocalMath.Allocate(undef), + geometric_output => LocalMath.Allocate(undef), + neighborhoods => LocalMath.Allocate( + reshape(Int32[1, 2, 3, 4], 2, 2)); + backend) + wait(LocalMath.execute!(reductions_prepared)) + @test Array(LocalMath.storage(reductions_prepared, sum_output)) == + Float32[5, 20] + @test Array(LocalMath.storage(reductions_prepared, minimum_output)) == + Float32[1, 4] + @test Array(LocalMath.storage(reductions_prepared, maximum_output)) == + Float32[4, 16] + @test Array(LocalMath.storage(reductions_prepared, mean_output)) == + Float32[2.5, 10] + @test Array(LocalMath.storage(reductions_prepared, geometric_output)) == + Float32[2, 8] + + rejected_geometric = LocalMath.prepare(geometric_law, + fold_values => LocalMath.Allocate(Float32[1, 0, 4, 16]), + geometric_output => LocalMath.Allocate(Float32(-1)), + neighborhoods => LocalMath.Allocate( + reshape(Int32[1, 2, 3, 4], 2, 2)); + backend) + @test_throws LocalMath.LocalMathValidationError wait( + LocalMath.execute!(rejected_geometric)) + @test Array(LocalMath.storage(rejected_geometric, geometric_output)) == + Float32[-1, -1] + fresh = LocalMath.CompactedStorage( backend, Int32, 3; group_count = 2, source_items = 6, source_position = true) diff --git a/test/metal/runtests.jl b/test/metal/runtests.jl index 3e2753f..e7f011a 100644 --- a/test/metal/runtests.jl +++ b/test/metal/runtests.jl @@ -1,6 +1,7 @@ using KernelAbstractions using LocalMath using Metal +using Statistics using Test Metal.functional() || error("the selected Metal witness is not functional") diff --git a/test/runtests.jl b/test/runtests.jl index 06bb90a..467e72e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -36,6 +36,7 @@ const LOCALMATH_INCLUDED_TESTS = ( "test_storage_authoring.jl", "test_prepare_authoring.jl", "test_localmath_authoring.jl", + "test_bounded_reductions.jl", "test_semantic_oracles.jl", ) diff --git a/test/test_bounded_reductions.jl b/test/test_bounded_reductions.jl new file mode 100644 index 0000000..e41f7a6 --- /dev/null +++ b/test/test_bounded_reductions.jl @@ -0,0 +1,121 @@ +using Statistics + +const LMBR = LocalMath + +@testset "bounded reductions match ordinary Julia values and types" begin + for T in ( + Bool, + Int8, Int16, Int32, Int64, + UInt8, UInt16, UInt32, UInt64, + Float16, Float32, Float64, + ) + data = T === Bool ? Bool[true, false, true] : T[T(1), T(1), T(2)] + view = LMBR._bounded_group_view(data, Int32(1), Int32(3), Val(3)) + @test sum(view) == sum(data) + @test typeof(sum(view)) === typeof(sum(data)) + @test minimum(view) == minimum(data) + @test typeof(minimum(view)) === typeof(minimum(data)) + @test maximum(view) == maximum(data) + @test typeof(maximum(view)) === typeof(maximum(data)) + @test mean(view) == mean(data) + @test typeof(mean(view)) === typeof(mean(data)) + + empty_view = LMBR._bounded_group_view( + T[], Int32(1), Int32(0), Val(3)) + @test sum(empty_view) == sum(T[]) + @test typeof(sum(empty_view)) === typeof(sum(T[])) + @test isnan(mean(empty_view)) + @test typeof(mean(empty_view)) === typeof(mean(T[])) + end + + floating = Float32[1, 4, 16] + floating_view = LMBR._bounded_group_view( + floating, Int32(1), Int32(3), Val(3)) + geometric = LMBR.geometric_mean(floating_view) + @test geometric ≈ 4.0f0 + @test geometric isa Float32 + + exceptional = Float32[NaN, Inf, -0.0f0, 0.0f0] + exceptional_view = LMBR._bounded_group_view( + exceptional, Int32(1), Int32(4), Val(4)) + @test isnan(sum(exceptional_view)) + @test isnan(minimum(exceptional_view)) + @test isnan(maximum(exceptional_view)) +end + +@testset "authored bounded reductions use transaction validation" begin + source = LMBR.Space(1) + values_space = LMBR.Space(3) + values = LMBR.Field(values_space, Float32) + totals = LMBR.Field(source, Float32) + minima = LMBR.Field(source, Float32) + maxima = LMBR.Field(source, Float32) + averages = LMBR.Field(source, Float32) + neighbors = LMBR.FixedRelation(source => values_space; degree = 3) + sum_law = LMBR.@localmath item ∈ source begin + gathered = values[neighbors(item)] + totals[item] = sum(gathered) + end + minimum_law = LMBR.@localmath item ∈ source begin + gathered = values[neighbors(item)] + minima[item] = minimum(gathered) + end + maximum_law = LMBR.@localmath item ∈ source begin + gathered = values[neighbors(item)] + maxima[item] = maximum(gathered) + end + mean_law = LMBR.@localmath item ∈ source begin + gathered = values[neighbors(item)] + averages[item] = Statistics.mean(gathered) + end + law = LMBR.sequence(sum_law, minimum_law, maximum_law, mean_law) + prepared = LMBR.prepare( + law, + values => Float32[1, 2, 3], + totals => zeros(Float32, 1), + minima => zeros(Float32, 1), + maxima => zeros(Float32, 1), + averages => zeros(Float32, 1), + neighbors => reshape(Int32[1, 2, 3], 3, 1); + backend = KernelAbstractions.CPU(), + ) + wait(LMBR.execute!(prepared)) + @test LMBR.storage(prepared, totals) == Float32[6] + @test LMBR.storage(prepared, minima) == Float32[1] + @test LMBR.storage(prepared, maxima) == Float32[3] + @test LMBR.storage(prepared, averages) == Float32[2] + + keys = LMBR.Field(source, Int32) + optional = LMBR.IndexRelation(keys => values_space; optional = true) + rejected_output = LMBR.Field(source, Float32) + rejected_law = LMBR.@localmath item ∈ source begin + gathered = samples(values[optional(item)]) + rejected_output[item] = minimum(gathered) + end + rejected = LMBR.prepare( + rejected_law, + keys => Int32[0], + values => Float32[1, 2, 3], + rejected_output => Float32[-1]; + backend = KernelAbstractions.CPU(), + ) + @test_throws LMBR.LocalMathValidationError wait(LMBR.execute!(rejected)) + @test LMBR.storage(rejected, rejected_output) == Float32[-1] + + floating = LMBR.Field(values_space, Float32) + geometric_output = LMBR.Field(source, Float32) + geometric_law = LMBR.@localmath item ∈ source begin + gathered = floating[neighbors(item)] + geometric_output[item] = LMBR.geometric_mean(gathered) + end + geometric_rejected = LMBR.prepare( + geometric_law, + floating => Float32[1, 0, 4], + geometric_output => Float32[-1], + neighbors => reshape(Int32[1, 2, 3], 3, 1); + backend = KernelAbstractions.CPU(), + ) + @test_throws LMBR.LocalMathValidationError wait( + LMBR.execute!(geometric_rejected)) + @test LMBR.storage(geometric_rejected, geometric_output) == Float32[-1] +end diff --git a/test/test_public_api.jl b/test/test_public_api.jl index 91390ca..47f7a07 100644 --- a/test/test_public_api.jl +++ b/test/test_public_api.jl @@ -38,7 +38,7 @@ :TieMin, :TieMax, :RejectOverflow, :EmptyCollection, :FoldComponent, :InitializedState, :initialized_state, :BoundedWrites, :FoldStep, - :BoundedFold, :fold, :Where, + :BoundedFold, :fold, :geometric_mean, :Where, :RejectInvalid, :SkipInvalid, :FillInvalid, :RejectEmpty, :RelaxedAssociative, :BoundedFoldOutcome, :evaluate_bounded, :UniqueValue, :ConditionalUniqueValue, :RoutedUniqueValue, diff --git a/test/test_stage_preparation.jl b/test/test_stage_preparation.jl index 89c0c3e..cd31926 100644 --- a/test/test_stage_preparation.jl +++ b/test/test_stage_preparation.jl @@ -194,10 +194,7 @@ end @test !(prepared.accesses[1].relation.view isa LMPRE.Relation) @test !(prepared.publications[1].components[1].relation.view isa LMPRE.Relation) - read = LMPRE._StageRead( - prepared.fields, prepared.accesses[1].relation, Int32(2), - LMPRE._NoEvaluationValidation(), - ) + read = LMPRE._stage_read(prepared, prepared.accesses[1], Int32(2)) @test length(read) == 1 @test read[1].value == 2.0f0 @test read[1].present @@ -205,13 +202,7 @@ end @test prepared.evaluator isa LMPRE._PortProjector{PreparedStageEvaluator} @test admission.result_type === NamedTuple{(:value,),Tuple{LMPRE.UniqueValue{Float32}}} - @test admission.signature == Tuple{ - Int32, - Tuple{LMPRE._StageRead{ - typeof(prepared.fields),typeof(prepared.accesses[1].relation), - LMPRE._NoEvaluationValidation}}, - Tuple{Float32}, - } + @test admission.signature == Tuple{Int32,Tuple{typeof(read)},Tuple{Float32}} @test !hasproperty(prepared, :backend) @test !hasproperty(prepared, :signature) @test LMPRE._stage_evaluator_parameters(