diff --git a/PRASCore.jl/src/Results/DemandResponseAvailability.jl b/PRASCore.jl/src/Results/DemandResponseAvailability.jl index fce28d8e..1076969f 100644 --- a/PRASCore.jl/src/Results/DemandResponseAvailability.jl +++ b/PRASCore.jl/src/Results/DemandResponseAvailability.jl @@ -30,6 +30,9 @@ struct DRAvailabilityAccumulator <: ResultAccumulator{DemandResponseAvailability end +sampledata(acc::DRAvailabilityAccumulator) = acc.available +usesamplepartitions(::DemandResponseAvailability) = true + function accumulator( sys::SystemModel{N}, nsamples::Int, ::DemandResponseAvailability ) where {N} diff --git a/PRASCore.jl/src/Results/DemandResponseEnergySamples.jl b/PRASCore.jl/src/Results/DemandResponseEnergySamples.jl index a9646ef5..927b14c7 100644 --- a/PRASCore.jl/src/Results/DemandResponseEnergySamples.jl +++ b/PRASCore.jl/src/Results/DemandResponseEnergySamples.jl @@ -32,6 +32,9 @@ struct DemandResponseEnergySamplesAccumulator <: ResultAccumulator{DemandRespons end +sampledata(acc::DemandResponseEnergySamplesAccumulator) = acc.energy +usesamplepartitions(::DemandResponseEnergySamples) = true + function accumulator( sys::SystemModel{N}, nsamples::Int, ::DemandResponseEnergySamples ) where {N} diff --git a/PRASCore.jl/src/Results/FlowSamples.jl b/PRASCore.jl/src/Results/FlowSamples.jl index ba94f5cc..4c18db6c 100644 --- a/PRASCore.jl/src/Results/FlowSamples.jl +++ b/PRASCore.jl/src/Results/FlowSamples.jl @@ -39,6 +39,9 @@ struct FlowSamplesAccumulator <: ResultAccumulator{FlowSamples} end +sampledata(acc::FlowSamplesAccumulator) = acc.flow +usesamplepartitions(::FlowSamples) = true + function accumulator( sys::SystemModel{N}, nsamples::Int, ::FlowSamples ) where {N} diff --git a/PRASCore.jl/src/Results/GeneratorAvailability.jl b/PRASCore.jl/src/Results/GeneratorAvailability.jl index 1728a7fd..12963c75 100644 --- a/PRASCore.jl/src/Results/GeneratorAvailability.jl +++ b/PRASCore.jl/src/Results/GeneratorAvailability.jl @@ -42,6 +42,9 @@ struct GenAvailabilityAccumulator <: end +sampledata(acc::GenAvailabilityAccumulator) = acc.available +usesamplepartitions(::GeneratorAvailability) = true + function merge!( x::GenAvailabilityAccumulator, y::GenAvailabilityAccumulator ) diff --git a/PRASCore.jl/src/Results/GeneratorStorageAvailability.jl b/PRASCore.jl/src/Results/GeneratorStorageAvailability.jl index ba040873..12634f86 100644 --- a/PRASCore.jl/src/Results/GeneratorStorageAvailability.jl +++ b/PRASCore.jl/src/Results/GeneratorStorageAvailability.jl @@ -31,6 +31,9 @@ struct GenStorAvailabilityAccumulator <: ResultAccumulator{GeneratorStorageAvail end +sampledata(acc::GenStorAvailabilityAccumulator) = acc.available +usesamplepartitions(::GeneratorStorageAvailability) = true + function accumulator( sys::SystemModel{N}, nsamples::Int, ::GeneratorStorageAvailability ) where {N} diff --git a/PRASCore.jl/src/Results/GeneratorStorageEnergySamples.jl b/PRASCore.jl/src/Results/GeneratorStorageEnergySamples.jl index 0273c12e..8a43032c 100644 --- a/PRASCore.jl/src/Results/GeneratorStorageEnergySamples.jl +++ b/PRASCore.jl/src/Results/GeneratorStorageEnergySamples.jl @@ -33,6 +33,9 @@ struct GenStorageEnergySamplesAccumulator <: ResultAccumulator{GeneratorStorageE end +sampledata(acc::GenStorageEnergySamplesAccumulator) = acc.energy +usesamplepartitions(::GeneratorStorageEnergySamples) = true + function accumulator( sys::SystemModel{N}, nsamples::Int, ::GeneratorStorageEnergySamples ) where {N} diff --git a/PRASCore.jl/src/Results/LineAvailability.jl b/PRASCore.jl/src/Results/LineAvailability.jl index f03c9ed9..36833550 100644 --- a/PRASCore.jl/src/Results/LineAvailability.jl +++ b/PRASCore.jl/src/Results/LineAvailability.jl @@ -47,6 +47,9 @@ struct LineAvailabilityAccumulator <: ResultAccumulator{LineAvailability} end +sampledata(acc::LineAvailabilityAccumulator) = acc.available +usesamplepartitions(::LineAvailability) = true + accumulatortype(::LineAvailability) = LineAvailabilityAccumulator function accumulator( diff --git a/PRASCore.jl/src/Results/Results.jl b/PRASCore.jl/src/Results/Results.jl index 9ba3b8ac..059bae38 100644 --- a/PRASCore.jl/src/Results/Results.jl +++ b/PRASCore.jl/src/Results/Results.jl @@ -35,6 +35,8 @@ abstract type ResultSpec end abstract type ResultAccumulator{R<:ResultSpec} end +usesamplepartitions(::ResultSpec) = false + abstract type Result{ N, # Number of timesteps simulated L, # Length of each simulation timestep @@ -194,29 +196,68 @@ include("GeneratorStorageEnergySamples.jl") include("DemandResponseEnergySamples.jl") function resultchannel( - results::T, threads::Int + results::T, nworkers::Int ) where T <: Tuple{Vararg{ResultSpec}} types = accumulatortype.(results) - return Channel{Tuple{types...}}(threads) + return Channel{Tuple{Tuple{types...},UnitRange{Int}}}(nworkers) end -merge!(xs::T, ys::T) where T <: Tuple{Vararg{ResultAccumulator}} = - foreach(merge!, xs, ys) +function copy_sample_partition!( + x::A, + y::A, + sampleids::UnitRange{Int}, +) where {A<:ResultAccumulator} + + xarr = sampledata(x) + yarr = sampledata(y) + + xarr[:, :, sampleids] .= yarr + return +end function finalize( - results::Channel{<:Tuple{Vararg{ResultAccumulator}}}, + results::Channel{Tuple{A,UnitRange{Int}}}, system::SystemModel{N,L,T,P,E}, - threads::Int -) where {N,L,T,P,E} + nworkers::Int, + nsamples::Int, + resultspecs::Tuple{Vararg{ResultSpec}}, +) where {A<:Tuple{Vararg{ResultAccumulator}},N,L,T,P,E} + + first_recorders, first_sampleids = take!(results) - total_result = take!(results) + if nworkers == 1 && first_sampleids == 1:nsamples + close(results) + return finalize.(first_recorders, system) + end - for _ in 2:threads - thread_result = take!(results) - merge!(total_result, thread_result) + total_result = map(resultspecs, first_recorders) do spec, recorder + usesamplepartitions(spec) ? accumulator(system, nsamples, spec) : recorder end + + for i in eachindex(total_result) + if usesamplepartitions(resultspecs[i]) + copy_sample_partition!( + total_result[i], first_recorders[i], first_sampleids + ) + end + end + + for _ in 2:nworkers + thread_recorders, sampleids = take!(results) + + for i in eachindex(total_result) + if usesamplepartitions(resultspecs[i]) + copy_sample_partition!( + total_result[i], thread_recorders[i], sampleids + ) + else + merge!(total_result[i], thread_recorders[i]) + end + end + end + close(results) return finalize.(total_result, system) diff --git a/PRASCore.jl/src/Results/Shortfall.jl b/PRASCore.jl/src/Results/Shortfall.jl index 1ceb76ec..1076f858 100644 --- a/PRASCore.jl/src/Results/Shortfall.jl +++ b/PRASCore.jl/src/Results/Shortfall.jl @@ -108,7 +108,7 @@ function accumulator( end -function merge!( +function merge_shortfall_statistics!( x::ShortfallAccumulator, y::ShortfallAccumulator ) @@ -122,6 +122,17 @@ function merge!( foreach(merge!, x.unservedload_period, y.unservedload_period) foreach(merge!, x.unservedload_regionperiod, y.unservedload_regionperiod) + return + +end + + +function merge!( + x::ShortfallAccumulator, y::ShortfallAccumulator +) + + merge_shortfall_statistics!(x, y) + x.unservedload_sample .+= y.unservedload_sample x.unservedload_region_sample .+= y.unservedload_region_sample @@ -129,6 +140,26 @@ function merge!( end + +function copy_sample_partition!( + x::ShortfallAccumulator, + y::ShortfallAccumulator, + sampleids::UnitRange{Int}, +) + + merge_shortfall_statistics!(x, y) + + x.unservedload_sample[sampleids] .= y.unservedload_sample + x.unservedload_region_sample[:, sampleids] .= + y.unservedload_region_sample + + return + +end + +usesamplepartitions(::Shortfall) = true +usesamplepartitions(::DemandResponseShortfall) = true + accumulatortype(::S) where { S<:Union{Shortfall,DemandResponseShortfall} } = ShortfallAccumulator{S} diff --git a/PRASCore.jl/src/Results/ShortfallSamples.jl b/PRASCore.jl/src/Results/ShortfallSamples.jl index 11f0f7d6..ac8c4650 100644 --- a/PRASCore.jl/src/Results/ShortfallSamples.jl +++ b/PRASCore.jl/src/Results/ShortfallSamples.jl @@ -57,6 +57,10 @@ struct ShortfallSamplesAccumulator{S} <: ResultAccumulator{ShortfallSamples} end +sampledata(acc::ShortfallSamplesAccumulator) = acc.shortfall +usesamplepartitions(::ShortfallSamples) = true +usesamplepartitions(::DemandResponseShortfallSamples) = true + function accumulator( sys::SystemModel{N}, nsamples::Int, ::S ) where {N,S<:Union{ShortfallSamples,DemandResponseShortfallSamples}} diff --git a/PRASCore.jl/src/Results/StorageAvailability.jl b/PRASCore.jl/src/Results/StorageAvailability.jl index 46b5aa27..dddb0827 100644 --- a/PRASCore.jl/src/Results/StorageAvailability.jl +++ b/PRASCore.jl/src/Results/StorageAvailability.jl @@ -30,6 +30,9 @@ struct StorAvailabilityAccumulator <: ResultAccumulator{StorageAvailability} end +sampledata(acc::StorAvailabilityAccumulator) = acc.available +usesamplepartitions(::StorageAvailability) = true + function accumulator( sys::SystemModel{N}, nsamples::Int, ::StorageAvailability ) where {N} diff --git a/PRASCore.jl/src/Results/StorageEnergySamples.jl b/PRASCore.jl/src/Results/StorageEnergySamples.jl index 688f5ef6..f527610a 100644 --- a/PRASCore.jl/src/Results/StorageEnergySamples.jl +++ b/PRASCore.jl/src/Results/StorageEnergySamples.jl @@ -32,6 +32,9 @@ struct StorageEnergySamplesAccumulator <: ResultAccumulator{StorageEnergySamples end +sampledata(acc::StorageEnergySamplesAccumulator) = acc.energy +usesamplepartitions(::StorageEnergySamples) = true + function accumulator( sys::SystemModel{N}, nsamples::Int, ::StorageEnergySamples ) where {N} diff --git a/PRASCore.jl/src/Results/SurplusSamples.jl b/PRASCore.jl/src/Results/SurplusSamples.jl index 2d5b02f1..84f5dde4 100644 --- a/PRASCore.jl/src/Results/SurplusSamples.jl +++ b/PRASCore.jl/src/Results/SurplusSamples.jl @@ -32,6 +32,9 @@ struct SurplusSamplesAccumulator <: ResultAccumulator{SurplusSamples} end +sampledata(acc::SurplusSamplesAccumulator) = acc.surplus +usesamplepartitions(::SurplusSamples) = true + function accumulator( sys::SystemModel{N}, nsamples::Int, ::SurplusSamples ) where {N} diff --git a/PRASCore.jl/src/Results/UtilizationSamples.jl b/PRASCore.jl/src/Results/UtilizationSamples.jl index b56410dc..5b57d1f4 100644 --- a/PRASCore.jl/src/Results/UtilizationSamples.jl +++ b/PRASCore.jl/src/Results/UtilizationSamples.jl @@ -49,6 +49,9 @@ struct UtilizationSamplesAccumulator <: ResultAccumulator{UtilizationSamples} end +sampledata(acc::UtilizationSamplesAccumulator) = acc.utilization +usesamplepartitions(::UtilizationSamples) = true + function accumulator( sys::SystemModel{N}, nsamples::Int, ::UtilizationSamples ) where {N} diff --git a/PRASCore.jl/src/Simulations/Simulations.jl b/PRASCore.jl/src/Simulations/Simulations.jl index 8849b28f..cd4e31f7 100644 --- a/PRASCore.jl/src/Simulations/Simulations.jl +++ b/PRASCore.jl/src/Simulations/Simulations.jl @@ -5,7 +5,7 @@ import ..Systems: SystemModel, AbstractAssets, Generators, Lines, import ..Results import ..Results: ResultSpec, ResultAccumulator, - accumulator, resultchannel, finalize + accumulator, finalize, resultchannel, usesamplepartitions import Base: broadcastable import Base.Threads: nthreads, @spawn @@ -40,7 +40,11 @@ It it recommended that you fix the random seed for reproducibility. - `samples::Int=10_000`: Number of samples - `seed::Integer=rand(UInt64)`: Random seed - `verbose::Bool=false`: Print progress - - `threaded::Bool=true`: Use multi-threading + - `threaded::Bool=true`: Enable threaded Monte Carlo simulation + +When `threaded=true`, sample-level result specifications are internally +partitioned across threads during simulation and assembled into dense +result arrays during finalization. # Returns @@ -65,6 +69,40 @@ end broadcastable(x::SequentialMonteCarlo) = Ref(x) +function sample_ranges(nsamples::Int, nworkers::Int) + base, rem = divrem(nsamples, nworkers) + + ranges = UnitRange{Int}[] + start = 1 + + for i in 1:nworkers + len = base + (i <= rem ? 1 : 0) + + if len > 0 + stop = start + len - 1 + push!(ranges, start:stop) + start = stop + 1 + end + end + + return ranges +end + +function partition_recorders( + system::SystemModel, + nsamples::Int, + local_nsamples::Int, + resultspecs::Tuple{Vararg{ResultSpec}}, +) + return map(resultspecs) do spec + accumulator( + system, + usesamplepartitions(spec) ? local_nsamples : nsamples, + spec, + ) + end +end + """ assess(system::SystemModel, method::SequentialMonteCarlo, resultspecs::ResultSpec...) @@ -79,7 +117,7 @@ and return `resultspecs`. # Returns - - `results::Tuple{Vararg{ResultAccumulator{SequentialMonteCarlo}}}`: PRAS metric results + - `results::Tuple`: PRAS metric results """ function assess( system::SystemModel, @@ -87,75 +125,81 @@ function assess( resultspecs::ResultSpec... ) - threads = nthreads() - sampleseeds = Channel{Int}(2*threads) - results = resultchannel(resultspecs, threads) - - @spawn makeseeds(sampleseeds, method.nsamples) + threads = method.threaded ? nthreads() : 1 - if method.threaded - - if (threads == 1) - @warn "It looks like you haven't configured JULIA_NUM_THREADS before you started the julia repl. \n If you want to use multi-threading, stop the execution and start your julia repl using : \n julia --project --threads auto" - end - - for _ in 1:threads - @spawn assess(system, method, sampleseeds, results, resultspecs...) - end - else - assess(system, method, sampleseeds, results, resultspecs...) + if method.threaded && threads == 1 + @warn "It looks like you haven't configured JULIA_NUM_THREADS before you started the julia repl. \n If you want to use multi-threading, stop the execution and start your julia repl using : \n julia --project --threads auto" end - return finalize(results, system, method.threaded ? threads : 1) + ranges = sample_ranges(method.nsamples, threads) + nworkers = length(ranges) -end + results = resultchannel(resultspecs, nworkers) -function makeseeds(sampleseeds::Channel{Int}, nsamples::Int) - - for s in 1:nsamples - put!(sampleseeds, s) + for sampleids in ranges + if method.threaded + @spawn assess(system, method, sampleids, results, resultspecs...) + else + assess(system, method, sampleids, results, resultspecs...) + end end - close(sampleseeds) + return finalize(results, system, nworkers, method.nsamples, resultspecs) end function assess( - system::SystemModel{N}, method::SequentialMonteCarlo, - sampleseeds::Channel{Int}, - results::Channel{<:Tuple{Vararg{ResultAccumulator}}}, + system::SystemModel{N}, + method::SequentialMonteCarlo, + sampleids::UnitRange{Int}, + results::Channel{Tuple{A,UnitRange{Int}}}, resultspecs::ResultSpec... -) where N +) where {A<:Tuple{Vararg{ResultAccumulator}},N} dispatchproblem = DispatchProblem(system) systemstate = SystemState(system) - recorders = accumulator.(system, method.nsamples, resultspecs) + + recorders = partition_recorders( + system, + method.nsamples, + length(sampleids), + resultspecs, + ) # TODO: Test performance of Philox vs Threefry, choice of rounds # Also consider implementing an efficient Bernoulli trial with direct # mantissa comparison rng = Philox4x((0, 0), 10) - for s in sampleseeds + for (local_sampleid, global_sampleid) in enumerate(sampleids) - seed!(rng, (method.seed, s)) + seed!(rng, (method.seed, global_sampleid)) initialize!(rng, systemstate, system) for t in 1:N - advance!(rng, systemstate, dispatchproblem, system, t) solve!(dispatchproblem, systemstate, system, t) - foreach(recorder -> record!( - recorder, system, systemstate, dispatchproblem, s, t - ), recorders) + foreach(recorders) do recorder + record!( + recorder, + system, + systemstate, + dispatchproblem, + local_sampleid, + t, + ) + end end - foreach(recorder -> reset!(recorder, s), recorders) - + foreach(recorders) do recorder + reset!(recorder, local_sampleid) + end end - put!(results, recorders) + put!(results, (recorders, sampleids)) + + return end diff --git a/PRASCore.jl/test/Simulations/runtests.jl b/PRASCore.jl/test/Simulations/runtests.jl index ae44b0ce..67c55ab6 100644 --- a/PRASCore.jl/test/Simulations/runtests.jl +++ b/PRASCore.jl/test/Simulations/runtests.jl @@ -601,12 +601,12 @@ @testset "Whole-horizon equals sum over days" begin for x in (shortfall2_1a, shortfall2_1a5, shortfall2_1b, shortfall2_3) days = unique(Date.(x.timestamps)) - + @test isapprox( val(LOLD(x)), sum(val(LOLD(x, d)) for d in days) ) - + for r in x.regions.names @test isapprox( val(LOLD(x, r)), @@ -615,11 +615,11 @@ end end end - + @testset "Single-day query matches direct sample calculation" begin for x in (shortfall2_1a, shortfall2_1a5, shortfall2_1b, shortfall2_3) days = unique(Date.(x.timestamps)) - + # test first, middle, and last day testdays = unique([first(days), days[cld(length(days), 2)], last(days)]) @@ -738,5 +738,174 @@ end + @testset "Threaded sample result partitioning" begin + simspec_serial = + SequentialMonteCarlo(samples=100, seed=123, threaded=false) + simspec_threaded = + SequentialMonteCarlo(samples=100, seed=123, threaded=true) + + @testset "Shortfall" begin + serial_shortfall, serial_samples = + assess(TestData.singlenode_a, simspec_serial, + Shortfall(), ShortfallSamples()) + + threaded_shortfall, threaded_samples = + assess(TestData.singlenode_a, simspec_threaded, + Shortfall(), ShortfallSamples()) + + region = first(TestData.singlenode_a.regions.names) + + @test threaded_samples.shortfall == serial_samples.shortfall + @test threaded_shortfall.shortfall_samples == + serial_shortfall.shortfall_samples + @test threaded_shortfall.shortfall_region_samples == + serial_shortfall.shortfall_region_samples + @test threaded_shortfall.shortfall_samples == threaded_samples[] + @test threaded_shortfall.shortfall_region_samples[1, :] == + threaded_samples[region] + + serial_cvar = CVAR(:energy, serial_shortfall, alpha) + threaded_cvar = CVAR(:energy, threaded_shortfall, alpha) + serial_region_cvar = + CVAR(:energy, serial_shortfall, alpha, region) + threaded_region_cvar = + CVAR(:energy, threaded_shortfall, alpha, region) + + @test threaded_cvar ≈ serial_cvar + @test threaded_region_cvar ≈ serial_region_cvar + + @test LOLE(threaded_shortfall) ≈ LOLE(serial_shortfall) + @test EUE(threaded_shortfall) ≈ EUE(serial_shortfall) + @test NEUE(threaded_shortfall) ≈ NEUE(serial_shortfall) + + @test LOLE(threaded_shortfall) ≈ LOLE(threaded_samples) + @test EUE(threaded_shortfall) ≈ EUE(threaded_samples) + @test NEUE(threaded_shortfall) ≈ NEUE(threaded_samples) + end + + @testset "Demand response shortfall" begin + serial_shortfall, serial_samples = + assess(TestData.test4, simspec_serial, + DemandResponseShortfall(), + DemandResponseShortfallSamples()) + + threaded_shortfall, threaded_samples = + assess(TestData.test4, simspec_threaded, + DemandResponseShortfall(), + DemandResponseShortfallSamples()) + + region = first(TestData.test4.regions.names) + + @test threaded_samples.shortfall == serial_samples.shortfall + @test threaded_shortfall.shortfall_samples == + serial_shortfall.shortfall_samples + @test threaded_shortfall.shortfall_region_samples == + serial_shortfall.shortfall_region_samples + @test threaded_shortfall.shortfall_samples == threaded_samples[] + @test threaded_shortfall.shortfall_region_samples[1, :] == + threaded_samples[region] + + serial_cvar = CVAR(:energy, serial_shortfall, alpha) + threaded_cvar = CVAR(:energy, threaded_shortfall, alpha) + + @test threaded_cvar ≈ serial_cvar + @test LOLE(threaded_shortfall) ≈ LOLE(serial_shortfall) + @test EUE(threaded_shortfall) ≈ EUE(serial_shortfall) + @test NEUE(threaded_shortfall) ≈ NEUE(serial_shortfall) + + @test LOLE(threaded_shortfall) ≈ LOLE(threaded_samples) + @test EUE(threaded_shortfall) ≈ EUE(threaded_samples) + @test NEUE(threaded_shortfall) ≈ NEUE(threaded_samples) + end + + @testset "Transmission sample assembly" begin + serial_shortfall, serial_samples = + assess(TestData.threenode, simspec_serial, + Shortfall(), ShortfallSamples()) + threaded_shortfall, threaded_samples = + assess(TestData.threenode, simspec_threaded, + Shortfall(), ShortfallSamples()) + + @test threaded_shortfall.shortfall_samples == + serial_shortfall.shortfall_samples + @test threaded_samples[] == serial_samples[] + + @test threaded_shortfall.shortfall_samples == threaded_samples[] + + # Regional dispatch may differ between equivalent optimal solutions. + for (i, region) in enumerate(regionscol) + @test threaded_shortfall.shortfall_region_samples[i, :] == + threaded_samples[region] + end + end + + @testset "Uneven sample partition" begin + simspec_few_samples_serial = + SequentialMonteCarlo(samples=5, seed=123, threaded=false) + simspec_few_samples_threaded = + SequentialMonteCarlo(samples=5, seed=123, threaded=true) + + serial_result, = assess( + TestData.singlenode_a, + simspec_few_samples_serial, + ShortfallSamples(), + ) + threaded_result, = assess( + TestData.singlenode_a, + simspec_few_samples_threaded, + ShortfallSamples(), + ) + + @test threaded_result.shortfall == serial_result.shortfall + end + + @testset "Sample accumulator partition copying" begin + sampleids = 2:3 + accumulator_cases = ( + (ShortfallSamples(), TestData.threenode), + (DemandResponseShortfallSamples(), TestData.threenode_dr), + (SurplusSamples(), TestData.threenode), + (FlowSamples(), TestData.threenode), + (UtilizationSamples(), TestData.threenode), + (StorageEnergySamples(), TestData.singlenode_stor), + (GeneratorStorageEnergySamples(), TestData.singlenode_stor), + (DemandResponseEnergySamples(), TestData.threenode_dr), + (GeneratorAvailability(), TestData.threenode), + (StorageAvailability(), TestData.singlenode_stor), + (GeneratorStorageAvailability(), TestData.singlenode_stor), + (DemandResponseAvailability(), TestData.threenode_dr), + (LineAvailability(), TestData.threenode), + ) + + for (spec, system) in accumulator_cases + @testset "$(nameof(typeof(spec)))" begin + destination = + PRASCore.Results.accumulator(system, 4, spec) + source, = PRASCore.Simulations.partition_recorders( + system, + 4, + length(sampleids), + (spec,), + ) + destination_data = PRASCore.Results.sampledata(destination) + source_data = PRASCore.Results.sampledata(source) + + @test size(source_data, 3) == length(sampleids) + + fill!(source_data, one(eltype(source_data))) + expected = zeros( + eltype(destination_data), size(destination_data) + ) + expected[:, :, sampleids] .= source_data + + PRASCore.Results.copy_sample_partition!( + destination, source, sampleids + ) + + @test destination_data == expected + end + end + end + end end diff --git a/docs/src/extending.md b/docs/src/extending.md index 31520f44..c0ad07d6 100644 --- a/docs/src/extending.md +++ b/docs/src/extending.md @@ -202,17 +202,21 @@ samples are performed. ```julia # Define the accumulator structure -struct SMCMyResultAccumulator <: ResultAccumulator{SequentialMonteCarlo,MyResultSpec} +struct SMCMyResultAccumulator <: + PRASCore.Results.ResultAccumulator{MyResultSpec} # fields for holding intermediate data go here end # Help PRAS know which accumulator type to expect before one's created -PRAS.ResourceAdequacy.accumulatortype(::SequentialMonteCarlo, ::MyResultSpec) = +PRASCore.Results.accumulatortype(::MyResultSpec) = SMCMyResultAccumulator # Initialize a new accumulator -function PRAS.ResourceAdequacy.accumulator( - sys::SystemModel, simspec::SequentialMonteCarlo, resultspec::MyResultSpec) +function PRASCore.Results.accumulator( + sys::SystemModel, + nsamples::Int, + ::MyResultSpec, +) return SMCMyResultAccumulator(...) end ``` @@ -227,9 +231,10 @@ current `state` and the solution to the period's dispatch problem in-place. ```julia -PRAS.ResourceAdequacy.record!( - acc::SMCMyResultAccumulator, sys::SystemModel, state::SystemState, - prob::DispatchProblem, s::Int, t::Int) +PRASCore.Simulations.record!( + acc::SMCMyResultAccumulator, sys::SystemModel, + state::PRASCore.Simulations.SystemState, + prob::PRASCore.Simulations.DispatchProblem, s::Int, t::Int) ``` #### reset! @@ -241,31 +246,56 @@ and prepare the accumulator to start receiving values from a new chronological simulation sequence. ```julia -PRAS.ResourceAdequacy.reset!(acc::SMCMyResultAccumulator, s::Int) +PRASCore.Simulations.reset!(acc::SMCMyResultAccumulator, s::Int) # Often no action is required here, # so a simple one-line implementation is possible -PRAS.ResourceAdequacy.reset!(acc::SMCMyResultAccumulator, s::Int) = nothing +PRASCore.Simulations.reset!(acc::SMCMyResultAccumulator, s::Int) = nothing ``` #### merge! -For multithreaded assessments PRAS creates one accumulator per worker thread (parallel task) and merges each thread's accumulator information togther once work is completed. `merge!` defines how an accumulator `a` should be updated in-place to incorporate the results obtained by another accumulator `b`. +For multithreaded assessments PRAS creates one accumulator per worker task. +For result specifications that do not use sample partitions, `merge!` defines how an accumulator `a` should be updated in-place to incorporate the results obtained by another accumulator `b`. ```julia -PRAS.ResourceAdequacy.merge!( +Base.merge!( a::SMCMyResultAccumulator, b::SMCMyResultAccumulator) ``` -#### finalize! +#### finalize -Once all of the thread accumulators have been merged down to a single accumulator reflecting results from all of the threads, this final accumulator `acc` is mapped to the final result output through a `finalize` method. +Once the worker accumulators have been combined into a single accumulator, this final accumulator `acc` is mapped to the result output through a `finalize` method. ```julia -function PRAS.ResourceAdequacy.finalize( +function PRASCore.Results.finalize( acc::SMCMyResultAccumulator, sys::SystemModel) return MyResult(...) end ``` + +#### sample partitioning + +For multithreaded assessments, a result specification that stores sample-indexed values should indicate that its accumulator can be partitioned by sample. +PRAS will then allocate each worker accumulator for only its assigned samples and assemble the complete sample axis during finalization. + +For an accumulator containing one three-dimensional array with samples on the final axis, define the following methods alongside the result specification: + +```julia +PRASCore.Results.usesamplepartitions(::MyResultSpec) = true +PRASCore.Results.sampledata(acc::SMCMyResultAccumulator) = acc.samples +``` + +During threaded execution the `nsamples` argument passed to `accumulator` and the sample index passed to `record!` refer to the worker's local sample partition. +The generic partition copier places each worker's `sampledata` in the corresponding sample range of the final accumulator. + +For accumulators with multiple sample arrays, non-three-dimensional arrays or additional statistics, define a specialized `copy_sample_partition!` method instead of `sampledata`. +`ShortfallAccumulator` is an example of this case because `Shortfall()` stores per-sample system and regional totals for CVaR together with aggregate statistics. + +```julia +PRASCore.Results.copy_sample_partition!( + a::SMCMyResultAccumulator, b::SMCMyResultAccumulator, + sampleids::UnitRange{Int}) +```