diff --git a/src/QEDprocesses.jl b/src/QEDprocesses.jl index e9184bd..aa52614 100644 --- a/src/QEDprocesses.jl +++ b/src/QEDprocesses.jl @@ -13,6 +13,10 @@ export initial_phasespace_dimension, final_phasespace_dimension export number_incoming_particles, number_outgoing_particles export differential_cross_section, total_cross_section +# Abstract setup interface +export AbstractComputationSetup, InvalidInputError, compute +export AbstractProcessSetup, scattering_process, physical_model + # particle types export AbstractParticleType export FermionLike, Fermion, AntiFermion, MajoranaFermion @@ -26,5 +30,6 @@ include("utils.jl") include("interfaces/particle_interface.jl") include("interfaces/model_interface.jl") include("interfaces/process_interface.jl") +include("interfaces/setup_interface.jl") include("particle_types.jl") end diff --git a/src/interfaces/model_interface.jl b/src/interfaces/model_interface.jl index a99b3f4..ce8fb5b 100644 --- a/src/interfaces/model_interface.jl +++ b/src/interfaces/model_interface.jl @@ -3,10 +3,6 @@ # # In this file, we define the interface of working with compute models in # general. -# -# This file is part of `QEDprocesses.jl` which is by itself part of the `QED.jl` -# ecosystem. -# ############### # root type for models """ diff --git a/src/interfaces/particle_interface.jl b/src/interfaces/particle_interface.jl index 624e684..1f95ef7 100644 --- a/src/interfaces/particle_interface.jl +++ b/src/interfaces/particle_interface.jl @@ -3,10 +3,6 @@ # # In this file, we define the interface for working with particles in a general # sense. -# -# This file is part of `QEDprocesses.jl` which is by itself part of the `QED.jl` -# ecosystem. -# ############### diff --git a/src/interfaces/process_interface.jl b/src/interfaces/process_interface.jl index 678f451..feff43e 100644 --- a/src/interfaces/process_interface.jl +++ b/src/interfaces/process_interface.jl @@ -3,10 +3,6 @@ # # In this file, we define the interface for working with scattering processes in # general. -# -# This file is part of `QEDprocesses.jl` which is by itself part of the `QED.jl` -# ecosystem. -# ############### """ @@ -47,7 +43,7 @@ function outgoing_particles end Return the number of incoming particles of a given process. """ -@inline function number_incoming_pariticles(proc_def::AbstractProcessDefinition) +@inline function number_incoming_particles(proc_def::AbstractProcessDefinition) return length(incoming_particles(proc_def)) end @@ -57,7 +53,7 @@ end Return the number of outgoing particles of a given process. """ -@inline function number_outgoing_pariticles(proc_def::AbstractProcessDefinition) +@inline function number_outgoing_particles(proc_def::AbstractProcessDefinition) return length(outgoing_particles(proc_def)) end @@ -70,8 +66,8 @@ end final_phasespace::AbstractVector{T}, ) where {T<:QEDbase.AbstractFourMomentum} -Interface function for the combination of scattering processes and models. Return the differential cross section of a -given process and model for a passed initial and final phase space. The elements of the `AbstractVector` representing the phase spaces +Interface function for the combination of scattering processes and physical models. Return the differential cross section of a +given process and physical model for a passed initial and final phase space. The elements of the `AbstractVector` representing the phase spaces are the momenta of the respective particles. The implementation of this function for a concrete process and model must not check if the length of the passed phase spaces match the respective number of particles. @@ -120,14 +116,14 @@ function differential_cross_section( init_phasespace::Union{AbstractVector{T},AbstractMatrix{T}}, final_phasespace::Union{AbstractVector{T},AbstractMatrix{T}}, ) where {T<:QEDbase.AbstractFourMomentum} - size(init_phasespace, 1) == number_incoming_pariticles(proc_def) || throw( + size(init_phasespace, 1) == number_incoming_particles(proc_def) || throw( DimensionMismatch( - "The number of momenta in the initial phasespace <{length(init_phasespace)}> does not match the number of incoming particles of the process <{number_incoming_pariticles(proc_def)}>.", + "The number of momenta in the initial phasespace <{length(init_phasespace)}> does not match the number of incoming particles of the process <{number_incoming_particles(proc_def)}>.", ), ) - size(final_phasespace, 1) == number_outgoing_pariticles(proc_def) || throw( + size(final_phasespace, 1) == number_outgoing_particles(proc_def) || throw( DimensionMismatch( - "The number of momenta in the final phasespace <{length(final_phasespace)}> does not match the number of outgoing particles of the process <{number_outgoing_pariticles(proc_def)}>.", + "The number of momenta in the final phasespace <{length(final_phasespace)}> does not match the number of outgoing particles of the process <{number_outgoing_particles(proc_def)}>.", ), ) return _differential_cross_section( @@ -207,7 +203,7 @@ end init_phasespace::AbstractVector{T}, ) where {T<:QEDbase.AbstractFourMomentum} end -Interface function for the combination of scattering processes and models. Return the total cross section of a +Interface function for the combination of scattering processes and physical models. Return the total cross section of a given process and model for a passed initial phase space. The elements of the `AbstractVector` representing the initial phase space are the momenta of the respective particles. The implementation of this function for a concrete process and model must not check if the length of the passed initial phase spaces match number of incoming particles. @@ -255,7 +251,7 @@ end init_phasespace::Union{AbstractVector{T},AbstractMatrix{T}}, ) where {T<:QEDbase.AbstractFourMomentum} -Return the total cross section for a combination of a scattering process and a compute model evaluated on a given initial phase space. +Return the total cross section for a combination of a scattering process and a physical model evaluated on a given initial phase space. This function will eventually call the respective interface function [`_total_cross_section`](@ref). @@ -265,9 +261,9 @@ function total_cross_section( model_def::AbstractModelDefinition, init_phasespace::Union{AbstractVector{T},AbstractMatrix{T}}, ) where {T<:QEDbase.AbstractFourMomentum} - size(init_phasespace, 1) == number_incoming_pariticles(proc_def) || throw( + size(init_phasespace, 1) == number_incoming_particles(proc_def) || throw( DimensionMismatch( - "The number of momenta in the initial phasespace <{length(init_phasespace)}> does not match the number of incoming particles of the process <{number_incoming_pariticles(proc_def)}>.", + "The number of momenta in the initial phasespace <{length(init_phasespace)}> does not match the number of incoming particles of the process <{number_incoming_particles(proc_def)}>.", ), ) return _total_cross_section(proc_def, model_def, init_phasespace) diff --git a/src/interfaces/setup_interface.jl b/src/interfaces/setup_interface.jl new file mode 100644 index 0000000..19e8122 --- /dev/null +++ b/src/interfaces/setup_interface.jl @@ -0,0 +1,194 @@ +############### +# The process setup +# +# In this file, we define the interface for general computation and process setups. +############### + +""" +Abstract base type for computation setups. A *setup* means +a collection of setup data needed to evaluate a dedicated quantity of given +running data. Therefore, each setup is associated with a single quantity, which one may compute using the setup data and the running data. +Despite that, the decomposition into setup and running data is +arbitrary, and this can be used for cases where a subset of the variables a +quantity depends on is kept constant. + +!!! note "Computation setup interface" + + The computation performed using a computation setup is separated into three steps: + + 1. input validation + 2. actual computation + 3. post processing + + where every step has its own interface function (see [`compute`](@ref) for details). + + ## Input validation + + Every subtype of `AbstractComputationSetup` should implement the interface function + + ```Julia + _assert_valid_input(stp::AbstractComputationSetup, input) + ``` + + which should throw and an exception subtyped from [`AbstractInvalidInputException`](@ref) if the `input` is not valid for the computation of the associated quantity (see [`_assert_valid_input`](@ref) for more details). + The default implementation does nothing, i.e. every input is valid by default. Provide a custom implementation if a different behavior is required. + + ## Actual computation + + Every subtype of `AbstractComputationSetup` must at least implement the required interface function + + ```Julia + _compute(stp::AbstractComputationSetup, input) + ``` + + which computes the value of the associated quantity for a given `input` (see [`_compute`](@ref) for more details). + + + ## Post processing + + Every subtype of `AbstractComputationSetup` should implement the interface function + + ```Julia + _post_processing(stp::AbstractComputationSetup, input, result) + ``` + + which performs task after the actual computation, e.g. conversions or normalizations (see [`_post_processing`](@ref) for more details). + +""" +abstract type AbstractComputationSetup end + +# convenience function to check if an object is a computation setup +_is_computation_setup(::AbstractComputationSetup) = true + +""" +Abstract base type for exceptions indicating invalid input. See [`InvalidInputError`](@ref) for a simple concrete implementation. +Concrete implementations should at least implement + +```Julia + +Base.showerror(io::IO, err::CustomInvalidError) where {CustomInvalidError<:AbstractInvalidInputException} + +``` +""" +abstract type AbstractInvalidInputException <: Exception end + +""" + + InvalidInputError(msg::String) + +Exception which is thrown if a given input is invalid, e.g. passed to [`_assert_valid_input`](@ref). +""" +struct InvalidInputError <: AbstractInvalidInputException + msg::String +end +Base.showerror(io::IO, err::InvalidInputError) = + println(io, "InvalidInputError: $(err.msg).") + +""" + + _assert_valid_input(stp::AbstractComputationSetup, input::Any) + +Interface function, which asserts that the given `input` is valid, and throws an [`InvalidInputError`](@ref) if not. + +!!! note "default implementation" + + By default, every input is assumed to be valid. Therefore, this function does nothing. + To customize this behavior, add your own implementation of + + ```Julia + _assert_valid_input(stp::YourCustomSetup,input) + ``` + which should throw an exception, which is a subtype of [`AbstractInvalidInputError`](@ref). One may also use the concrete implementation [`InvalidInputError`](@ref) if the input is invalid instead of writing a custom exception type. + +""" +@inline function _assert_valid_input(stp::AbstractComputationSetup, input) + return nothing +end + +""" + + function _post_processing(stp::AbstractComputationSetup, input::Any, result::Any) + +Interface function, which is called in [`compute`](@ref) after [`_compute`](@ref) has been called. This function is dedicated to +finalize the result of a computation. + +!!! note "default implementation" + + Since in the case of no post processing the result of [`_compute`](@ref) is unchanged, this function returns `result` by default. + +""" +@inline function _post_processing(stp::AbstractComputationSetup, input, result) + return result +end + +""" + + _compute(stp::AbstractComputationSetup, input::Any) + +Interface function that returns the value of the associated quantity evaluated on `input`, which can be anything the associated quantity is defined to be feasible for. + +!!! note "unsafe implementation" + + This function must be implemented for any subtype of [`AbstractComputationSetup`](@ref). It should not do any input validation or post processing (see [`_assert_valid_input`](@ref) and [`_post_processing`](@ref)), as those two are performed while calling + the safe version of this function [`compute`](@ref). + +""" +function _compute end + +""" + + compute(stp::AbstractComputationSetup, input::Any) + +Return the value of the quantity associated with `stp` for a given `input`. +In addition to the actual call of the associated unsafe version [`_compute`](@ref), +input validation ([`_assert_valid_input`]) and post processing +(using [`_post_processing`](@ref)) are wrapped around the calculation (see [`AbstractComputationSetup`](@ref) for details). +""" +function compute(stp::AbstractComputationSetup, input) + _assert_valid_input(stp, input) + raw_result = _compute(stp, input) + return _post_processing(stp, input, raw_result) +end + +""" +Abstract base type for setups related to combining scattering processes and physical models. +Every subtype of `AbstractProcessSetup` must implement at least the following +interface functions: + +```Julia +scattering_process(::AbstractProcessSetup) +physical_model(::AbstractProcessSetup) +``` + +Derived from these interface functions, the following delegations are provided: + +```Julia +number_incoming_particles(::AbstractProcessSetup) +number_outgoing_particles(::AbstractProcessSetup) +``` + +""" +abstract type AbstractProcessSetup <: AbstractComputationSetup end + +""" + + scattering_process(stp::AbstractProcessSetup) + +Interface function that returns the scattering process associated with `stp`, +i.e. an object which is a subtype of [`AbstractProcessDefinition`](@ref). +""" +function scattering_process end + +""" + + physical_model(stp::AbstractProcessSetup) + +Interface function that returns the physical model associated with `stp`, i.e. +an object which is a subtype of [`AbstractModelDefinition`](@ref). +""" +function physical_model end + +@inline number_incoming_particles(stp::AbstractProcessSetup) = + number_incoming_particles(scattering_process(stp)) +@inline number_outgoing_particles(stp::AbstractProcessSetup) = + number_outgoing_particles(scattering_process(stp)) diff --git a/src/particle_types.jl b/src/particle_types.jl index c6f1d35..10580ef 100644 --- a/src/particle_types.jl +++ b/src/particle_types.jl @@ -3,9 +3,6 @@ # # In this file, we define the types of particles used in `QEDprocesses.jl` and # implement the abstact particle interface accordingly. -# -# This file is part of `QEDprocesses.jl` which is by itself part of the `QED.jl` -# ecosystem. ############### """ diff --git a/src/utils.jl b/src/utils.jl index b5e31eb..dde31e3 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -2,9 +2,6 @@ # utility functions # # This file contains small helper and utility functions used throughout the package. -# -# This file is part of `QEDprocesses.jl` which is by itself part of the `QED.jl` -# ecosystem. ############### diff --git a/test/interfaces/process_interface.jl b/test/interfaces/process_interface.jl index b540907..8440b65 100644 --- a/test/interfaces/process_interface.jl +++ b/test/interfaces/process_interface.jl @@ -77,6 +77,11 @@ end @test outgoing_particles(TestProcess()) == OUTGOING_PARTICLES end + @testset "delegated functions" begin + @test number_incoming_particles(TestProcess()) == N_INCOMING + @test number_outgoing_particles(TestProcess()) == N_OUTGOING + end + @testset "cross section" begin diff --git a/test/interfaces/setup_interface.jl b/test/interfaces/setup_interface.jl new file mode 100644 index 0000000..c7ed6a4 --- /dev/null +++ b/test/interfaces/setup_interface.jl @@ -0,0 +1,195 @@ + +using Random +using QEDbase +using QEDprocesses + +RNG = MersenneTwister(137137) +ATOL = 0.0 +RTOL = sqrt(eps()) + +_groundtruth_compute(x) = x +_groundtruth_input_validation(x) = (x > 0) +struct TestException <: QEDprocesses.AbstractInvalidInputException end +function _groundtruth_valid_input_assert(x) + _groundtruth_input_validation(x) || throw(TestException()) + nothing +end +_transform_to_invalid(x) = -abs(x) +_groundtruth_post_processing(x, y) = x + y + +# setups for which the interface is implemented +abstract type AbstractTestSetup <: AbstractComputationSetup end +QEDprocesses._compute(stp::AbstractTestSetup, x) = _groundtruth_compute(x) + +# setup with default implementations +struct TestSetupDefault <: AbstractTestSetup end + +# setup with custom _assert_valid_input +struct TestSetupCustomAssertValidInput <: AbstractTestSetup end +QEDprocesses._assert_valid_input(stp::TestSetupCustomAssertValidInput, x) = + _groundtruth_valid_input_assert(x) + +# setup with custom post processing +struct TestSetupCustomPostProcessing <: AbstractTestSetup end +QEDprocesses._post_processing(::TestSetupCustomPostProcessing, x, y) = + _groundtruth_post_processing(x, y) + +# setup with custom input validation and post processing +struct TestSetupCustom <: AbstractTestSetup end +QEDprocesses._assert_valid_input(stp::TestSetupCustom, x) = + _groundtruth_valid_input_assert(x) +QEDprocesses._post_processing(::TestSetupCustom, x, y) = _groundtruth_post_processing(x, y) + +# setup which fail on computation with default implementations +struct TestSetupFAIL <: AbstractComputationSetup end + +# setup which fail on computation with custom input validation, where the +# invalid input will be caught before the computation. +struct TestSetupCustomValidationFAIL <: AbstractComputationSetup end +QEDprocesses._assert_valid_input(stp::TestSetupCustomValidationFAIL, x) = + _groundtruth_valid_input_assert(x) + +# setup which fail on computation with custom post processing +struct TestSetupCustomPostProcessingFAIL <: AbstractComputationSetup end +QEDprocesses._post_processing(::TestSetupCustomPostProcessingFAIL, x, y) = + _groundtruth_post_processing(x, y) +@testset "general computation setup interface" begin + @testset "interface fail" begin + rnd_input = rand(RNG) + + @test_throws MethodError QEDprocesses._compute(TestSetupFAIL(), rnd_input) + @test_throws MethodError compute(TestSetupFAIL(), rnd_input) + + @test_throws MethodError QEDprocesses._compute( + TestSetupCustomValidationFAIL(), + rnd_input, + ) + @test_throws MethodError compute(TestSetupCustomValidationFAIL(), rnd_input) + # invalid input should be caught without throwing a MethodError + @test_throws TestException compute( + TestSetupCustomValidationFAIL(), + _transform_to_invalid(rnd_input), + ) + + + @test_throws MethodError QEDprocesses._compute( + TestSetupCustomPostProcessingFAIL(), + rnd_input, + ) + @test_throws MethodError compute(TestSetupCustomPostProcessingFAIL(), rnd_input) + end + + @testset "default interface" begin + stp = TestSetupDefault() + + rnd_input = rand(RNG) + rnd_output = rand(RNG) + @test QEDprocesses._post_processing(stp, rnd_input, rnd_output) == rnd_output + @test isapprox( + QEDprocesses._compute(stp, rnd_input), + _groundtruth_compute(rnd_input), + atol = ATOL, + rtol = RTOL, + ) + @test isapprox( + compute(stp, rnd_input), + _groundtruth_compute(rnd_input), + atol = ATOL, + rtol = RTOL, + ) + end + + @testset "custom input validation" begin + stp = TestSetupCustomAssertValidInput() + rnd_input = rand(RNG) + @test QEDprocesses._assert_valid_input(stp, rnd_input) == nothing + @test_throws TestException QEDprocesses._assert_valid_input( + stp, + _transform_to_invalid(rnd_input), + ) + @test_throws TestException compute(stp, _transform_to_invalid(rnd_input)) + + end + + @testset "custom post processing" begin + stp = TestSetupCustomPostProcessing() + rnd_input = rand(RNG) + rnd_output = rand(RNG) + @test isapprox( + QEDprocesses._post_processing(stp, rnd_input, rnd_output), + _groundtruth_post_processing(rnd_input, rnd_output), + ) + @test isapprox( + compute(stp, rnd_input), + _groundtruth_post_processing(rnd_input, _groundtruth_compute(rnd_input)), + ) + end + + @testset "custom input validation and post processing" begin + stp = TestSetupCustom() + rnd_input = rand(RNG) + rnd_output = rand(RNG) + + @test_throws TestException() compute(stp, _transform_to_invalid(rnd_input)) + @test isapprox( + QEDprocesses._post_processing(stp, rnd_input, rnd_output), + _groundtruth_post_processing(rnd_input, rnd_output), + ) + @test isapprox( + compute(stp, rnd_input), + _groundtruth_post_processing(rnd_input, _groundtruth_compute(rnd_input)), + ) + end +end +# process setup + +struct TestParticle1 <: AbstractParticle end +struct TestParticle2 <: AbstractParticle end +struct TestParticle3 <: AbstractParticle end +struct TestParticle4 <: AbstractParticle end + +PARTICLE_SET = [TestParticle1(), TestParticle2(), TestParticle3(), TestParticle4()] + +struct TestProcess <: AbstractProcessDefinition end +struct TestModel <: AbstractModelDefinition end + +struct TestProcessSetup <: AbstractProcessSetup end +QEDprocesses.scattering_process(::TestProcessSetup) = TestProcess() +QEDprocesses.physical_model(::TestProcessSetup) = TestModel() + +struct TestProcessSetupFAIL <: AbstractProcessSetup end + +@testset "process setup interface" begin + @testset "interface fail" begin + rnd_input = rand(RNG) + @test_throws MethodError scattering_process(TestProcessSetupFAIL()) + @test_throws MethodError physical_model(TestProcessSetupFAIL()) + @test_throws MethodError QEDprocesses._compute(TestProcessSetupFAIL(), rnd_input) + end + + @testset "hard interface" begin + stp = TestProcessSetup() + + @test QEDprocesses._is_computation_setup(stp) + @test scattering_process(stp) == TestProcess() + @test physical_model(stp) == TestModel() + end + + @testset "($N_INCOMING,$N_OUTGOING)" for (N_INCOMING, N_OUTGOING) in Iterators.product( + (1, rand(RNG, 2:8)), + (1, rand(RNG, 2:8)), + ) + INCOMING_PARTICLES = rand(RNG, PARTICLE_SET, N_INCOMING) + OUTGOING_PARTICLES = rand(RNG, PARTICLE_SET, N_OUTGOING) + + QEDprocesses.incoming_particles(::TestProcess) = INCOMING_PARTICLES + QEDprocesses.outgoing_particles(::TestProcess) = OUTGOING_PARTICLES + + @testset "delegated functions" begin + stp = TestProcessSetup() + @test number_incoming_particles(stp) == N_INCOMING + @test number_outgoing_particles(stp) == N_OUTGOING + end + + end +end diff --git a/test/runtests.jl b/test/runtests.jl index 9e647ff..4771120 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -13,6 +13,9 @@ begin @time @safetestset "process interface" begin include("interfaces/process_interface.jl") end + @time @safetestset "computation setup interface" begin + include("interfaces/setup_interface.jl") + end # modules @time @safetestset "particles types" begin