From 9aa0b3b55acdcf4f9956ef805fb752b0ab681527 Mon Sep 17 00:00:00 2001 From: Matt LeBlanc Date: Tue, 5 Aug 2025 20:35:18 -0400 Subject: [PATCH 01/32] Initial VLC implementation --- docs/src/examples.md | 2 + docs/src/index.md | 1 + src/AlgorithmStrategyEnums.jl | 10 +- src/ClusterSequence.jl | 4 +- src/EEAlgorithm.jl | 195 +++++++-- src/GenericAlgo.jl | 17 +- ...stjet-valencia-exclusive-d500-eeH.json.zst | Bin 0 -> 2172 bytes ...s-fastjet-valencia-exclusive4-eeH.json.zst | Bin 0 -> 4184 bytes ...-fastjet-valencia-inclusive20-eeH.json.zst | Bin 0 -> 2447 bytes .../valencia-cluster-sequence-eeH.json.zst | Bin 0 -> 13 bytes test/runtests.jl | 3 + test/test-valencia.jl | 388 ++++++++++++++++++ 12 files changed, 575 insertions(+), 45 deletions(-) create mode 100644 test/data/jet-collections-fastjet-valencia-exclusive-d500-eeH.json.zst create mode 100644 test/data/jet-collections-fastjet-valencia-exclusive4-eeH.json.zst create mode 100644 test/data/jet-collections-fastjet-valencia-inclusive20-eeH.json.zst create mode 100644 test/data/valencia-cluster-sequence-eeH.json.zst create mode 100644 test/test-valencia.jl diff --git a/docs/src/examples.md b/docs/src/examples.md index 08e3688a..60f7dac8 100644 --- a/docs/src/examples.md +++ b/docs/src/examples.md @@ -26,6 +26,8 @@ julia --project jetreco.jl --algorithm=AntiKt ../test/data/events.pp13TeV.hepmc3 ... julia --project jetreco.jl --algorithm=Durham ../test/data/events.eeH.hepmc3.zst ... +julia --project jetreco.jl --algorithm=Valencia --p=1.2 --gamma=0.8 ../test/data/events.eeH.hepmc3.zst +... julia --project jetreco.jl --maxevents=10 --strategy=N2Plain --algorithm=Kt --exclusive-njets=3 ../test/data/events.pp13TeV.hepmc3.zst ... ``` diff --git a/docs/src/index.md b/docs/src/index.md index f8eb4854..ba4c4451 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -56,6 +56,7 @@ Each known algorithm is referenced using a `JetAlgorithm` scoped enum value. | generalised ``k_\text{T}`` | `JetAlgorithm.GenKt` | For $pp$, value of `p` must also be specified | | ``e^+e-`` ``k_\text{T}`` / Durham | `JetAlgorithm.Durham` | `R` value ignored and can be omitted | | generalised ``e^+e-`` ``k_\text{T}`` | `JetAlgorithm.EEKt` | For ``e^+e^-``, value of `p` must also be specified | +| Valencia | `JetAlgorithm.Valencia` | For ``e^+e^-``, values of `p` (β) and `γ` must be specified | ### Strategy diff --git a/src/AlgorithmStrategyEnums.jl b/src/AlgorithmStrategyEnums.jl index 64fe8fa7..1a7f26dc 100644 --- a/src/AlgorithmStrategyEnums.jl +++ b/src/AlgorithmStrategyEnums.jl @@ -26,8 +26,9 @@ Scoped enumeration (using EnumX) representing different jet algorithms used in t - `GenKt`: The Generalised Kt algorithm (with arbitrary power). - `EEKt`: The Generalised e+e- kt algorithm. - `Durham`: The e+e- kt algorithm, aka Durham. +- `Valencia`: The Valencia e+e- algorithm. """ -@enumx T=Algorithm JetAlgorithm AntiKt CA Kt GenKt EEKt Durham +@enumx T=Algorithm JetAlgorithm AntiKt CA Kt GenKt EEKt Durham Valencia const AllJetRecoAlgorithms = [String(Symbol(x)) for x in instances(JetAlgorithm.Algorithm)] """ @@ -36,7 +37,7 @@ const AllJetRecoAlgorithms = [String(Symbol(x)) for x in instances(JetAlgorithm. A constant array that contains the jet algorithms for which power is variable. """ -const varpower_algorithms = [JetAlgorithm.GenKt, JetAlgorithm.EEKt] +const varpower_algorithms = [JetAlgorithm.GenKt, JetAlgorithm.EEKt, JetAlgorithm.Valencia] """ algorithm2power @@ -46,7 +47,8 @@ A dictionary that maps algorithm names to their corresponding power values. const algorithm2power = Dict(JetAlgorithm.AntiKt => -1, JetAlgorithm.CA => 0, JetAlgorithm.Kt => 1, - JetAlgorithm.Durham => 1) + JetAlgorithm.Durham => 1, + JetAlgorithm.Valencia => 1) """ get_algorithm_power(; algorithm::JetAlgorithm.Algorithm, p::Union{Real, Nothing}) -> Real @@ -109,7 +111,7 @@ Check if the algorithm is a e+e- reconstruction algorithm. `true` if the algorithm is a e+e- reconstruction algorithm, `false` otherwise. """ function is_ee(algorithm::JetAlgorithm.Algorithm) - return algorithm in [JetAlgorithm.EEKt, JetAlgorithm.Durham] + return algorithm in (JetAlgorithm.EEKt, JetAlgorithm.Durham, JetAlgorithm.Valencia) end """ diff --git a/src/ClusterSequence.jl b/src/ClusterSequence.jl index 47c4fd03..2476190e 100644 --- a/src/ClusterSequence.jl +++ b/src/ClusterSequence.jl @@ -332,7 +332,7 @@ function exclusive_jets(clusterseq::ClusterSequence{U}, throw(ArgumentError("Algorithm $(clusterseq.algorithm) requires power >= 0 for exclusive jets (power=$(clusterseq.power))")) elseif clusterseq.algorithm ∉ (JetAlgorithm.CA, JetAlgorithm.Kt, JetAlgorithm.Durham, JetAlgorithm.GenKt, - JetAlgorithm.EEKt) + JetAlgorithm.EEKt, JetAlgorithm.Valencia) throw(ArgumentError("Algorithm used is not suitable for exclusive jets ($(clusterseq.algorithm))")) end @@ -395,7 +395,7 @@ function n_exclusive_jets(clusterseq::ClusterSequence; dcut::AbstractFloat) throw(ArgumentError("Algorithm $(clusterseq.algorithm) requires power >= 0 for exclusive jets(power=$(clusterseq.power))")) elseif clusterseq.algorithm ∉ (JetAlgorithm.CA, JetAlgorithm.Kt, JetAlgorithm.Durham, JetAlgorithm.GenKt, - JetAlgorithm.EEKt) + JetAlgorithm.EEKt, JetAlgorithm.Valencia) throw(ArgumentError("Algorithm used is not suitable for exclusive jets ($(clusterseq.algorithm))")) end diff --git a/src/EEAlgorithm.jl b/src/EEAlgorithm.jl index 7c070479..e03823b8 100644 --- a/src/EEAlgorithm.jl +++ b/src/EEAlgorithm.jl @@ -1,5 +1,4 @@ -# Use these constants whenever we need to set a large value for the distance or -# metric distance +# Use these constants whenever we need to set a large value for the distance or metric distance const large_distance = 16.0 # = 4^2 const large_dij = 1.0e6 @@ -24,7 +23,62 @@ Calculate the angular distance between two jets `i` and `j` using the formula end """ - dij_dist(eereco, i, j, dij_factor) + valencia_distance(eereco, i, j, R) -> Float64 + +Calculate the Valencia distance between two jets `i` and `j` as +``min(E_i^{2β}, E_j^{2β}) * 2 * (1 - cos(θ_{ij})) / R²``. + +# Arguments +- `eereco`: The array of `EERecoJet` objects. +- `i`: The first jet. +- `j`: The second jet. +- `R`: The jet radius parameter. + +# Returns +- `Float64`: The Valencia distance between `i` and `j`. +""" +Base.@propagate_inbounds function valencia_distance(eereco, i, j, R) + angular_dist = angular_distance(eereco, i, j) + # Valencia dij : min(E_i^{2β}, E_j^{2β}) * 2 * (1 - cos θ) / R² + # Note that β plays the role of p in other algorithms, so E2p can be used. + min(eereco[i].E2p, eereco[j].E2p) * 2 * angular_dist / (R * R) +end + +""" + valencia_beam_distance(eereco, i, γ) -> Float64 + +Calculate the Valencia beam distance for jet `i` using the formula +``E_i^{2β} * sin(θ_i)^{2γ}``, where `sin(θ_i) = pt / sqrt(pt^2 + pz^2)`. +This matches the FastJet contrib::ValenciaPlugin implementation. + +# Arguments +- `eereco`: The array of `EERecoJet` objects. +- `i`: The jet index. +- `γ`: The angular exponent parameter used in the Valencia beam distance. + +# Returns +- `Float64`: The Valencia beam distance for jet `i`. + +# Details +The Valencia beam distance is used in the Valencia jet algorithm for e⁺e⁻ collisions. +It generalizes the beam distance by including an angular exponent γ, allowing for +flexible jet finding. The formula is: + + d_beam = E_i^{2β} * [pt / sqrt(pt^2 + pz^2)]^{2γ} + +where β is the energy exponent (typically set via the algorithm parameters). +""" +@inline function valencia_beam_distance(eereco, i, γ, β) + # Since nx, ny, nz are normalized direction vectors (px/E, py/E, pz/E), + # sin(θ) = pt/sqrt(pt^2 + pz^2) = sqrt(nx^2 + ny^2) + nx = eereco[i].nx + ny = eereco[i].ny + sin_theta = sqrt(nx^2 + ny^2) + @inbounds eereco[i].E2p * sin_theta^(2γ) +end + +""" + dij_dist(eereco, i, j, dij_factor, algorithm = JetAlgorithm.Durham, R = 4.0) Calculate the dij distance between two ``e^+e^-``jets. @@ -33,17 +87,25 @@ Calculate the dij distance between two ``e^+e^-``jets. - `i`: The first jet. - `j`: The second jet. - `dij_factor`: The scaling factor to multiply the dij distance by. +- `algorithm`: The jet algorithm being used. +- `R`: the radius or resolution parameter # Returns - The dij distance between `i` and `j`. """ -@inline function dij_dist(eereco, i, j, dij_factor) +@inline function dij_dist(eereco, i, j, dij_factor, algorithm = JetAlgorithm.Durham, + R = 4.0) # Calculate the dij distance for jet i from jet j j == 0 && return large_dij - @inbounds min(eereco[i].E2p, eereco[j].E2p) * dij_factor * eereco[i].nndist + + if algorithm == JetAlgorithm.Valencia + @inbounds valencia_distance(eereco, i, j, R) + else + @inbounds min(eereco[i].E2p, eereco[j].E2p) * dij_factor * eereco[i].nndist + end end -function get_angular_nearest_neighbours!(eereco, algorithm, dij_factor) +function get_angular_nearest_neighbours!(eereco, algorithm, dij_factor, p, γ = 1.0, R = 4.0) # Get the initial nearest neighbours for each jet N = length(eereco) # this_dist_vector = Vector{Float64}(undef, N) @@ -55,6 +117,7 @@ function get_angular_nearest_neighbours!(eereco, algorithm, dij_factor) # The problem here will be avoiding allocations for the array outputs, which would easily # kill performance @inbounds for j in (i + 1):N + # Always use angular distance for nearest neighbor search this_nndist = angular_distance(eereco, i, j) # Using these ternary operators is faster than the if-else block @@ -68,7 +131,11 @@ function get_angular_nearest_neighbours!(eereco, algorithm, dij_factor) end # Nearest neighbour dij distance for i in 1:N - eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor) + if algorithm == JetAlgorithm.Valencia + eereco.dijdist[i] = @inbounds valencia_distance(eereco, i, eereco[i].nni, R) + else + eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, algorithm, R) + end end # For the EEKt algorithm, we need to check the beam distance as well # (This is structured to only check for EEKt once) @@ -78,36 +145,54 @@ function get_angular_nearest_neighbours!(eereco, algorithm, dij_factor) eereco.dijdist[i] = beam_closer ? eereco[i].E2p : eereco.dijdist[i] eereco.nni[i] = beam_closer ? 0 : eereco.nni[i] end + elseif algorithm == JetAlgorithm.Valencia + @inbounds for i in 1:N + valencia_beam_dist = valencia_beam_distance(eereco, i, γ, p) + beam_closer = valencia_beam_dist < eereco[i].dijdist + eereco.dijdist[i] = beam_closer ? valencia_beam_dist : eereco.dijdist[i] + eereco.nni[i] = beam_closer ? 0 : eereco.nni[i] + end end end # Update the nearest neighbour for jet i, w.r.t. all other active jets -function update_nn_no_cross!(eereco, i, N, algorithm, dij_factor) +function update_nn_no_cross!(eereco, i, N, algorithm, dij_factor, β = 1.0, γ = 1.0, R = 4.0) eereco.nndist[i] = large_distance eereco.nni[i] = i @inbounds for j in 1:N if j != i + # Always use angular distance for nearest neighbor search this_nndist = angular_distance(eereco, i, j) better_nndist_i = this_nndist < eereco[i].nndist eereco.nndist[i] = better_nndist_i ? this_nndist : eereco.nndist[i] eereco.nni[i] = better_nndist_i ? j : eereco.nni[i] end end - eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor) + if algorithm == JetAlgorithm.Valencia + eereco.dijdist[i] = @inbounds valencia_distance(eereco, i, eereco[i].nni, R) + else + eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, algorithm, R) + end if algorithm == JetAlgorithm.EEKt beam_close = eereco[i].E2p < eereco[i].dijdist eereco.dijdist[i] = beam_close ? eereco[i].E2p : eereco.dijdist[i] eereco.nni[i] = beam_close ? 0 : eereco.nni[i] + elseif algorithm == JetAlgorithm.Valencia + valencia_beam_dist = valencia_beam_distance(eereco, i, γ, β) + beam_close = valencia_beam_dist < eereco[i].dijdist + eereco.dijdist[i] = beam_close ? valencia_beam_dist : eereco.dijdist[i] + eereco.nni[i] = beam_close ? 0 : eereco.nni[i] end end -function update_nn_cross!(eereco, i, N, algorithm, dij_factor) +function update_nn_cross!(eereco, i, N, algorithm, dij_factor, β = 1.0, γ = 1.0, R = 4.0) # Update the nearest neighbour for jet i, w.r.t. all other active jets # also doing the cross check for the other jet eereco.nndist[i] = large_distance eereco.nni[i] = i @inbounds for j in 1:N if j != i + # Always use angular distance for nearest neighbor search this_nndist = angular_distance(eereco, i, j) better_nndist_i = this_nndist < eereco[i].nndist eereco.nndist[i] = better_nndist_i ? this_nndist : eereco.nndist[i] @@ -116,21 +201,40 @@ function update_nn_cross!(eereco, i, N, algorithm, dij_factor) eereco.nndist[j] = this_nndist eereco.nni[j] = i # j will not be revisited, so update metric distance here - eereco.dijdist[j] = dij_dist(eereco, j, i, dij_factor) + if algorithm == JetAlgorithm.Valencia + eereco.dijdist[j] = @inbounds valencia_distance(eereco, j, i, R) + else + eereco.dijdist[j] = dij_dist(eereco, j, i, dij_factor, algorithm, R) + end if algorithm == JetAlgorithm.EEKt if eereco[j].E2p < eereco[j].dijdist eereco.dijdist[j] = eereco[j].E2p eereco.nni[j] = 0 end + elseif algorithm == JetAlgorithm.Valencia + valencia_beam_dist = valencia_beam_distance(eereco, j, γ, β) + if valencia_beam_dist < eereco[j].dijdist + eereco.dijdist[j] = valencia_beam_dist + eereco.nni[j] = 0 + end end end end end - eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor) + if algorithm == JetAlgorithm.Valencia + eereco.dijdist[i] = @inbounds valencia_distance(eereco, i, eereco[i].nni, R) + else + eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, algorithm, R) + end if algorithm == JetAlgorithm.EEKt beam_close = eereco[i].E2p < eereco[i].dijdist eereco.dijdist[i] = beam_close ? eereco[i].E2p : eereco.dijdist[i] eereco.nni[i] = beam_close ? 0 : eereco.nni[i] + elseif algorithm == JetAlgorithm.Valencia + valencia_beam_dist = valencia_beam_distance(eereco, i, γ, β) + beam_close = valencia_beam_dist < eereco[i].dijdist + eereco.dijdist[i] = beam_close ? valencia_beam_dist : eereco.dijdist[i] + eereco.nni[i] = beam_close ? 0 : eereco.nni[i] end end @@ -193,7 +297,7 @@ end """ ee_genkt_algorithm(particles::AbstractVector{T}; algorithm::JetAlgorithm.Algorithm, p::Union{Real, Nothing} = nothing, R = 4.0, recombine = addjets, - preprocess = nothing) where {T} + preprocess = nothing, γ::Real = 1.0) where {T} Run an e+e- reconstruction algorithm on a set of initial particles. @@ -201,12 +305,14 @@ Run an e+e- reconstruction algorithm on a set of initial particles. - `particles::AbstractVector{T}`: A vector of particles to be clustered. - `algorithm::JetAlgorithm.Algorithm`: The jet algorithm to use. - `p::Union{Real, Nothing} = nothing`: The power parameter for the algorithm. - This is not required for the `Durham` algorithm, but must be specified for the - `EEKt`` algorithm. -- `R = 4.0`: The jet radius parameter. Not required and ignored for the `Durham` + Must be specified for EEKt algorithm. + For Valencia algorithm, this corresponds to the β parameter. + Other algorithms will ignore this value. +- `R = 4.0`: The jet radius parameter. Not required / ignored for the Durham algorithm. -- `recombine = addjets`: The recombination scheme to use. -- `preprocess = nothing`: Preprocessing function for input particles. +- `recombine`: The recombination scheme to use. +- `preprocess`: Preprocessing function for input particles. +- `γ::Real = 1.0`: The angular exponent parameter for Valencia algorithm. Ignored for other algorithms. # Returns - The result of the jet clustering as a `ClusterSequence` object. @@ -217,18 +323,27 @@ will check for consistency between the algorithm and the power parameter as needed. It will then prepare the internal EDM particles for the clustering itself, and call the actual reconstruction method `_ee_genkt_algorithm!`. -If the algorithm is Durham, `R` is nominally set to 4. If the algorithm is EEkt, -power `p` must also be specified. +If the algorithm is Durham, `R` is nominally set to 4. +If the algorithm is EEkt, power `p` must be specified. +If the algorithm is Valencia, both `p` (β) and `γ` should be specified. """ function ee_genkt_algorithm(particles::AbstractVector{T}; algorithm::JetAlgorithm.Algorithm, p::Union{Real, Nothing} = nothing, R = 4.0, recombine = addjets, - preprocess = nothing) where {T} + preprocess = nothing, γ::Real = 1.0, + β::Union{Real, Nothing} = nothing) where {T} + + # For Valencia, if β is provided, overwrite p + if algorithm == JetAlgorithm.Valencia && β !== nothing + p = β + end # Check for consistency algorithm power p = get_algorithm_power(p = p, algorithm = algorithm) - # Integer p if possible - p = (round(p) == p) ? Int(p) : p + # Integer p if possible, i.e. if not running Valencia + if algorithm != JetAlgorithm.Valencia + p = (round(p) == p) ? Int(p) : p + end # For the Durham algorithm, p=1 and R is not used, but nominally set to 4 if algorithm == JetAlgorithm.Durham @@ -262,15 +377,15 @@ function ee_genkt_algorithm(particles::AbstractVector{T}; algorithm::JetAlgorith end # Now call the actual reconstruction method, tuned for our internal EDM - _ee_genkt_algorithm!(recombination_particles; p = p, R = R, - algorithm = algorithm, - recombine = recombine) + _ee_genkt_algorithm(particles = recombination_particles, p = p, R = R, + algorithm = algorithm, + recombine = recombine, γ = γ) end """ _ee_genkt_algorithm!(particles::AbstractVector{EEJet}; algorithm::JetAlgorithm.Algorithm, p::Real, R = 4.0, - recombine = addjets) + recombine = addjets, γ::Real = 1.0) This function is the internal implementation of the e+e- jet clustering algorithm. It takes a vector of `EEJet` `particles` representing the input @@ -294,14 +409,17 @@ entry point to this jet reconstruction. - `clusterseq`: The resulting `ClusterSequence` object representing the reconstructed jets. """ -function _ee_genkt_algorithm!(particles::AbstractVector{EEJet}; - algorithm::JetAlgorithm.Algorithm, p::Real, R::Real = 4.0, - recombine = addjets) +function _ee_genkt_algorithm(; particles::AbstractVector{EEJet}, + algorithm::JetAlgorithm.Algorithm, p::Real, R = 4.0, + recombine = addjets, γ::Real = 1.0, + beta::Union{Real, Nothing} = nothing) # Bounds N::Int = length(particles) - # R squared R2 = R^2 + if algorithm == JetAlgorithm.Valencia && beta !== nothing + p = beta + end # Constant factor for the dij metric and the beam distance function if algorithm == JetAlgorithm.Durham @@ -312,6 +430,8 @@ function _ee_genkt_algorithm!(particles::AbstractVector{EEJet}; else dij_factor = 1 / (3 + cos(R)) end + elseif algorithm == JetAlgorithm.Valencia + dij_factor = 1.0 # Valencia distance function contains complete formula with /R² division else throw(ArgumentError("Algorithm $algorithm not supported for e+e-")) end @@ -320,6 +440,7 @@ function _ee_genkt_algorithm!(particles::AbstractVector{EEJet}; # jet information and populate it accordingly # We need N slots for this array eereco = StructArray{EERecoJet}(undef, N) + fill_reco_array!(eereco, particles, R2, p) # Setup the initial history and get the total energy @@ -329,7 +450,7 @@ function _ee_genkt_algorithm!(particles::AbstractVector{EEJet}; Qtot) # Run over initial pairs of jets to find nearest neighbours - get_angular_nearest_neighbours!(eereco, algorithm, dij_factor) + get_angular_nearest_neighbours!(eereco, algorithm, dij_factor, p, γ, R) # Only for debugging purposes... # ee_check_consistency(clusterseq, clusterseq_index, N, nndist, nndij, nni, "Start") @@ -378,7 +499,11 @@ function _ee_genkt_algorithm!(particles::AbstractVector{EEJet}; newjet_k, dij_min) # Update the compact arrays, reusing the JetA slot - insert_new_jet!(eereco, ijetA, newjet_k, R2, merged_jet, p) + if algorithm == JetAlgorithm.Valencia + insert_new_jet!(eereco, ijetA, newjet_k, R2, merged_jet, p) # Use p (β) for Valencia energy powers + else + insert_new_jet!(eereco, ijetA, newjet_k, R2, merged_jet, p) # Use p for other algorithms + end end # Squash step - copy the final jet's compact data into the jetB slot @@ -400,7 +525,7 @@ function _ee_genkt_algorithm!(particles::AbstractVector{EEJet}; # plus "belt and braces" check for an invalid NN (>N) if (eereco[i].nni == ijetA) || (eereco[i].nni == ijetB) || (eereco[i].nni > N) - update_nn_no_cross!(eereco, i, N, algorithm, dij_factor) + update_nn_no_cross!(eereco, i, N, algorithm, dij_factor, p, γ, R) end end end @@ -408,7 +533,7 @@ function _ee_genkt_algorithm!(particles::AbstractVector{EEJet}; # Finally, we need to update the nearest neighbours for the new jet, checking both ways # (But only if there was a new jet!) if ijetA != ijetB - update_nn_cross!(eereco, ijetA, N, algorithm, dij_factor) + update_nn_cross!(eereco, ijetA, N, algorithm, dij_factor, p, γ, R) end # Only for debugging purposes... diff --git a/src/GenericAlgo.jl b/src/GenericAlgo.jl index 8227c8ed..380e5a84 100644 --- a/src/GenericAlgo.jl +++ b/src/GenericAlgo.jl @@ -2,7 +2,8 @@ jet_reconstruct(particles::AbstractVector; algorithm::JetAlgorithm.Algorithm, p::Union{Real, Nothing} = nothing, R = 1.0, recombine = addjets, preprocess = nothing, - strategy::RecoStrategy.Strategy = RecoStrategy.Best) + strategy::RecoStrategy.Strategy = RecoStrategy.Best, + γ::Real = 1.0) Reconstructs jets from a collection of particles using a specified algorithm and strategy. @@ -22,6 +23,8 @@ strategy. - `strategy::RecoStrategy.Strategy = RecoStrategy.Best`: The jet reconstruction strategy to use. `RecoStrategy.Best` makes a dynamic decision based on the number of starting particles. +- `γ::Real = 1.0`: The angular exponent parameter for Valencia algorithm. Ignored + by other algorithms. Note that `p` must be specified for `GenKt` and `EEKt` algorithms, other algorithms will ignore its value. @@ -67,7 +70,8 @@ jet_reconstruct(particles; algorithm = JetAlgorithm.AntiKt, R = 1.0, preprocess function jet_reconstruct(particles::AbstractVector; algorithm::JetAlgorithm.Algorithm, p::Union{Real, Nothing} = nothing, R = 1.0, recombine = addjets, preprocess = nothing, - strategy::RecoStrategy.Strategy = RecoStrategy.Best) + strategy::RecoStrategy.Strategy = RecoStrategy.Best, + γ::Real = 1.0) if is_pp(algorithm) # We assume a pp reconstruction if strategy == RecoStrategy.Best @@ -87,6 +91,11 @@ function jet_reconstruct(particles::AbstractVector; algorithm::JetAlgorithm.Algo end # Now call the chosen algorithm, passing through the other parameters - alg(particles; p = p, algorithm = algorithm, R = R, recombine = recombine, - preprocess = preprocess) + if is_ee(algorithm) + alg(particles; p, algorithm, R, recombine, + preprocess, γ) + else + alg(particles; p, algorithm, R, recombine, + preprocess) + end end diff --git a/test/data/jet-collections-fastjet-valencia-exclusive-d500-eeH.json.zst b/test/data/jet-collections-fastjet-valencia-exclusive-d500-eeH.json.zst new file mode 100644 index 0000000000000000000000000000000000000000..7576407ebcd7cdbcbc2e7aaa89747e32fc587be4 GIT binary patch literal 2172 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 literal 0 HcmV?d00001 diff --git a/test/data/jet-collections-fastjet-valencia-exclusive4-eeH.json.zst b/test/data/jet-collections-fastjet-valencia-exclusive4-eeH.json.zst new file mode 100644 index 0000000000000000000000000000000000000000..f978f69b9f44bd4befef031558c3f7a93cd22b5b GIT binary patch literal 4184 zcmV-e5U1}bwJ-f-QBPHZ07?PL9UX8dNudo!L==?h%#%V#px^&hi;-ilg{#s;4flUgcWu(VYcQN zQQd4PU(wDQw<;Uvs2sNrv%!cZW4W>tB%GpYwEoaqZpSFr_UvrR3yGo_lc336#n@mY z$B!@Vun{4glA5H7Ca#{?IZfAxS@DqS>Db3)oQjqv;V=D%SeY@?;ciYNyE0~}<;0ws z4Yi$BM-pt+Iko#>t&U{Jyc)D!nR1#4`s&4cjOkD>BL&sRQCfZxCc=i8yZHy_V_`vu z8y!CjnncbSTg)x@xuXRQZAP?hJ{>_3ncvKyI&8>N7k0)cX3An=e8gysl^b)&L1t{Q zm}BQM`5F_v1c~EjP>m29bY-2%1hHY*+ALzWk+PwqM@4dst0+3^%*Mpg!OVyvk8h@I z58o_AdRfWF2C9_Ni=z+<5e3c7aVXC29I0x;1|th?Ztm=VLnkp&7rchh;X=grkRWzgTmk(`GDLt`0fjEj?k4jQ?1jU8UGi zV^rc-WS4O=2WuXoOn5{{c0&?%`d;)iu{LH*=`8Dp%m!g^C2~`?Oq*V;D>yb88_aBV zDl8pe)iR%6G>y%&j+7W(bt6$k)x!od#SiP1=5uM=8H+0HCsN^S!V)fP#dGz>1`8`I z#7a+ySk#ORWhACGVkaD%nYNnP%4s@=$jr;WceIL-#?aN8b4CX;)~(ZKqIu$86`q7t zu%SkY>Xi+|lG=X8p?HK)Y=`UQ*gziMBQ`3MSi==ck%hVJ6zv*!LAM_Lsv6e87`m~r z#;8!U>RV3A3f*X1q%;xfKcCqdx?FKFtoj{eiHwXfN5ax^u5|i}rKr+DO2wi&osHs1 zc8JseT~C>uF-2kQgJkrc=nKn3HtfabTy}oNxJVHlg!TCh`D{2{kZj~A(qDEqgo;-~ zm9ybv))uiT=qSamVMD~6eqUx1kx+}J7~@z}4UG-$4PozKr8cuQbaE{*?BB#C_EMxd zA~~}bL|4QapCG!57H_nOjzVHZ40Q$X4%g%5^n}csv0}DI+g#0tW7)3u#u>4*K~8Jk z8Sy1?#s(|eTK}B)=fg&ku0INW;^?~`Yo+~?89JseN+KDSudP$3vPIhwRnx6(IQAN2 z)<||y=_K2fF0r2oV`+ppqKlffFk0M{4mFzZMio2-omq=lLqsvb4t*-4v%guc^cDs-UpBtDm7`>||joHhF@(~VWsn&>I+S7M*RdT1;E&O21Uys5l_K;g< zPo}K!(;3p3U6O0WsI)E2#?ax26DA3bTyZQVj+~1!Ar3Z}ax;yals%)X<}bX9o~<*^ z%^4k~<2Bs%B^(I5Ubqk|b8BSv8} zW4yh`%00x1(n1FW1Q68Rl|4O0d);PV{;I}o;MF~|G_-#YHWYEf#%z(w#6hyWB%kr5 z?q-EER9|_v6Nj)x^k-s%4zbW>gn3vcR}YnIh4nbIc}o#hy&5Zifwiw#x0Kh9=u z-eITCc>UocU5F;r;zS3ch+I1tA|%I+6mNGVq&m)i6$+Mr!$z@DQ`&fO#j&BRx*uIx zlVkaOtm~U2`)(1UoJy29yAAPTTxT9|>*KQ&!?THrx|ce zGvhg~;>G{SLU>jp6>|Q>Vx}RyW&X0Fm?Ri>MeMN`Mq+PjgI{d ze!I$d7M5zm5<;NK@ z9V}QHn>H3r*-Lkd*!qGEW~C{0{KkkPH5bIX9hz^VG#N3m!R(~D8urAYsF4bO$SDSwc2lW(uTsjKZf08$q3T*f|!>jBx&EmX^-(YV=}eOf8&__vx{BjtJAW#g$xYlIpv-3h%~=$~(O z^c>T6$&w~yoL+ip6MB1eapE(P6*9MPN6<`shrv5tTMBaV&J`CzH-~D)A&6=nd`q*e zv+S9GTYAL<;r@1W7RP+;o}BgVNR*rrtC};63#y zd&3fBW#FQZAZO6&Th09itMk9-KHL&r5;3U z_Ga6hq0hnh3$NziNaJLiXZwNsSWGtCKwByQ2G)r9RC|KbZ5z@hvo=9z@W-VZM&&0N zHl##pTXCJ8$d#N}n{F2zM8xm15fB}EVxxnjcU1czSly?wxd#8oMd_g_9oIYD&b!sk z>zO@ig>$sAzqj0iSn(Mq38jZ~9G03r2-4l>bablC(3w%Jw`gPS zDKL32O@-i(oHE`6U^Ge`QqFuBMuJ{#=wezw5b#0$ zpD;=cXMTB1#gBV#PTBD5ESGt(%LqXPqBGES5m^#$KLk~lS}h?LdL*iy*Q(d(eSJJi!SZkxixQd0ZJDP6vvN%j-5mUA3R;}cHpCw$FKlH`8;grBMjT1if z+vrHb;LrnL_|kphE+->M-P5%^rTB6?(ol_3m4w^uL*XjV30eT zZ@e!z1@iT300=Auv~#p200<}KHz`qSK$Z6vV*j#9nAbF861)$>A(XzDoHm;+1v2(IAjQAEX`QXcOHg@b0Yjct z8WsRG#F5!$o|5#pFa+3g(-Tn~=04w(NZ_QPfyg_od)|u8fGANbUQ>ge44}*_l-Q`t zsFIqr2`x86FMlpdH3L~Ol_#+aYzlz!bf;mC)iWsM*$u;iO4q)#o z-3rOu&DagA1+V8N=VRl-*$cDgn@Ud^Fs4iC+It}C&Y{w$OELT`pC1C{Vum4@uEhUQ z4B=f%LeAUwnlo1LvmC`)0B(G+pj@U76I9?zZFb{dJc%3cEmRa2#XH>JU4oTx(?v|m z&F%mkJsy&0PK|9Lpk3tWI9q7}@fkK|LYy5K?6ahPJG0+l>k{HXjql|mb@5Z*jI#&W zK@%|YK4-_pv}18Jd1}Kw9jZzy7r*$kf3##0KG!KykQ6bvu3@Ht||^gX3f~5K+qyU(m3Q}J@+H^bCQ}yfarAQ0t5;Y zur*JhQ_9d9%{rv*7+uGwUL7fIGni4hxqL$Wo&L}UlFnpB+`}HuRqEU}cm1j_O@@5* zlo!xS{M6y=$nl1~sfYa(`3+Q|Isn&b#Ji{zX-r9@a1huUKnj{h;@Q*dsq2cpigIw9 z)N7>eM)IgQ^7PctcsF?`pH$<yH$P+gsbW`aBBAAoStl8+5mD-6A znLlG)%SfhBB1?O+`%x@b%X7L2sVov^qewaM5ra$1a_K@=_Ad&ge=0L~)AA^g=g1 i-K;6Bi=gfl literal 0 HcmV?d00001 diff --git a/test/data/jet-collections-fastjet-valencia-inclusive20-eeH.json.zst b/test/data/jet-collections-fastjet-valencia-inclusive20-eeH.json.zst new file mode 100644 index 0000000000000000000000000000000000000000..8816d0ad9ba3b14af2d09590828e6760a7417f7c GIT binary patch literal 2447 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 N2OK^Ik2bzfQ=F1y_03PE6+5i9m literal 0 HcmV?d00001 diff --git a/test/runtests.jl b/test/runtests.jl index 4256853f..5390a039 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -19,6 +19,9 @@ function main() include("test-pp-reconstruction.jl") include("test-ee-reconstruction.jl") + # Valencia algorithm tests + include("test-valencia.jl") + # Test jets selection include("test-selection.jl") diff --git a/test/test-valencia.jl b/test/test-valencia.jl new file mode 100644 index 00000000..f6403d9c --- /dev/null +++ b/test/test-valencia.jl @@ -0,0 +1,388 @@ +""" + test-valencia.jl + +Test for the Valencia jet algorithm implementation. + +Validates clustering results and cluster sequence against FastJet C++ reference output +using jet matching to handle numerical precision differences. +""" + +# Include common test utilities +include("common.jl") +import JetReconstruction: pt, rapidity, PseudoJet + +using Test +using JetReconstruction + +# JSON package for parsing +using JSON + +# JSON reading function +function read_fastjet_outputs(filename) + if endswith(filename, ".zst") + # Decompress .zst file to a buffer and parse JSON + io = open(`zstdcat $(filename)`, "r") + data = read(io, String) + close(io) + return JSON.parse(data) + else + open(filename, "r") do file + return JSON.parse(file) + end + end +end + +""" + match_jets_with_pairing_optimization(ref_jets, julia_jets; pt_tolerance=0.5, rap_tolerance=1.0, pt_similarity_threshold=0.01) + +Enhanced jet matching that tries alternative pairings when jets have nearly identical pT. +""" +function match_jets_with_pairing_optimization(ref_jets, julia_jets; pt_tolerance = 0.5, + rap_tolerance = 1.0, + pt_similarity_threshold = 0.01) + # First, identify groups of jets with similar pT + n_ref = length(ref_jets) + n_julia = length(julia_jets) + + # Check if we have exactly 2 jets with very similar pT + if n_ref == 2 && n_julia == 2 + ref_pts = [ref_jets[1]["pt"], ref_jets[2]["pt"]] + julia_pts = [begin + jet_pj = PseudoJet(px(julia_jets[i]), py(julia_jets[i]), + pz(julia_jets[i]), energy(julia_jets[i])) + pt(jet_pj) + end + for i in 1:2] + + # Check if reference jets have similar pT + if abs(ref_pts[1] - ref_pts[2]) < pt_similarity_threshold + # Try both pairing arrangements + + # Arrangement 1: ref1->julia1, ref2->julia2 + jet1_pj = PseudoJet(px(julia_jets[1]), py(julia_jets[1]), pz(julia_jets[1]), + energy(julia_jets[1])) + jet2_pj = PseudoJet(px(julia_jets[2]), py(julia_jets[2]), pz(julia_jets[2]), + energy(julia_jets[2])) + + julia1_pt, julia1_rap = pt(jet1_pj), rapidity(jet1_pj) + julia2_pt, julia2_rap = pt(jet2_pj), rapidity(jet2_pj) + + # Score arrangement 1 + score1 = sqrt((abs(julia1_pt - ref_jets[1]["pt"]) / pt_tolerance)^2 + + (abs(julia1_rap - ref_jets[1]["rap"]) / rap_tolerance)^2) + + sqrt((abs(julia2_pt - ref_jets[2]["pt"]) / pt_tolerance)^2 + + (abs(julia2_rap - ref_jets[2]["rap"]) / rap_tolerance)^2) + + # Score arrangement 2: ref1->julia2, ref2->julia1 (swapped) + score2 = sqrt((abs(julia2_pt - ref_jets[1]["pt"]) / pt_tolerance)^2 + + (abs(julia2_rap - ref_jets[1]["rap"]) / rap_tolerance)^2) + + sqrt((abs(julia1_pt - ref_jets[2]["pt"]) / pt_tolerance)^2 + + (abs(julia1_rap - ref_jets[2]["rap"]) / rap_tolerance)^2) + + # Use the better arrangement + if score2 < score1 + # Swapped arrangement is better + return [(ref_jets[1], julia_jets[2], 1, 2), + (ref_jets[2], julia_jets[1], 2, 1)] + else + # Original arrangement is better + return [(ref_jets[1], julia_jets[1], 1, 1), + (ref_jets[2], julia_jets[2], 2, 2)] + end + end + end + + # Fall back to standard greedy matching for other cases + used_julia_jets = Set{Int}() + matched_pairs = [] + + # Sort reference jets by pT (descending) for consistent matching + ref_jets_sorted = sort(collect(enumerate(ref_jets)), by = x -> x[2]["pt"], rev = true) + + for (ref_idx, ref_jet) in ref_jets_sorted + best_match = nothing + best_distance = Inf + + for (julia_idx, julia_jet) in enumerate(julia_jets) + if julia_idx in used_julia_jets + continue + end + + jet_pj = PseudoJet(px(julia_jet), py(julia_jet), pz(julia_jet), + energy(julia_jet)) + julia_pt = pt(jet_pj) + julia_rap = rapidity(jet_pj) + + # Check if jet is within tolerance + pt_diff = abs(julia_pt - ref_jet["pt"]) + rap_diff = abs(julia_rap - ref_jet["rap"]) + + if pt_diff < pt_tolerance && rap_diff < rap_tolerance + # Use combined distance metric + distance = sqrt((pt_diff / pt_tolerance)^2 + (rap_diff / rap_tolerance)^2) + + if distance < best_distance + best_distance = distance + best_match = (julia_jet, julia_idx) + end + end + end + + if best_match !== nothing + julia_jet, julia_idx = best_match + push!(matched_pairs, (ref_jet, julia_jet, ref_idx, julia_idx)) + push!(used_julia_jets, julia_idx) + end + end + + return matched_pairs +end + +""" + match_jets(ref_jets, julia_jets; pt_tolerance=0.5, rap_tolerance=1.0) + +Match reference jets to Julia jets based on kinematics with enhanced handling for similar pT jets. +""" +function match_jets(ref_jets, julia_jets; pt_tolerance = 0.5, rap_tolerance = 1.0) + return match_jets_with_pairing_optimization(ref_jets, julia_jets; + pt_tolerance = pt_tolerance, + rap_tolerance = rap_tolerance) +end + +@testset "Valencia algorithm basic test" begin + # Test with simple 2-particle system + particles = [ + PseudoJet(1.0, 0.0, 0.0, 1.0), + PseudoJet(0.0, 1.0, 0.0, 1.0) + ] + + # Run Valencia algorithm with test parameters + β = 0.8 + γ = 0.8 + R = 1.2 + + # Basic checks + clusterseq = ee_genkt_algorithm(particles, algorithm = JetAlgorithm.Valencia, p = β, + γ = γ, R = R) + @test clusterseq isa ClusterSequence + + # Test exclusive jets + exclusive_jets_result = exclusive_jets(clusterseq, njets = 1) + @test length(exclusive_jets_result) == 1 + + eventfile = joinpath(@__DIR__, "data", "events.eeH.hepmc3.zst") + if isfile(eventfile) + events = read_final_state_particles(eventfile) + # Load reference data for all events (now using .zst files) + inclusive_ref_file = joinpath(@__DIR__, "data", + "jet-collections-fastjet-valencia-inclusive20-eeH.json.zst") + inclusive_ref_all = read_fastjet_outputs(inclusive_ref_file) + exclusive_ref_file = joinpath(@__DIR__, "data", + "jet-collections-fastjet-valencia-exclusive4-eeH.json.zst") + exclusive_ref_all = read_fastjet_outputs(exclusive_ref_file) + exclusive_d500_ref_file = joinpath(@__DIR__, "data", + "jet-collections-fastjet-valencia-exclusive-d500-eeH.json.zst") + exclusive_d500_ref_all = read_fastjet_outputs(exclusive_d500_ref_file) + + for (evt_idx, event) in enumerate(events) + filtered_event = filter(p -> abs(rapidity(p)) <= 4.0, event) + clusterseq = ee_genkt_algorithm(filtered_event, + algorithm = JetAlgorithm.Valencia, p = 0.8, + γ = 0.8, R = 1.2) + + # Debug Event 13 specifically + if evt_idx == 13 + @info "=== EVENT 13 DEBUG ===" + inclusive_20gev = inclusive_jets(clusterseq, ptmin = 20.0) + inclusive_ref_data = inclusive_ref_all[evt_idx]["jets"] + + @info "Reference jets:" + for (i, ref_jet) in enumerate(inclusive_ref_data) + @info " Jet $i: pT=$(ref_jet["pt"]), rap=$(ref_jet["rap"])" + end + + @info "Julia jets:" + for (i, julia_jet) in enumerate(inclusive_20gev) + jet_pj = PseudoJet(px(julia_jet), py(julia_jet), pz(julia_jet), + energy(julia_jet)) + @info " Jet $i: pT=$(pt(jet_pj)), rap=$(rapidity(jet_pj))" + end + + @info "Matching results:" + matched_pairs = match_jets(inclusive_ref_data, inclusive_20gev; + pt_tolerance = 0.5, rap_tolerance = 2.0) + for (ref_jet, julia_jet, ref_idx, julia_idx) in matched_pairs + jet_pj = PseudoJet(px(julia_jet), py(julia_jet), pz(julia_jet), + energy(julia_jet)) + our_pt = pt(jet_pj) + our_rap = rapidity(jet_pj) + pt_diff = abs(our_pt - ref_jet["pt"]) + rap_diff = abs(our_rap - ref_jet["rap"]) + @info " Ref $ref_idx -> Julia $julia_idx: pT_diff=$pt_diff, rap_diff=$rap_diff" + end + end + + # Inclusive jets (pt > 20 GeV) with jet matching + inclusive_20gev = inclusive_jets(clusterseq, ptmin = 20.0) + inclusive_ref_data = inclusive_ref_all[evt_idx]["jets"] + @info "Event $evt_idx: Inclusive jets (pT > 20 GeV): found $(length(inclusive_20gev)), reference $(length(inclusive_ref_data))" + + # Match jets instead of position-based comparison + matched_pairs = match_jets(inclusive_ref_data, inclusive_20gev; + pt_tolerance = 0.1, rap_tolerance = 2.0) + + # Enhanced test: if there are 2 jets with nearly identical pT, check both rapidity pairings + if length(matched_pairs) == 2 + ref_pts = [matched_pairs[1][1]["pt"], matched_pairs[2][1]["pt"]] + if abs(ref_pts[1] - ref_pts[2]) < 0.01 + # Try both rapidity pairings + jet1_pj = PseudoJet(px(matched_pairs[1][2]), py(matched_pairs[1][2]), + pz(matched_pairs[1][2]), + energy(matched_pairs[1][2])) + jet2_pj = PseudoJet(px(matched_pairs[2][2]), py(matched_pairs[2][2]), + pz(matched_pairs[2][2]), + energy(matched_pairs[2][2])) + rap1 = rapidity(jet1_pj) + rap2 = rapidity(jet2_pj) + ref_rap1 = matched_pairs[1][1]["rap"] + ref_rap2 = matched_pairs[2][1]["rap"] + # Arrangement 1 + arrangement1 = abs(rap1 - ref_rap1) < 0.15 && + abs(rap2 - ref_rap2) < 0.15 + # Arrangement 2 (swapped) + arrangement2 = abs(rap2 - ref_rap1) < 0.15 && + abs(rap1 - ref_rap2) < 0.15 + if !(arrangement1 || arrangement2) + println("Event $evt_idx inclusive jets rapidity pairing failed. Printing jets:") + println("Reference jets:") + for (i, ref_jet) in enumerate(inclusive_ref_data) + println(" Jet $i: pT=$(ref_jet["pt"]), rap=$(ref_jet["rap"])") + end + println("Julia jets:") + for (i, julia_jet) in enumerate(inclusive_20gev) + jet_pj = PseudoJet(px(julia_jet), py(julia_jet), pz(julia_jet), + energy(julia_jet)) + println(" Jet $i: pT=$(pt(jet_pj)), rap=$(rapidity(jet_pj))") + end + end + @test arrangement1 || arrangement2 + # Always test pT agreement + if !(abs(pt(jet1_pj) - matched_pairs[1][1]["pt"]) < 0.1) + println("Event $evt_idx inclusive jets pT test failed for jet 1.") + end + if !(abs(pt(jet2_pj) - matched_pairs[2][1]["pt"]) < 0.1) + println("Event $evt_idx inclusive jets pT test failed for jet 2.") + end + @test abs(pt(jet1_pj) - matched_pairs[1][1]["pt"]) < 0.1 + @test abs(pt(jet2_pj) - matched_pairs[2][1]["pt"]) < 0.1 + else + # Standard test for jets with distinct pT + failed = false + for (ref_jet, julia_jet, ref_idx, julia_idx) in matched_pairs + jet_pj = PseudoJet(px(julia_jet), py(julia_jet), pz(julia_jet), + energy(julia_jet)) + our_pt = pt(jet_pj) + our_rap = rapidity(jet_pj) + if !(abs(our_pt - ref_jet["pt"]) < 0.1) || + !(abs(our_rap - ref_jet["rap"]) < 0.15) + failed = true + end + end + if failed + println("Event $evt_idx inclusive jets kinematic test failed. Printing all jets:") + println("Reference jets:") + for (i, ref_jet) in enumerate(inclusive_ref_data) + println(" Jet $i: pT=$(ref_jet["pt"]), rap=$(ref_jet["rap"])") + end + println("Julia jets:") + for (i, julia_jet) in enumerate(inclusive_20gev) + jet_pj = PseudoJet(px(julia_jet), py(julia_jet), pz(julia_jet), + energy(julia_jet)) + println(" Jet $i: pT=$(pt(jet_pj)), rap=$(rapidity(jet_pj))") + end + end + for (ref_jet, julia_jet, ref_idx, julia_idx) in matched_pairs + jet_pj = PseudoJet(px(julia_jet), py(julia_jet), pz(julia_jet), + energy(julia_jet)) + our_pt = pt(jet_pj) + our_rap = rapidity(jet_pj) + @test abs(our_pt - ref_jet["pt"]) < 0.1 + @test abs(our_rap - ref_jet["rap"]) < 0.15 + end + end + else + # Standard test for other cases + failed = false + for (ref_jet, julia_jet, ref_idx, julia_idx) in matched_pairs + jet_pj = PseudoJet(px(julia_jet), py(julia_jet), pz(julia_jet), + energy(julia_jet)) + our_pt = pt(jet_pj) + our_rap = rapidity(jet_pj) + if !(abs(our_pt - ref_jet["pt"]) < 0.1) || + !(abs(our_rap - ref_jet["rap"]) < 0.15) + failed = true + end + end + if failed + println("Event $evt_idx inclusive jets kinematic test failed. Printing all jets:") + println("Reference jets:") + for (i, ref_jet) in enumerate(inclusive_ref_data) + println(" Jet $i: pT=$(ref_jet["pt"]), rap=$(ref_jet["rap"])") + end + println("Julia jets:") + for (i, julia_jet) in enumerate(inclusive_20gev) + jet_pj = PseudoJet(px(julia_jet), py(julia_jet), pz(julia_jet), + energy(julia_jet)) + println(" Jet $i: pT=$(pt(jet_pj)), rap=$(rapidity(jet_pj))") + end + end + for (ref_jet, julia_jet, ref_idx, julia_idx) in matched_pairs + jet_pj = PseudoJet(px(julia_jet), py(julia_jet), pz(julia_jet), + energy(julia_jet)) + our_pt = pt(jet_pj) + our_rap = rapidity(jet_pj) + @test abs(our_pt - ref_jet["pt"]) < 0.1 + @test abs(our_rap - ref_jet["rap"]) < 0.15 + end + end + + # Exclusive N=4 jets with jet matching + exclusive_4 = exclusive_jets(clusterseq, njets = 4) + exclusive_ref_data = exclusive_ref_all[evt_idx]["jets"] + @info "Event $evt_idx: Exclusive jets (N=4): found $(length(exclusive_4)), reference $(length(exclusive_ref_data))" + + # Match jets + matched_pairs = match_jets(exclusive_ref_data, exclusive_4; pt_tolerance = 0.1, + rap_tolerance = 2.0) + + # Test kinematic agreement for matched jets + for (ref_jet, julia_jet, ref_idx, julia_idx) in matched_pairs + jet_pj = PseudoJet(px(julia_jet), py(julia_jet), pz(julia_jet), + energy(julia_jet)) + our_pt = pt(jet_pj) + our_rap = rapidity(jet_pj) + @test abs(our_pt - ref_jet["pt"]) < 0.1 + @test abs(our_rap - ref_jet["rap"]) < 0.15 + end + + # Exclusive d < 500 jets with jet matching + exclusive_d500 = exclusive_jets(clusterseq, dcut = 500.0) + exclusive_d500_ref_data = exclusive_d500_ref_all[evt_idx]["jets"] + @info "Event $evt_idx: Exclusive jets (d < 500): found $(length(exclusive_d500)), reference $(length(exclusive_d500_ref_data))" + + # Match jets with more lenient tolerances for d-cut jets + matched_pairs = match_jets(exclusive_d500_ref_data, exclusive_d500; + pt_tolerance = 0.1, rap_tolerance = 2.0) + + # Test kinematic agreement for matched jets + for (ref_jet, julia_jet, ref_idx, julia_idx) in matched_pairs + jet_pj = PseudoJet(px(julia_jet), py(julia_jet), pz(julia_jet), + energy(julia_jet)) + our_pt = pt(jet_pj) + our_rap = rapidity(jet_pj) + @test abs(our_pt - ref_jet["pt"]) < 0.1 + @test abs(our_rap - ref_jet["rap"]) < 0.15 + end + end + end +end From 6928cea5f5b183532e64f86482d02231cfdba047 Mon Sep 17 00:00:00 2001 From: Matt LeBlanc Date: Mon, 11 Aug 2025 14:55:15 -0400 Subject: [PATCH 02/32] Clean up VLC tests to use a version of ComparisonTest instead, add references with alternative parameter settings. Some tests still failing. --- test/_common.jl | 149 ++++++- ...ve-d500-beta1.0-gamma1.0-R1.0-eeH.json.zst | Bin 0 -> 3441 bytes ...ve-d500-beta1.2-gamma1.2-R0.8-eeH.json.zst | Bin 0 -> 3876 bytes ...stjet-valencia-exclusive-d500-eeH.json.zst | Bin 2172 -> 0 bytes ...lusive4-beta1.0-gamma1.0-R1.0-eeH.json.zst | Bin 0 -> 5434 bytes ...lusive4-beta1.2-gamma1.2-R0.8-eeH.json.zst | Bin 0 -> 5441 bytes ...s-fastjet-valencia-exclusive4-eeH.json.zst | Bin 4184 -> 0 bytes ...usive20-beta1.0-gamma1.0-R1.0-eeH.json.zst | Bin 0 -> 3346 bytes ...usive20-beta1.2-gamma1.2-R0.8-eeH.json.zst | Bin 0 -> 3266 bytes ...-fastjet-valencia-inclusive20-eeH.json.zst | Bin 2447 -> 0 bytes test/test-valencia.jl | 420 ++---------------- 11 files changed, 185 insertions(+), 384 deletions(-) create mode 100644 test/data/jet-collections-fastjet-valencia-exclusive-d500-beta1.0-gamma1.0-R1.0-eeH.json.zst create mode 100644 test/data/jet-collections-fastjet-valencia-exclusive-d500-beta1.2-gamma1.2-R0.8-eeH.json.zst delete mode 100644 test/data/jet-collections-fastjet-valencia-exclusive-d500-eeH.json.zst create mode 100644 test/data/jet-collections-fastjet-valencia-exclusive4-beta1.0-gamma1.0-R1.0-eeH.json.zst create mode 100644 test/data/jet-collections-fastjet-valencia-exclusive4-beta1.2-gamma1.2-R0.8-eeH.json.zst delete mode 100644 test/data/jet-collections-fastjet-valencia-exclusive4-eeH.json.zst create mode 100644 test/data/jet-collections-fastjet-valencia-inclusive20-beta1.0-gamma1.0-R1.0-eeH.json.zst create mode 100644 test/data/jet-collections-fastjet-valencia-inclusive20-beta1.2-gamma1.2-R0.8-eeH.json.zst delete mode 100644 test/data/jet-collections-fastjet-valencia-inclusive20-eeH.json.zst diff --git a/test/_common.jl b/test/_common.jl index 63dd6b5a..5ac64846 100644 --- a/test/_common.jl +++ b/test/_common.jl @@ -58,20 +58,40 @@ struct ComparisonTest end """Constructor where there is no selector_name given""" -function ComparisonTest(events_file, fastjet_outputs, algorithm, strategy, power, R, - selector) +function ComparisonTest(events_file, fastjet_outputs, algorithm, strategy, power, R, selector) selector_name = "" - ComparisonTest(events_file, fastjet_outputs, algorithm, strategy, power, R, selector, - selector_name, addjets, nothing) + ComparisonTest(events_file, fastjet_outputs, algorithm, strategy, power, R, selector, selector_name, addjets, nothing) end """Constructor with no recombine or preprocess specified""" -function ComparisonTest(events_file, fastjet_outputs, algorithm, strategy, power, R, - selector, selector_name) - ComparisonTest(events_file, fastjet_outputs, algorithm, strategy, power, R, selector, - selector_name, addjets, nothing) +function ComparisonTest(events_file, fastjet_outputs, algorithm, strategy, power, R, selector, selector_name) + ComparisonTest(events_file, fastjet_outputs, algorithm, strategy, power, R, selector, selector_name, addjets, nothing) end +""" + struct ComparisonTestValencia + +Test parameters for Valencia jet algorithm comparison, with explicit gamma parameter. +""" +struct ComparisonTestValencia + events_file::AbstractString + fastjet_outputs::AbstractString + algorithm::JetAlgorithm.Algorithm + strategy::RecoStrategy.Strategy + power::Real + R::Real + γ::Real + selector::Any + selector_name::AbstractString + recombine::Any + preprocess::Any +end + +function ComparisonTestValencia(events_file, fastjet_outputs, algorithm, strategy, power, R, γ, selector, selector_name) + ComparisonTestValencia(events_file, fastjet_outputs, algorithm, strategy, power, R, γ, selector, selector_name, addjets, nothing) +end + + """Read JSON file with fastjet jets in it""" function read_fastjet_outputs(fname) f = JetReconstruction.open_with_stream(fname) @@ -107,11 +127,22 @@ function run_reco_test(test::ComparisonTest; testname = nothing) # Run the jet reconstruction jet_collection = Vector{FinalJets}() for (ievent, event) in enumerate(events) - cluster_seq = JetReconstruction.jet_reconstruct(event; R = test.R, p = test.power, - algorithm = test.algorithm, - strategy = test.strategy, - recombine = test.recombine, - preprocess = test.preprocess) + if test.algorithm == JetAlgorithm.Valencia + # For VLC: pass both beta (power) and γ + cluster_seq = JetReconstruction.jet_reconstruct(event; R = test.R, p = test.power, + γ = getfield(test, :γ), + algorithm = test.algorithm, + strategy = test.strategy, + recombine = test.recombine, + preprocess = test.preprocess) + else + # All other algorithms + cluster_seq = JetReconstruction.jet_reconstruct(event; R = test.R, p = test.power, + algorithm = test.algorithm, + strategy = test.strategy, + recombine = test.recombine, + preprocess = test.preprocess) + end finaljets = final_jets(test.selector(cluster_seq)) sort_jets!(finaljets) push!(jet_collection, FinalJets(ievent, finaljets)) @@ -147,3 +178,95 @@ function run_reco_test(test::ComparisonTest; testname = nothing) end end end + +function run_reco_test(test::ComparisonTestValencia; testname = nothing) + events = JetReconstruction.read_final_state_particles(test.events_file) + fastjet_jets = read_fastjet_outputs(test.fastjet_outputs) + sort_jets!(fastjet_jets) + + jet_collection = Vector{FinalJets}() + for (ievent, event) in enumerate(events) + cluster_seq = JetReconstruction.jet_reconstruct(event; R = test.R, p = test.power, + γ = test.γ, + algorithm = test.algorithm, + strategy = test.strategy, + recombine = test.recombine, + preprocess = test.preprocess) + finaljets = final_jets(test.selector(cluster_seq)) + sort_jets!(finaljets) + push!(jet_collection, FinalJets(ievent, finaljets)) + end + + if isnothing(testname) + testname = "FastJet comparison: alg=$(test.algorithm), p=$(test.power), R=$(test.R), strategy=$(test.strategy)" + if test.selector_name != "" + testname *= ", $(test.selector_name)" + end + end + + @testset "$testname" begin + for (ievt, event) in enumerate(jet_collection) + @testset "Event $(ievt)" begin + @test size(event.jets) == size(fastjet_jets[ievt]["jets"]) + # For each reconstructed jet, find the closest reference jet in deltaR + ref_jets = fastjet_jets[ievt]["jets"] + used_refs = falses(length(ref_jets)) + function deltaR(rap1, phi1, rap2, phi2) + dphi = abs(phi1 - phi2) + if dphi > π + dphi -= 2π + end + return sqrt((rap1 - rap2)^2 + dphi^2) + end + for (ijet, jet) in enumerate(event.jets) + # Find closest reference jet in deltaR that hasn't been used + minidx = 0 + mindr = Inf + norm_phi = jet.phi < 0.0 ? jet.phi + 2π : jet.phi + for (ridx, rjet) in enumerate(ref_jets) + if used_refs[ridx] + continue + end + norm_phi_ref = rjet["phi"] < 0.0 ? rjet["phi"] + 2π : rjet["phi"] + dr = deltaR(jet.rap, norm_phi, rjet["rap"], norm_phi_ref) + if dr < mindr + mindr = dr + minidx = ridx + end + end + if minidx == 0 + println("No unused reference jet found for reconstructed jet $(ijet)") + continue + end + used_refs[minidx] = true + rjet = ref_jets[minidx] + rap_ref = rjet["rap"] + phi_ref = rjet["phi"] + pt_ref = rjet["pt"] + normalised_phi_ref = phi_ref < 0.0 ? phi_ref + 2π : phi_ref + rap_test = isapprox(jet.rap, rap_ref; atol=2e-1) + phi_test = isapprox(norm_phi, normalised_phi_ref; atol=2e-1) + pt_test = isapprox(jet.pt, pt_ref; rtol=2e0) + if !rap_test || !phi_test || !pt_test + println("Jet mismatch in Event $(ievt), Jet $(ijet):") + println(" Failing Jet: pt=$(jet.pt), rap=$(jet.rap), phi=$(norm_phi)") + println(" Reference Jet: pt=$(pt_ref), rap=$(rap_ref), phi=$(phi_ref)") + println(" Passes: pt=$(pt_test), rap=$(rap_test), phi=$(phi_test)") + println("\nAll jets in this event:") + for (jidx, j) in enumerate(event.jets) + norm_phi_j = j.phi < 0.0 ? j.phi + 2π : j.phi + println(" Jet $(jidx): pt=$(j.pt), rap=$(j.rap), phi=$(norm_phi_j)") + end + println("\nAll reference jets in this event:") + for (ridx, rjet2) in enumerate(ref_jets) + println(" Ref Jet $(ridx): pt=$(rjet2["pt"]), rap=$(rjet2["rap"]), phi=$(rjet2["phi"])") + end + end + @test rap_test + @test phi_test + @test pt_test + end + end + end + end +end \ No newline at end of file diff --git a/test/data/jet-collections-fastjet-valencia-exclusive-d500-beta1.0-gamma1.0-R1.0-eeH.json.zst b/test/data/jet-collections-fastjet-valencia-exclusive-d500-beta1.0-gamma1.0-R1.0-eeH.json.zst new file mode 100644 index 0000000000000000000000000000000000000000..b69fc0acfb0e10c3ec32e53b4d5586e2386cd422 GIT binary patch literal 3441 zcmV-%4UY0CwJ-f-l{g)107{Tx5gl+hNwEP&;L}e%(1}8;syGzcy}4!LDc{!r;-3j6 z0RsUA0ri>kM01^Ak0oOt4VKYeteApS-zY)9GM&AvXrB_17~ip|&YNkY&y^w;wos;d zkRpUNRgvr%3Rdi(+u)piN{QS`_!uMN?S(7S&|_#h6Z#L%_{iopri-fe2&&~=7iKk~ z1evjRVUC@+Ofj+1qSlOf*xIZXoG>wZF5_Ux(P5@YwdU$`Wg@()jdjh@6?uI0O~kQw zn9H&Iw2>ln&Ti*OGekj|nzBJNRve?2w#PR?zjC?|r((&eOq{r2uQ-hoqvAfptHsr! zoDo})sA)$+!cLqjh;?zWU>FPxEU1k*ciDpf8x9r>$uI^21OWm-Ko}0%C_4}!AON5M z!f;TPVgv$$f&v2pf&mE*3~=Cq03iVZ2M#zu7=~dmP%sPz1p=L(eN>`(R+C8O7-hfUs}d!-XDphL3R1+!i1Ip<(zak!Q5sfqsUlAAXjLDE`HYV8P7H%8 zfKb+NJ)BEwsRSdX6sqi*QJOZ00v8W7XoEu|0=AHSCe6jK20qp<-{w_=L=D%sCqp6}%Xl z(w_>NWIRST$`DJ$h7?uDZuI;-qvL;f(+nFT7Hrswj+F`zQNl8usyoH%MzNCa70xBD zX3W__Pc+FnMoV0XrkG2ZbfrCvds&UR{-CR(6G!e?v64rWp4*H!X2XX_5koGdD7G;q zOhdLBzTlya<2B0pHIyo;6Z-mS)zK_0?C5A;hiXm?xqLQkbO8noY+;Lgu`OEEG9|h) zr!YeD)qd5l@$7<1C~;a$>g33zGL>>vcUQ%e_hRnm=pg!^3!D6otW2g(PRgpfRQe%i z*x4%1jD7Q(h$55g4aZEZC5-V|+nvttTt3#B$?5jAO;6R$ODS(rQH0FDpL6@i$`~(U z;~ZAMl7`&v$~bXj)Ja&_ewu7?ax)iVQZ=;K;zC868JjDa({-uMFwtb<_&2NBo!O>BkvX<%f;d)V1B5`R^JGggSXND? zu(26a6i|>TG$ff4*?tqi527FtP&}>)GC~Lul**X4G5%8xZ?^$b{!QKVaTw_cmh}w5 zT6-nAttCWoT0)r41T*^5WCA7cQy8y?$LYA~q0hL@{cxio7v13u>+p;Ko=m!RG zjV7xACMm#~6p>=;T(X;%XK_SJf=FxQy3p@cdPZ`X zm^X#%C18^~7+3#t%}is4O&n}{z*+9p09AwJVQS|Cec5LCqwv9@7X8$S28##cwuk$0 zV7UZ6sh#^-Y#GNvF;4GRZ$>x*J)<{5w(jm5P+v-qI&|o0p`Oz-?R8m3J_8gXWlB#S z&lRJ)iK*V)83ZN5#t!X-g8(eu=#%Pm2{>BYDcy6Qri@J0wgYu31BC!Ybi2s;bXp-0 zFT#%0!^?)8I=I$2Ih#5%SbrF5)mo60dcDAL51?ZE>uTLr_DhIUP-2;K4ie+UW&y;^ zlWyX1Eg?@LWrZg>Nb6x#5wt@2a$ti#1%kK)l=32*SW(0nN7KqCJk@a`0XkH$3d6jm z#E&KY9l?`9K;22a)1l@vI6yzV9fW@u1aXC02 zC3h*1NsP-a`WlIw<&skSg0_=tX+Y z_$z{^Qx~qcoLdx`pya>>i8XIFkf3iFfZxS!wSopmLS4N^R#GlLr08Q*izn-u?GhFn z>j;gx2{9|@<6>f+f2(nuo5B1Fp@yw+?JK+|NYn~JD=4*{4wJY&recu7n{rYzunM)A zSgm+Omujy?LzlNI3f9yROAE$g;(W}GTKZlD<2dwJ^Aa41yvtMTMCp`D|ebZYzw(F1JCL7M_p=zViBP7HUE<+qaA z^u&u%>R?B_+u`QdI;{EKw7NSNRaxzjA@5w^bAI3UiZ3mVTFf`Gw<{>G*25yW%;fA{ zQ>oW>+!ObTi#kxW$3_+8tKE`3!>*UKH*P z4nd)cPhw{s!}+gX?iMD6mjP;0<(4&m5(`yS_5EW?j{fn2AXF9md9s-~p}=Gj#$KTt zZOB(YR>_21l9VfKxN;#RC>!F3f_2uENqy7NxRkrfxm?51sX?shHl< z%3N7RJb?D5^6sRDJr&4j(6ud{r`*YlaaL#;zR4EsT!3F}@&*zT#^CylR*pJ^qe&tN zo}aLy)CmktWR*x8Cp?Na){wtWt#FD7gzq3ej|k9QDFzi&ZY|i`9w@Ie#ZrJ#6#m@M zWF|LRLfx~eVTW^W@XHC=kG35OtvESH6MYT?4XV!-T=7@|dV^(`UHRCs19Ff!bI zUX;KgJQ#ciJ%&ufn=X!L#p4ne z{@6z2J`M1dKNgL~^22C&o-!nmUCRFG%Tf5ysm=Dq*!)o=~tIP^m2WV$evs@ zY2zcC#hm`grzDAi!B}16FgzZrHKOi`eXyoaET@cOCJkT&@@>=l5-SCp6bGcs>e^8r z0r3K9lhZv$^B3=i3BvC4?qQ2~CxVG` z&_Y}oYhroKI*-J;mjg+dg?BD1RjG-XW~SMH?s_Q!-x~FpHHc(8?aCMKgRkTgN*ftbU@YizN($2Q5%@iUtI;1!0%U zKyp@|2$IB6rEKnj>IctO!fR2HqI57La*+NHH{c4IHsbn~kU)z}&@y-L6Eih5| z1xrHDsfRRublPi&Lxy`A6Fn;)VeMc5xFiO@m3_lzgyt0XdO_CEom57CKB7g#CQxBf z5U;14@NLpOkwKWjFECOaM4{rs;q`tq#9_NH!}wS$QVAJD@Tzjk97dV=Z9-29w4x&6H z0Tclm0W)TeSKlZD{mOLquA+USB6Qs}`B+rv&GcYoqnaWXcDOxA4XHYkJru0l28X1l zSc0ay;mJnC+lMRCJcd3KdVFN_8q-D9dW4xmb>(apW*?T>g#_Kqf@LQzQyQjd5!;#( z^02iXnGt35FhdcE2j@yk>daP{l@|v+_(Y2E8HH@@P|8Tns=`>2V!SNH*0fcOBs4Kc zPuP#Q!`#~0fqdFXkvZq;9BHORL7AGy1%qbHsHNv7=vPh;aVnNv?1o`s!47m9);=?_ z-Q_YaLd7YYu&&_{T@X8B=c8C2BrNsB-WajY)uEh`L`}OmSU?~c7+7%qFx)8nDI**# zsEs%T0tEpAKu{nY^e#IP2ow+iP(Xoj;QttbprD`tfk4541P2BuCOqQqv6igYnPrY_ZC z>x2MicYE0*`u-;8$z=+$Z z;>6K$G+T}FFv$~UCe@Z-uo1=ArW2AMzG8>EqY}Qw${5Ea&PC6aD0YYpFG8$emD9SF ztqAFZLGd?+V~KL`rAj|0WLk>08+L~--E_#N=(8XB_B)jgyUp0J2xY`<%`vf$(I1h- zeCe5S)Jv&8Q=Z1y>zrj4A6qhZOkusLruq7fRGJC65QcLj(XB-k0Y?W;&V$wZwlWIa!HQqLV zxiZEql$ona*)2Zlba95M|CXoHDU?v>XxT^;St_b|TItu4Xu{PPw!=G-Mjx=giPj zr%i%FLG%e7a|x45*S(BULq1)9$Q(P+Sw$y~+_7RM7SXw9>>*RckP9h_Z43zu+3;uM z(@J-%^+jT>I-13V-O0GrHL0kSqq-Zac=F3dkSF*# z@h|az#K=mns_s)N{kW+Zljuca&?cVx6h_d-Mv#h9#Hdbca)!1^ zEFNq5fvTymrYG}5B~|I;&<4Sf-{5jx2DHa!tVdf(w z8F`miy>vLQr=?XUsPvadMHmrxY+qmEt+7Op7uBh5D3V=}fbueA4OJh!U~twAeW-}m zuJ+0EVwlQ4sD$(Lro2}KZe@ey`DA3)fp(AI$&AmL+6YS)SRB28^-oH)oD5Y+@1aj=wogLip$$e}4Nk}^Gi-}5yZY^XQ&s@$FQzpd+oM0);$(j17#)ObbhM$WCm zzxuFy`bT*$SB|jSOG|+xR2tp-cH3*Tbj3-{$eEK*%ROsE|KTGy$UV1mgowjNAo7-J z>4*x;h8`dwRN>SQM21Qc8zOpJP)Aw{8;U%=>0Fz&1;Xt4$<(+pNX(?t5Jm*;mmC~7 z<0<-e+H+JP6}5(f$mPSdF%`3qfqe=lZhRat4^RoFKsvJ8L+0mp^stW-U8;x6P|yTJ z94sTXyXX4U1Ysu8&|qkDS+aaJP;=%WH0=tmZfDT(?x+S7uRq68nq(BmOboF075l5f zuXYHjDD}+CA-5YlN=utSJA61|Hh!~tl}TuEd3m{t;d&Glq7(w8%jCGk1;4T1^TT;3 zu^d4|h|N-F#M6mcnnNk0?Zbh=8#5%kHv<`+?r2)MUhaBbrjbVw8Fa!T67x4OyDq-} zdS%+-ZCtOz&~&2~o3PKqwvH-MJrfe;agd8mxZR*4YORMBdM>pxt(Xhi{VskPTgzBBB{2r+PhvMKz2_!+-1u} zNO{}vfk&dghNzdCM*9DNV*ef^W=9&rd~9yYc4r!5u!_xBt#>Uei2vSTyY?PZNy`Mx zfaFFw-@0eW#h;k!`*AiHgXED4Ty{r!eev24=I(t;oXk3WY?w~Yvk_BqJ=L>9ky9GN zdI*rr0}$AzWP-&=>Diz2jldV9RGn7}dV&Q1;A1g9)fQ*xlLB}JauIqLUTZ|3J)Va$ zfng)nAFGAs-Fr$Q5-0=9&}uoZ33VPuD!QEfPq=4K%MuKrjjF;0-ag`Y@Us@4gwhdd z&0c{;djv&?59)U-B>=POLI}ZDgl9IXlOh1BFn=Y28Bo%cf43)<$m&HzV2bh1h1Iq>57$zjbSp(;9zZ;-aI=rcrGzIvhx9bA z;C|wu9ir-iO@JUtpj2;wU?qr?rzT3rhJkZU$J$UC>r?qK`sFIq2NoDu?bK3kxOXG$ zlta{RmzNV4QcWPe;HaLZjfJS%2?)Fjrk&!0I6G=-u9(u|9{`_vcp6Hwz5J6d?|sfI zF`}#=){YyvK{4JaQ7!POlCJULD*q%a^p_kCEY3A1l7tg{ZYvaO^SD~)UzH$*B%M86 zXF>=MtZ+g=$A^6lf>2L}`QU zV;%VG+~otDktxX?(X7jJI3N9?*74zi6JSRKMy`~0+KW`I4cisZ%lPKA(1rXaCfg7h zfi}L$sB%}AvsN|1SmG)|?SWG03UooAlVjEv@5bV~L5S9Jn_{6A%pq*ru6Uwm41O?M z8q;qgshoCdaN10q8QL~e7EJ{vQV`ul)^HHHveTd&0F*Q8$PO`$@ns zD_hQF*&FHU@FJldbP^(n$jHXiAzd``+ zH2DQhj{6WGZOIMZy}$hR6*<$~_dN+qy!_`J244k20#Dyd+=yYCQnp+V%rr(HDWtA= zpS5C&3CXe{9T@@9bZZRLVhYVW=~w0FTJ^OrE1it}3SH0snn796M z&2Kmf(j^;ET`AL9YRY+h{1lOPGXT*JagJ{d`+cMLUKwNt6fzKfIIt9T6f=Hr+^$IF zy+A|juStJQq_6GG$jCzlUEf^?MrZz^cTGqis`RpWbWnRfnYk7}{<5Hn)m6D^qx~by zbSgk!)2Z(9N|dw(-6O&k9(x{S+uYbUo@Q`d% z-~)=LKUz1u27t5}6jhy#L{J7{3!Ej`x2DPjG{d|#0ncM@P(e)E!M4|HVo52OwFI?u z=6*5r!J%T>y{?HgMaTLMLVxwJvXW^zgX)@1aLfRjgU|7X;r1FCvt$#HwMW-%+yRaw zipl`yNuUy9Ftl5v1dkY1>#=kb2;7HNV3z$jN|XI7J!qbo0K$D_adL=NCqyn`$bFt3 zJy8BXywALtFyKrWW-oqJ>KQdhDmp;~w0b=_3dk0z>sB@9o7^xIYj+jy`^VMZ=Nip2*1p8ILvL_ygbccXCR{A!}N?f5#hpcFl5H!=Y z0;C3v8#RexwC$I8SkGHkVlS+LpmV|x!Yd=lXWoP#Fj5eR`j#{XJATcyQn7iib>Sib zPZDhbF$1UP0$hU+x+s6@@IB#a@sotU-}Sk0L$nOl68je`$~o=fIxWAJbX{FK?bn!> zC9nF#n?dnQDX4=%&8hB`gTJ?d?Az^kMnKdCaCe^-Sn91aWfGfED5>iOX|9a>m6b-g z34%$>tJyNZlp#1-zVP&89!sm8z!3IqD42OTQ}Jj^r->YDS1RESyJ?*)1w=8Cl&yHp zhflr>5pkS0pWG1$7ba8y{u$hdS}z7-$@DV0;+fQyDqk|T(mciVJxcLva-D?JUdp3} z5;n<37*c+lqzzCcSx5hw{@CJzWg?o7v7IL>A3YXL9Tfm8{p5*<2O9Q#96{B9r5aER m#8f6F)~Tfg_98Kr<$j9!Z;EXihDst`3T)`k9ttbDK;AQ@R6JnOkz_^k=@-ppk17bt~9i(5}Ca__s-WuHm0k5S`nS;!cW1okyzM5xgqS?SvHHEICZOoJ`$5E zE`CxoL86-zk{gRWMy9QXBIuYirD|L1jxF3{(pBwo>@|vZmm3nYc06^qKC^4gUEXXp zbhcn8!dp#G5odkZ#ik^i;;NG2-Ja3G+;Sh!J^>=GJ;r{l7P0RX~*0SFL=gqbtq00M(y2rvLZNT7f~fdmf#5EK|7 zuz&!Yc)+Xb%MC|*~ zsx0DoM)JARFH2-W5*!l`QS|4TNG)hN7jMoa59{y~BBq)s7E6f44ab%`IWpd5!mL5tr3=%ty2nb64PjkTxW+gost$QxQ#C5ljV;)J zq?4%6Yp$E`8pV!?R)y%J*JwM!M9efk-t^}e<0q*{Zq`q8!Nz?P9%|gLDlMyM1y(m_ z`iiQ@ibU}ybz;Z84sF7cRPEyC=6n@V%0-RKh;g@Z7ZGQ|6AZgmtsc?wI zTC}p!$gi@W$BnzG**{9g>{xEo37H<2ii`RY$!r=03ZG=+t6^toNJ^K7wwu?)u(7&W zPP3Drm7yWKzslA~g)S6D6qSe0N{qb_qI@iRly{Cu;sOT(4lI1}JImFz#T5&=C6<3N zd3pOjQ!j1TF#AmDQ`IBnNA&q$BPTZ_*-Nwdk%V4HX4SroRS4Nw8_N6Z(C=(zD*wBC z4PBrTnyl#hI-9YGkDO1{laPy(kf4Vc)tfQO7wzRU#lAO|5&FnZ>YXuSjq|gQQ5ao@ z&3NL%*fB`h$UB% z`;sX+MtxP43n|lVtFvmWQoja*c&I|Uq=YfGl0C-a3KWjvNRlb?0}_B!2tkUW6h$Ev z#~DO~gb1>PZ8v2hV_raD(RalLH-crV<8wNGwU(iF^3<_SJ*FdyypgNLl+A|C(_#?c zHl4jb%zvBI$s*Bk5})c&B`D01&NPnvPt0|`I?|1`l}f*=OZ>aW0)-hVy$Lup30?6YR>)av6a2ALVQ(^7FRRp$A<%->nG0ry{5h*1gZ%I>PoAD& zlU_o8qSv$EdU@BaFq&qKIIwE89MCmUeUO(eR;U9k`egI6sUuDVs`APokGTH3hF8fh z1n}@&J~6{-aIm2C;ajW%=Co0fgwe(Y1SPBN@lfLT2@(KIlqkuFpb@_<8kAVz*CQ$_ zFYaPUbAkeEQI-V;Xgdv%8UiPII*Af}q6>Dhry=neU&g%(9ZZFO5Me;t8ws&!5wB)t zPENc<`yu^IE+D<|z5ufisAtC|YMl$jo^W2BR91d}DyBkgt|i@`d*6>XkpCS$2cwLX zQ16l1+H16ENrA7}!}V-Bj~2VHwUZ!3o9j+~U#gByn>k-B`%?q6h1DV}ej&)3_QDlyH7McB8XMvD@4K@FlU2?L5^7P~LgkmE_PO%j%(8g2*&!n38X z=b-A1YJLP98;q4-c)ZWaGQ(N&QyUDzk#~MR5&xRDNM*H|>qykB00+hOUSP&G%!dSz zZcp_Ki_mBOoD?81r-cew+DJ}GIwXp_iXj`LLTx;54kJA&B?`-Ft3PDI+qo97@A9xT zD*#W%PzJ5mpJx6tzz{Av4=I6*1h{yW#=7nU;c21qtVF!$ZHYvIU)K~1=qM>AV@IpBGIER{8(Qn2VU>ou3Fd13Gscy+g^@wADF+%Dd4a-MJhHtD%P7q1} z(tb9Xi$HF94M~oJ`R53@PRrpid{ucZH`>i|znj!MyZylH8%bNtDq}c4)^`JNLYKzX; zrAL~oiHIgG-w9Y&=hSv_XuTlhszksG{q`RmY6L$iJU8XWDOi~DbdL8PVXtUt*}7cy z*Iu@ccqR(W@lBM(WJAaHyOiotzL}M5$CSazci8<(`JSlDJo8YZ$W^5i{0*olgcFnM z9{WeM{YR4z-mZpNQ4^LgVmgDfl~gq23X6mm z9V8u4(myoJ9C@0JyBo0v&9vI06?t~nohgYp5e0s&Z_%H-WXnFA0c yUw)Bb=I{3%>-#$my{t-E7SQXj{|Y3hwY96Nm$`F;)X+QnxSYeManS~4YohPBP8aV0 diff --git a/test/data/jet-collections-fastjet-valencia-exclusive4-beta1.0-gamma1.0-R1.0-eeH.json.zst b/test/data/jet-collections-fastjet-valencia-exclusive4-beta1.0-gamma1.0-R1.0-eeH.json.zst new file mode 100644 index 0000000000000000000000000000000000000000..6ab528937e033e75e16323974f18e3a46d30e2dc GIT binary patch literal 5434 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 literal 0 HcmV?d00001 diff --git a/test/data/jet-collections-fastjet-valencia-exclusive4-eeH.json.zst b/test/data/jet-collections-fastjet-valencia-exclusive4-eeH.json.zst deleted file mode 100644 index f978f69b9f44bd4befef031558c3f7a93cd22b5b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4184 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 i-K;6Bi=gfl diff --git a/test/data/jet-collections-fastjet-valencia-inclusive20-beta1.0-gamma1.0-R1.0-eeH.json.zst b/test/data/jet-collections-fastjet-valencia-inclusive20-beta1.0-gamma1.0-R1.0-eeH.json.zst new file mode 100644 index 0000000000000000000000000000000000000000..725d85fc5c3e3d426fd8a9b153712db0e6733edf GIT binary patch literal 3346 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 literal 0 HcmV?d00001 diff --git a/test/data/jet-collections-fastjet-valencia-inclusive20-beta1.2-gamma1.2-R0.8-eeH.json.zst b/test/data/jet-collections-fastjet-valencia-inclusive20-beta1.2-gamma1.2-R0.8-eeH.json.zst new file mode 100644 index 0000000000000000000000000000000000000000..84f125168aa4d7c8a380aedcc7f7dc7c4f3c5b7f GIT binary patch literal 3266 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 Ah5!Hn literal 0 HcmV?d00001 diff --git a/test/data/jet-collections-fastjet-valencia-inclusive20-eeH.json.zst b/test/data/jet-collections-fastjet-valencia-inclusive20-eeH.json.zst deleted file mode 100644 index 8816d0ad9ba3b14af2d09590828e6760a7417f7c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2447 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 N2OK^Ik2bzfQ inclusive_jets(cs; ptmin = 20.0), "inclusive beta=1.2 gamma=1.2 R=0.8" +) +run_reco_test(valencia_inclusive) + +valencia_exclusive4 = ComparisonTestValencia( + events_file_ee, + joinpath(@__DIR__, "data", "jet-collections-fastjet-valencia-exclusive4-beta1.2-gamma1.2-R0.8-eeH.json.zst"), + JetAlgorithm.Valencia, RecoStrategy.N2Plain, 1.2, 0.8, 1.2, + cs -> exclusive_jets(cs; njets = 4), "exclusive njets beta=1.2 gamma=1.2 R=0.8" +) +run_reco_test(valencia_exclusive4) + +valencia_exclusive_d500 = ComparisonTestValencia( + events_file_ee, + joinpath(@__DIR__, "data", "jet-collections-fastjet-valencia-exclusive-d500-beta1.2-gamma1.2-R0.8-eeH.json.zst"), + JetAlgorithm.Valencia, RecoStrategy.N2Plain, 1.2, 0.8, 1.2, + cs -> exclusive_jets(cs; dcut = 500.0), "exclusive dcut=500 beta=1.2 gamma=1.2 R=0.8" +) +run_reco_test(valencia_exclusive_d500) + +Reference outputs for R=1.0, beta=1.0, gamma=1.0 +valencia_inclusive_b1g1 = ComparisonTestValencia( + events_file_ee, + joinpath(@__DIR__, "data", "jet-collections-fastjet-valencia-inclusive20-beta1.0-gamma1.0-R1.0-eeH.json.zst"), + JetAlgorithm.Valencia, RecoStrategy.N2Plain, 1.0, 1.0, 1.0, + cs -> inclusive_jets(cs; ptmin = 20.0), "inclusive beta=1.0 gamma=1.0 R=1.0" +) +run_reco_test(valencia_inclusive_b1g1) + +valencia_exclusive4_b1g1 = ComparisonTestValencia( + events_file_ee, + joinpath(@__DIR__, "data", "jet-collections-fastjet-valencia-exclusive4-beta1.0-gamma1.0-R1.0-eeH.json.zst"), + JetAlgorithm.Valencia, RecoStrategy.N2Plain, 1.0, 1.0, 1.0, + cs -> exclusive_jets(cs; njets = 4), "exclusive njets beta=1.0 gamma=1.0 R=1.0" +) +run_reco_test(valencia_exclusive4_b1g1) + +valencia_exclusive_d500_b1g1 = ComparisonTestValencia( + events_file_ee, + joinpath(@__DIR__, "data", "jet-collections-fastjet-valencia-exclusive-d500-beta1.0-gamma1.0-R1.0-eeH.json.zst"), + JetAlgorithm.Valencia, RecoStrategy.N2Plain, 1.0, 1.0, 1.0, + cs -> exclusive_jets(cs; dcut = 500.0), "exclusive dcut=500 beta=1.0 gamma=1.0 R=1.0" +) +run_reco_test(valencia_exclusive_d500_b1g1) -# JSON reading function -function read_fastjet_outputs(filename) - if endswith(filename, ".zst") - # Decompress .zst file to a buffer and parse JSON - io = open(`zstdcat $(filename)`, "r") - data = read(io, String) - close(io) - return JSON.parse(data) - else - open(filename, "r") do file - return JSON.parse(file) - end - end -end - -""" - match_jets_with_pairing_optimization(ref_jets, julia_jets; pt_tolerance=0.5, rap_tolerance=1.0, pt_similarity_threshold=0.01) - -Enhanced jet matching that tries alternative pairings when jets have nearly identical pT. -""" -function match_jets_with_pairing_optimization(ref_jets, julia_jets; pt_tolerance = 0.5, - rap_tolerance = 1.0, - pt_similarity_threshold = 0.01) - # First, identify groups of jets with similar pT - n_ref = length(ref_jets) - n_julia = length(julia_jets) - - # Check if we have exactly 2 jets with very similar pT - if n_ref == 2 && n_julia == 2 - ref_pts = [ref_jets[1]["pt"], ref_jets[2]["pt"]] - julia_pts = [begin - jet_pj = PseudoJet(px(julia_jets[i]), py(julia_jets[i]), - pz(julia_jets[i]), energy(julia_jets[i])) - pt(jet_pj) - end - for i in 1:2] - - # Check if reference jets have similar pT - if abs(ref_pts[1] - ref_pts[2]) < pt_similarity_threshold - # Try both pairing arrangements - - # Arrangement 1: ref1->julia1, ref2->julia2 - jet1_pj = PseudoJet(px(julia_jets[1]), py(julia_jets[1]), pz(julia_jets[1]), - energy(julia_jets[1])) - jet2_pj = PseudoJet(px(julia_jets[2]), py(julia_jets[2]), pz(julia_jets[2]), - energy(julia_jets[2])) - - julia1_pt, julia1_rap = pt(jet1_pj), rapidity(jet1_pj) - julia2_pt, julia2_rap = pt(jet2_pj), rapidity(jet2_pj) - - # Score arrangement 1 - score1 = sqrt((abs(julia1_pt - ref_jets[1]["pt"]) / pt_tolerance)^2 + - (abs(julia1_rap - ref_jets[1]["rap"]) / rap_tolerance)^2) + - sqrt((abs(julia2_pt - ref_jets[2]["pt"]) / pt_tolerance)^2 + - (abs(julia2_rap - ref_jets[2]["rap"]) / rap_tolerance)^2) - - # Score arrangement 2: ref1->julia2, ref2->julia1 (swapped) - score2 = sqrt((abs(julia2_pt - ref_jets[1]["pt"]) / pt_tolerance)^2 + - (abs(julia2_rap - ref_jets[1]["rap"]) / rap_tolerance)^2) + - sqrt((abs(julia1_pt - ref_jets[2]["pt"]) / pt_tolerance)^2 + - (abs(julia1_rap - ref_jets[2]["rap"]) / rap_tolerance)^2) - - # Use the better arrangement - if score2 < score1 - # Swapped arrangement is better - return [(ref_jets[1], julia_jets[2], 1, 2), - (ref_jets[2], julia_jets[1], 2, 1)] - else - # Original arrangement is better - return [(ref_jets[1], julia_jets[1], 1, 1), - (ref_jets[2], julia_jets[2], 2, 2)] - end - end - end - - # Fall back to standard greedy matching for other cases - used_julia_jets = Set{Int}() - matched_pairs = [] - - # Sort reference jets by pT (descending) for consistent matching - ref_jets_sorted = sort(collect(enumerate(ref_jets)), by = x -> x[2]["pt"], rev = true) - - for (ref_idx, ref_jet) in ref_jets_sorted - best_match = nothing - best_distance = Inf - - for (julia_idx, julia_jet) in enumerate(julia_jets) - if julia_idx in used_julia_jets - continue - end - - jet_pj = PseudoJet(px(julia_jet), py(julia_jet), pz(julia_jet), - energy(julia_jet)) - julia_pt = pt(jet_pj) - julia_rap = rapidity(jet_pj) - - # Check if jet is within tolerance - pt_diff = abs(julia_pt - ref_jet["pt"]) - rap_diff = abs(julia_rap - ref_jet["rap"]) - - if pt_diff < pt_tolerance && rap_diff < rap_tolerance - # Use combined distance metric - distance = sqrt((pt_diff / pt_tolerance)^2 + (rap_diff / rap_tolerance)^2) - - if distance < best_distance - best_distance = distance - best_match = (julia_jet, julia_idx) - end - end - end - - if best_match !== nothing - julia_jet, julia_idx = best_match - push!(matched_pairs, (ref_jet, julia_jet, ref_idx, julia_idx)) - push!(used_julia_jets, julia_idx) - end - end - - return matched_pairs -end - -""" - match_jets(ref_jets, julia_jets; pt_tolerance=0.5, rap_tolerance=1.0) - -Match reference jets to Julia jets based on kinematics with enhanced handling for similar pT jets. -""" -function match_jets(ref_jets, julia_jets; pt_tolerance = 0.5, rap_tolerance = 1.0) - return match_jets_with_pairing_optimization(ref_jets, julia_jets; - pt_tolerance = pt_tolerance, - rap_tolerance = rap_tolerance) -end - -@testset "Valencia algorithm basic test" begin - # Test with simple 2-particle system - particles = [ - PseudoJet(1.0, 0.0, 0.0, 1.0), - PseudoJet(0.0, 1.0, 0.0, 1.0) - ] - - # Run Valencia algorithm with test parameters - β = 0.8 - γ = 0.8 - R = 1.2 - - # Basic checks - clusterseq = ee_genkt_algorithm(particles, algorithm = JetAlgorithm.Valencia, p = β, - γ = γ, R = R) - @test clusterseq isa ClusterSequence - - # Test exclusive jets - exclusive_jets_result = exclusive_jets(clusterseq, njets = 1) - @test length(exclusive_jets_result) == 1 - - eventfile = joinpath(@__DIR__, "data", "events.eeH.hepmc3.zst") - if isfile(eventfile) - events = read_final_state_particles(eventfile) - # Load reference data for all events (now using .zst files) - inclusive_ref_file = joinpath(@__DIR__, "data", - "jet-collections-fastjet-valencia-inclusive20-eeH.json.zst") - inclusive_ref_all = read_fastjet_outputs(inclusive_ref_file) - exclusive_ref_file = joinpath(@__DIR__, "data", - "jet-collections-fastjet-valencia-exclusive4-eeH.json.zst") - exclusive_ref_all = read_fastjet_outputs(exclusive_ref_file) - exclusive_d500_ref_file = joinpath(@__DIR__, "data", - "jet-collections-fastjet-valencia-exclusive-d500-eeH.json.zst") - exclusive_d500_ref_all = read_fastjet_outputs(exclusive_d500_ref_file) - - for (evt_idx, event) in enumerate(events) - filtered_event = filter(p -> abs(rapidity(p)) <= 4.0, event) - clusterseq = ee_genkt_algorithm(filtered_event, - algorithm = JetAlgorithm.Valencia, p = 0.8, - γ = 0.8, R = 1.2) - - # Debug Event 13 specifically - if evt_idx == 13 - @info "=== EVENT 13 DEBUG ===" - inclusive_20gev = inclusive_jets(clusterseq, ptmin = 20.0) - inclusive_ref_data = inclusive_ref_all[evt_idx]["jets"] - - @info "Reference jets:" - for (i, ref_jet) in enumerate(inclusive_ref_data) - @info " Jet $i: pT=$(ref_jet["pt"]), rap=$(ref_jet["rap"])" - end - - @info "Julia jets:" - for (i, julia_jet) in enumerate(inclusive_20gev) - jet_pj = PseudoJet(px(julia_jet), py(julia_jet), pz(julia_jet), - energy(julia_jet)) - @info " Jet $i: pT=$(pt(jet_pj)), rap=$(rapidity(jet_pj))" - end - - @info "Matching results:" - matched_pairs = match_jets(inclusive_ref_data, inclusive_20gev; - pt_tolerance = 0.5, rap_tolerance = 2.0) - for (ref_jet, julia_jet, ref_idx, julia_idx) in matched_pairs - jet_pj = PseudoJet(px(julia_jet), py(julia_jet), pz(julia_jet), - energy(julia_jet)) - our_pt = pt(jet_pj) - our_rap = rapidity(jet_pj) - pt_diff = abs(our_pt - ref_jet["pt"]) - rap_diff = abs(our_rap - ref_jet["rap"]) - @info " Ref $ref_idx -> Julia $julia_idx: pT_diff=$pt_diff, rap_diff=$rap_diff" - end - end - - # Inclusive jets (pt > 20 GeV) with jet matching - inclusive_20gev = inclusive_jets(clusterseq, ptmin = 20.0) - inclusive_ref_data = inclusive_ref_all[evt_idx]["jets"] - @info "Event $evt_idx: Inclusive jets (pT > 20 GeV): found $(length(inclusive_20gev)), reference $(length(inclusive_ref_data))" - - # Match jets instead of position-based comparison - matched_pairs = match_jets(inclusive_ref_data, inclusive_20gev; - pt_tolerance = 0.1, rap_tolerance = 2.0) - - # Enhanced test: if there are 2 jets with nearly identical pT, check both rapidity pairings - if length(matched_pairs) == 2 - ref_pts = [matched_pairs[1][1]["pt"], matched_pairs[2][1]["pt"]] - if abs(ref_pts[1] - ref_pts[2]) < 0.01 - # Try both rapidity pairings - jet1_pj = PseudoJet(px(matched_pairs[1][2]), py(matched_pairs[1][2]), - pz(matched_pairs[1][2]), - energy(matched_pairs[1][2])) - jet2_pj = PseudoJet(px(matched_pairs[2][2]), py(matched_pairs[2][2]), - pz(matched_pairs[2][2]), - energy(matched_pairs[2][2])) - rap1 = rapidity(jet1_pj) - rap2 = rapidity(jet2_pj) - ref_rap1 = matched_pairs[1][1]["rap"] - ref_rap2 = matched_pairs[2][1]["rap"] - # Arrangement 1 - arrangement1 = abs(rap1 - ref_rap1) < 0.15 && - abs(rap2 - ref_rap2) < 0.15 - # Arrangement 2 (swapped) - arrangement2 = abs(rap2 - ref_rap1) < 0.15 && - abs(rap1 - ref_rap2) < 0.15 - if !(arrangement1 || arrangement2) - println("Event $evt_idx inclusive jets rapidity pairing failed. Printing jets:") - println("Reference jets:") - for (i, ref_jet) in enumerate(inclusive_ref_data) - println(" Jet $i: pT=$(ref_jet["pt"]), rap=$(ref_jet["rap"])") - end - println("Julia jets:") - for (i, julia_jet) in enumerate(inclusive_20gev) - jet_pj = PseudoJet(px(julia_jet), py(julia_jet), pz(julia_jet), - energy(julia_jet)) - println(" Jet $i: pT=$(pt(jet_pj)), rap=$(rapidity(jet_pj))") - end - end - @test arrangement1 || arrangement2 - # Always test pT agreement - if !(abs(pt(jet1_pj) - matched_pairs[1][1]["pt"]) < 0.1) - println("Event $evt_idx inclusive jets pT test failed for jet 1.") - end - if !(abs(pt(jet2_pj) - matched_pairs[2][1]["pt"]) < 0.1) - println("Event $evt_idx inclusive jets pT test failed for jet 2.") - end - @test abs(pt(jet1_pj) - matched_pairs[1][1]["pt"]) < 0.1 - @test abs(pt(jet2_pj) - matched_pairs[2][1]["pt"]) < 0.1 - else - # Standard test for jets with distinct pT - failed = false - for (ref_jet, julia_jet, ref_idx, julia_idx) in matched_pairs - jet_pj = PseudoJet(px(julia_jet), py(julia_jet), pz(julia_jet), - energy(julia_jet)) - our_pt = pt(jet_pj) - our_rap = rapidity(jet_pj) - if !(abs(our_pt - ref_jet["pt"]) < 0.1) || - !(abs(our_rap - ref_jet["rap"]) < 0.15) - failed = true - end - end - if failed - println("Event $evt_idx inclusive jets kinematic test failed. Printing all jets:") - println("Reference jets:") - for (i, ref_jet) in enumerate(inclusive_ref_data) - println(" Jet $i: pT=$(ref_jet["pt"]), rap=$(ref_jet["rap"])") - end - println("Julia jets:") - for (i, julia_jet) in enumerate(inclusive_20gev) - jet_pj = PseudoJet(px(julia_jet), py(julia_jet), pz(julia_jet), - energy(julia_jet)) - println(" Jet $i: pT=$(pt(jet_pj)), rap=$(rapidity(jet_pj))") - end - end - for (ref_jet, julia_jet, ref_idx, julia_idx) in matched_pairs - jet_pj = PseudoJet(px(julia_jet), py(julia_jet), pz(julia_jet), - energy(julia_jet)) - our_pt = pt(jet_pj) - our_rap = rapidity(jet_pj) - @test abs(our_pt - ref_jet["pt"]) < 0.1 - @test abs(our_rap - ref_jet["rap"]) < 0.15 - end - end - else - # Standard test for other cases - failed = false - for (ref_jet, julia_jet, ref_idx, julia_idx) in matched_pairs - jet_pj = PseudoJet(px(julia_jet), py(julia_jet), pz(julia_jet), - energy(julia_jet)) - our_pt = pt(jet_pj) - our_rap = rapidity(jet_pj) - if !(abs(our_pt - ref_jet["pt"]) < 0.1) || - !(abs(our_rap - ref_jet["rap"]) < 0.15) - failed = true - end - end - if failed - println("Event $evt_idx inclusive jets kinematic test failed. Printing all jets:") - println("Reference jets:") - for (i, ref_jet) in enumerate(inclusive_ref_data) - println(" Jet $i: pT=$(ref_jet["pt"]), rap=$(ref_jet["rap"])") - end - println("Julia jets:") - for (i, julia_jet) in enumerate(inclusive_20gev) - jet_pj = PseudoJet(px(julia_jet), py(julia_jet), pz(julia_jet), - energy(julia_jet)) - println(" Jet $i: pT=$(pt(jet_pj)), rap=$(rapidity(jet_pj))") - end - end - for (ref_jet, julia_jet, ref_idx, julia_idx) in matched_pairs - jet_pj = PseudoJet(px(julia_jet), py(julia_jet), pz(julia_jet), - energy(julia_jet)) - our_pt = pt(jet_pj) - our_rap = rapidity(jet_pj) - @test abs(our_pt - ref_jet["pt"]) < 0.1 - @test abs(our_rap - ref_jet["rap"]) < 0.15 - end - end - - # Exclusive N=4 jets with jet matching - exclusive_4 = exclusive_jets(clusterseq, njets = 4) - exclusive_ref_data = exclusive_ref_all[evt_idx]["jets"] - @info "Event $evt_idx: Exclusive jets (N=4): found $(length(exclusive_4)), reference $(length(exclusive_ref_data))" - - # Match jets - matched_pairs = match_jets(exclusive_ref_data, exclusive_4; pt_tolerance = 0.1, - rap_tolerance = 2.0) - - # Test kinematic agreement for matched jets - for (ref_jet, julia_jet, ref_idx, julia_idx) in matched_pairs - jet_pj = PseudoJet(px(julia_jet), py(julia_jet), pz(julia_jet), - energy(julia_jet)) - our_pt = pt(jet_pj) - our_rap = rapidity(jet_pj) - @test abs(our_pt - ref_jet["pt"]) < 0.1 - @test abs(our_rap - ref_jet["rap"]) < 0.15 - end - - # Exclusive d < 500 jets with jet matching - exclusive_d500 = exclusive_jets(clusterseq, dcut = 500.0) - exclusive_d500_ref_data = exclusive_d500_ref_all[evt_idx]["jets"] - @info "Event $evt_idx: Exclusive jets (d < 500): found $(length(exclusive_d500)), reference $(length(exclusive_d500_ref_data))" - - # Match jets with more lenient tolerances for d-cut jets - matched_pairs = match_jets(exclusive_d500_ref_data, exclusive_d500; - pt_tolerance = 0.1, rap_tolerance = 2.0) - - # Test kinematic agreement for matched jets - for (ref_jet, julia_jet, ref_idx, julia_idx) in matched_pairs - jet_pj = PseudoJet(px(julia_jet), py(julia_jet), pz(julia_jet), - energy(julia_jet)) - our_pt = pt(jet_pj) - our_rap = rapidity(jet_pj) - @test abs(our_pt - ref_jet["pt"]) < 0.1 - @test abs(our_rap - ref_jet["rap"]) < 0.15 - end - end - end -end From ce9964d83dbb18d4be9e482b539e800dcdf87585 Mon Sep 17 00:00:00 2001 From: Matt LeBlanc Date: Mon, 11 Aug 2025 14:59:45 -0400 Subject: [PATCH 03/32] Running julia formatter --- test/_common.jl | 33 +++++++++++------- test/test-valencia.jl | 81 +++++++++++++++++++++++-------------------- 2 files changed, 63 insertions(+), 51 deletions(-) diff --git a/test/_common.jl b/test/_common.jl index 5ac64846..bffc5d8e 100644 --- a/test/_common.jl +++ b/test/_common.jl @@ -58,14 +58,18 @@ struct ComparisonTest end """Constructor where there is no selector_name given""" -function ComparisonTest(events_file, fastjet_outputs, algorithm, strategy, power, R, selector) +function ComparisonTest(events_file, fastjet_outputs, algorithm, strategy, power, R, + selector) selector_name = "" - ComparisonTest(events_file, fastjet_outputs, algorithm, strategy, power, R, selector, selector_name, addjets, nothing) + ComparisonTest(events_file, fastjet_outputs, algorithm, strategy, power, R, selector, + selector_name, addjets, nothing) end """Constructor with no recombine or preprocess specified""" -function ComparisonTest(events_file, fastjet_outputs, algorithm, strategy, power, R, selector, selector_name) - ComparisonTest(events_file, fastjet_outputs, algorithm, strategy, power, R, selector, selector_name, addjets, nothing) +function ComparisonTest(events_file, fastjet_outputs, algorithm, strategy, power, R, + selector, selector_name) + ComparisonTest(events_file, fastjet_outputs, algorithm, strategy, power, R, selector, + selector_name, addjets, nothing) end """ @@ -87,11 +91,12 @@ struct ComparisonTestValencia preprocess::Any end -function ComparisonTestValencia(events_file, fastjet_outputs, algorithm, strategy, power, R, γ, selector, selector_name) - ComparisonTestValencia(events_file, fastjet_outputs, algorithm, strategy, power, R, γ, selector, selector_name, addjets, nothing) +function ComparisonTestValencia(events_file, fastjet_outputs, algorithm, strategy, power, R, + γ, selector, selector_name) + ComparisonTestValencia(events_file, fastjet_outputs, algorithm, strategy, power, R, γ, + selector, selector_name, addjets, nothing) end - """Read JSON file with fastjet jets in it""" function read_fastjet_outputs(fname) f = JetReconstruction.open_with_stream(fname) @@ -129,7 +134,8 @@ function run_reco_test(test::ComparisonTest; testname = nothing) for (ievent, event) in enumerate(events) if test.algorithm == JetAlgorithm.Valencia # For VLC: pass both beta (power) and γ - cluster_seq = JetReconstruction.jet_reconstruct(event; R = test.R, p = test.power, + cluster_seq = JetReconstruction.jet_reconstruct(event; R = test.R, + p = test.power, γ = getfield(test, :γ), algorithm = test.algorithm, strategy = test.strategy, @@ -137,7 +143,8 @@ function run_reco_test(test::ComparisonTest; testname = nothing) preprocess = test.preprocess) else # All other algorithms - cluster_seq = JetReconstruction.jet_reconstruct(event; R = test.R, p = test.power, + cluster_seq = JetReconstruction.jet_reconstruct(event; R = test.R, + p = test.power, algorithm = test.algorithm, strategy = test.strategy, recombine = test.recombine, @@ -244,9 +251,9 @@ function run_reco_test(test::ComparisonTestValencia; testname = nothing) phi_ref = rjet["phi"] pt_ref = rjet["pt"] normalised_phi_ref = phi_ref < 0.0 ? phi_ref + 2π : phi_ref - rap_test = isapprox(jet.rap, rap_ref; atol=2e-1) - phi_test = isapprox(norm_phi, normalised_phi_ref; atol=2e-1) - pt_test = isapprox(jet.pt, pt_ref; rtol=2e0) + rap_test = isapprox(jet.rap, rap_ref; atol = 2e-1) + phi_test = isapprox(norm_phi, normalised_phi_ref; atol = 2e-1) + pt_test = isapprox(jet.pt, pt_ref; rtol = 2e0) if !rap_test || !phi_test || !pt_test println("Jet mismatch in Event $(ievt), Jet $(ijet):") println(" Failing Jet: pt=$(jet.pt), rap=$(jet.rap), phi=$(norm_phi)") @@ -269,4 +276,4 @@ function run_reco_test(test::ComparisonTestValencia; testname = nothing) end end end -end \ No newline at end of file +end diff --git a/test/test-valencia.jl b/test/test-valencia.jl index 309150fe..3a2fa6d7 100644 --- a/test/test-valencia.jl +++ b/test/test-valencia.jl @@ -15,52 +15,57 @@ using Test using JetReconstruction # Reference outputs for R=0.8, beta=1.2, gamma=1.2 -valencia_inclusive = ComparisonTestValencia( - events_file_ee, - joinpath(@__DIR__, "data", "jet-collections-fastjet-valencia-inclusive20-beta1.2-gamma1.2-R0.8-eeH.json.zst"), - JetAlgorithm.Valencia, RecoStrategy.N2Plain, 1.2, 0.8, 1.2, - cs -> inclusive_jets(cs; ptmin = 20.0), "inclusive beta=1.2 gamma=1.2 R=0.8" -) +valencia_inclusive = ComparisonTestValencia(events_file_ee, + joinpath(@__DIR__, "data", + "jet-collections-fastjet-valencia-inclusive20-beta1.2-gamma1.2-R0.8-eeH.json.zst"), + JetAlgorithm.Valencia, RecoStrategy.N2Plain, + 1.2, 0.8, 1.2, + cs -> inclusive_jets(cs; ptmin = 20.0), + "inclusive beta=1.2 gamma=1.2 R=0.8") run_reco_test(valencia_inclusive) -valencia_exclusive4 = ComparisonTestValencia( - events_file_ee, - joinpath(@__DIR__, "data", "jet-collections-fastjet-valencia-exclusive4-beta1.2-gamma1.2-R0.8-eeH.json.zst"), - JetAlgorithm.Valencia, RecoStrategy.N2Plain, 1.2, 0.8, 1.2, - cs -> exclusive_jets(cs; njets = 4), "exclusive njets beta=1.2 gamma=1.2 R=0.8" -) +valencia_exclusive4 = ComparisonTestValencia(events_file_ee, + joinpath(@__DIR__, "data", + "jet-collections-fastjet-valencia-exclusive4-beta1.2-gamma1.2-R0.8-eeH.json.zst"), + JetAlgorithm.Valencia, RecoStrategy.N2Plain, + 1.2, 0.8, 1.2, + cs -> exclusive_jets(cs; njets = 4), + "exclusive njets beta=1.2 gamma=1.2 R=0.8") run_reco_test(valencia_exclusive4) -valencia_exclusive_d500 = ComparisonTestValencia( - events_file_ee, - joinpath(@__DIR__, "data", "jet-collections-fastjet-valencia-exclusive-d500-beta1.2-gamma1.2-R0.8-eeH.json.zst"), - JetAlgorithm.Valencia, RecoStrategy.N2Plain, 1.2, 0.8, 1.2, - cs -> exclusive_jets(cs; dcut = 500.0), "exclusive dcut=500 beta=1.2 gamma=1.2 R=0.8" -) +valencia_exclusive_d500 = ComparisonTestValencia(events_file_ee, + joinpath(@__DIR__, "data", + "jet-collections-fastjet-valencia-exclusive-d500-beta1.2-gamma1.2-R0.8-eeH.json.zst"), + JetAlgorithm.Valencia, + RecoStrategy.N2Plain, 1.2, 0.8, 1.2, + cs -> exclusive_jets(cs; dcut = 500.0), + "exclusive dcut=500 beta=1.2 gamma=1.2 R=0.8") run_reco_test(valencia_exclusive_d500) -Reference outputs for R=1.0, beta=1.0, gamma=1.0 -valencia_inclusive_b1g1 = ComparisonTestValencia( - events_file_ee, - joinpath(@__DIR__, "data", "jet-collections-fastjet-valencia-inclusive20-beta1.0-gamma1.0-R1.0-eeH.json.zst"), - JetAlgorithm.Valencia, RecoStrategy.N2Plain, 1.0, 1.0, 1.0, - cs -> inclusive_jets(cs; ptmin = 20.0), "inclusive beta=1.0 gamma=1.0 R=1.0" -) +#Reference outputs for R=1.0, beta=1.0, gamma=1.0 +valencia_inclusive_b1g1 = ComparisonTestValencia(events_file_ee, + joinpath(@__DIR__, "data", + "jet-collections-fastjet-valencia-inclusive20-beta1.0-gamma1.0-R1.0-eeH.json.zst"), + JetAlgorithm.Valencia, + RecoStrategy.N2Plain, 1.0, 1.0, 1.0, + cs -> inclusive_jets(cs; ptmin = 20.0), + "inclusive beta=1.0 gamma=1.0 R=1.0") run_reco_test(valencia_inclusive_b1g1) -valencia_exclusive4_b1g1 = ComparisonTestValencia( - events_file_ee, - joinpath(@__DIR__, "data", "jet-collections-fastjet-valencia-exclusive4-beta1.0-gamma1.0-R1.0-eeH.json.zst"), - JetAlgorithm.Valencia, RecoStrategy.N2Plain, 1.0, 1.0, 1.0, - cs -> exclusive_jets(cs; njets = 4), "exclusive njets beta=1.0 gamma=1.0 R=1.0" -) +valencia_exclusive4_b1g1 = ComparisonTestValencia(events_file_ee, + joinpath(@__DIR__, "data", + "jet-collections-fastjet-valencia-exclusive4-beta1.0-gamma1.0-R1.0-eeH.json.zst"), + JetAlgorithm.Valencia, + RecoStrategy.N2Plain, 1.0, 1.0, 1.0, + cs -> exclusive_jets(cs; njets = 4), + "exclusive njets beta=1.0 gamma=1.0 R=1.0") run_reco_test(valencia_exclusive4_b1g1) -valencia_exclusive_d500_b1g1 = ComparisonTestValencia( - events_file_ee, - joinpath(@__DIR__, "data", "jet-collections-fastjet-valencia-exclusive-d500-beta1.0-gamma1.0-R1.0-eeH.json.zst"), - JetAlgorithm.Valencia, RecoStrategy.N2Plain, 1.0, 1.0, 1.0, - cs -> exclusive_jets(cs; dcut = 500.0), "exclusive dcut=500 beta=1.0 gamma=1.0 R=1.0" -) +valencia_exclusive_d500_b1g1 = ComparisonTestValencia(events_file_ee, + joinpath(@__DIR__, "data", + "jet-collections-fastjet-valencia-exclusive-d500-beta1.0-gamma1.0-R1.0-eeH.json.zst"), + JetAlgorithm.Valencia, + RecoStrategy.N2Plain, 1.0, 1.0, 1.0, + cs -> exclusive_jets(cs; dcut = 500.0), + "exclusive dcut=500 beta=1.0 gamma=1.0 R=1.0") run_reco_test(valencia_exclusive_d500_b1g1) - From 2a9d379eba60d339c4bdfded7202691286e2103c Mon Sep 17 00:00:00 2001 From: Matt LeBlanc Date: Tue, 12 Aug 2025 19:59:21 -0400 Subject: [PATCH 04/32] Fixing beam distance and increasing numerical precision of reference -- VLC tests pass now with higher precision --- src/EEAlgorithm.jl | 86 +++++++++--------- test/_common.jl | 6 +- ...ve-d500-beta1.0-gamma1.0-R1.0-eeH.json.zst | Bin 3441 -> 5262 bytes ...ve-d500-beta1.2-gamma1.2-R0.8-eeH.json.zst | Bin 3876 -> 6212 bytes ...lusive4-beta1.0-gamma1.0-R1.0-eeH.json.zst | Bin 5434 -> 9016 bytes ...lusive4-beta1.2-gamma1.2-R0.8-eeH.json.zst | Bin 5441 -> 9028 bytes ...usive20-beta1.2-gamma1.2-R0.8-eeH.json.zst | Bin 3266 -> 5179 bytes 7 files changed, 45 insertions(+), 47 deletions(-) diff --git a/src/EEAlgorithm.jl b/src/EEAlgorithm.jl index e03823b8..f6ef8bd1 100644 --- a/src/EEAlgorithm.jl +++ b/src/EEAlgorithm.jl @@ -45,36 +45,27 @@ Base.@propagate_inbounds function valencia_distance(eereco, i, j, R) end """ - valencia_beam_distance(eereco, i, γ) -> Float64 + valencia_beam_distance(eereco, i, γ, β) -> Float64 -Calculate the Valencia beam distance for jet `i` using the formula -``E_i^{2β} * sin(θ_i)^{2γ}``, where `sin(θ_i) = pt / sqrt(pt^2 + pz^2)`. -This matches the FastJet contrib::ValenciaPlugin implementation. +Calculate the Valencia beam distance for jet `i` using the FastJet ValenciaPlugin +definition: ``d_iB = E_i^{2β} * (sin θ_i)^{2γ}``, where ``cos θ_i = nz`` +for unit direction cosines. Since ``sin^2 θ = 1 - nz^2``, we implement +``d_iB = E_i^{2β} * (1 - nz^2)^γ``. # Arguments - `eereco`: The array of `EERecoJet` objects. - `i`: The jet index. - `γ`: The angular exponent parameter used in the Valencia beam distance. +- `β`: The energy exponent (same as `p` in our implementation). # Returns - `Float64`: The Valencia beam distance for jet `i`. - -# Details -The Valencia beam distance is used in the Valencia jet algorithm for e⁺e⁻ collisions. -It generalizes the beam distance by including an angular exponent γ, allowing for -flexible jet finding. The formula is: - - d_beam = E_i^{2β} * [pt / sqrt(pt^2 + pz^2)]^{2γ} - -where β is the energy exponent (typically set via the algorithm parameters). """ @inline function valencia_beam_distance(eereco, i, γ, β) - # Since nx, ny, nz are normalized direction vectors (px/E, py/E, pz/E), - # sin(θ) = pt/sqrt(pt^2 + pz^2) = sqrt(nx^2 + ny^2) - nx = eereco[i].nx - ny = eereco[i].ny - sin_theta = sqrt(nx^2 + ny^2) - @inbounds eereco[i].E2p * sin_theta^(2γ) + nz = @inbounds eereco[i].nz + # sin^2(theta) = 1 - nz^2; beam distance independent of R + sin2 = 1 - nz * nz + @inbounds eereco[i].E2p * sin2^γ end """ @@ -108,24 +99,27 @@ end function get_angular_nearest_neighbours!(eereco, algorithm, dij_factor, p, γ = 1.0, R = 4.0) # Get the initial nearest neighbours for each jet N = length(eereco) - # this_dist_vector = Vector{Float64}(undef, N) - # Nearest neighbour geometric distance + # For Valencia, nearest-neighbour must be chosen on the full dij metric (FastJet NNH behaviour) + if algorithm == JetAlgorithm.Valencia + @inbounds for i in 1:N + eereco.nndist[i] = Inf + eereco.nni[i] = i + end + end + # Nearest neighbour search @inbounds for i in 1:N - # TODO: Replace the 'j' loop with a vectorised operation over the appropriate array elements? - # this_dist_vector .= 1.0 .- eereco.nx[i:N] .* eereco[i + 1:end].nx .- - # eereco[i].ny .* eereco[i + 1:end].ny .- eereco[i].nz .* eereco[i + 1:end].nz - # The problem here will be avoiding allocations for the array outputs, which would easily - # kill performance @inbounds for j in (i + 1):N - # Always use angular distance for nearest neighbor search - this_nndist = angular_distance(eereco, i, j) + # Metric used to pick the nearest neighbour + this_metric = algorithm == JetAlgorithm.Valencia ? + valencia_distance(eereco, i, j, R) : + angular_distance(eereco, i, j) # Using these ternary operators is faster than the if-else block - better_nndist_i = this_nndist < eereco[i].nndist - eereco.nndist[i] = better_nndist_i ? this_nndist : eereco.nndist[i] + better_nndist_i = this_metric < eereco[i].nndist + eereco.nndist[i] = better_nndist_i ? this_metric : eereco.nndist[i] eereco.nni[i] = better_nndist_i ? j : eereco.nni[i] - better_nndist_j = this_nndist < eereco[j].nndist - eereco.nndist[j] = better_nndist_j ? this_nndist : eereco.nndist[j] + better_nndist_j = this_metric < eereco[j].nndist + eereco.nndist[j] = better_nndist_j ? this_metric : eereco.nndist[j] eereco.nni[j] = better_nndist_j ? i : eereco.nni[j] end end @@ -157,14 +151,16 @@ end # Update the nearest neighbour for jet i, w.r.t. all other active jets function update_nn_no_cross!(eereco, i, N, algorithm, dij_factor, β = 1.0, γ = 1.0, R = 4.0) - eereco.nndist[i] = large_distance + eereco.nndist[i] = algorithm == JetAlgorithm.Valencia ? Inf : large_distance eereco.nni[i] = i @inbounds for j in 1:N if j != i - # Always use angular distance for nearest neighbor search - this_nndist = angular_distance(eereco, i, j) - better_nndist_i = this_nndist < eereco[i].nndist - eereco.nndist[i] = better_nndist_i ? this_nndist : eereco.nndist[i] + # Metric for nearest neighbour selection + this_metric = algorithm == JetAlgorithm.Valencia ? + valencia_distance(eereco, i, j, R) : + angular_distance(eereco, i, j) + better_nndist_i = this_metric < eereco[i].nndist + eereco.nndist[i] = better_nndist_i ? this_metric : eereco.nndist[i] eereco.nni[i] = better_nndist_i ? j : eereco.nni[i] end end @@ -188,17 +184,19 @@ end function update_nn_cross!(eereco, i, N, algorithm, dij_factor, β = 1.0, γ = 1.0, R = 4.0) # Update the nearest neighbour for jet i, w.r.t. all other active jets # also doing the cross check for the other jet - eereco.nndist[i] = large_distance + eereco.nndist[i] = algorithm == JetAlgorithm.Valencia ? Inf : large_distance eereco.nni[i] = i @inbounds for j in 1:N if j != i - # Always use angular distance for nearest neighbor search - this_nndist = angular_distance(eereco, i, j) - better_nndist_i = this_nndist < eereco[i].nndist - eereco.nndist[i] = better_nndist_i ? this_nndist : eereco.nndist[i] + # Metric for nearest neighbour selection + this_metric = algorithm == JetAlgorithm.Valencia ? + valencia_distance(eereco, i, j, R) : + angular_distance(eereco, i, j) + better_nndist_i = this_metric < eereco[i].nndist + eereco.nndist[i] = better_nndist_i ? this_metric : eereco.nndist[i] eereco.nni[i] = better_nndist_i ? j : eereco.nni[i] - if this_nndist < eereco[j].nndist - eereco.nndist[j] = this_nndist + if this_metric < eereco[j].nndist + eereco.nndist[j] = this_metric eereco.nni[j] = i # j will not be revisited, so update metric distance here if algorithm == JetAlgorithm.Valencia diff --git a/test/_common.jl b/test/_common.jl index bffc5d8e..94d4cbe4 100644 --- a/test/_common.jl +++ b/test/_common.jl @@ -251,9 +251,9 @@ function run_reco_test(test::ComparisonTestValencia; testname = nothing) phi_ref = rjet["phi"] pt_ref = rjet["pt"] normalised_phi_ref = phi_ref < 0.0 ? phi_ref + 2π : phi_ref - rap_test = isapprox(jet.rap, rap_ref; atol = 2e-1) - phi_test = isapprox(norm_phi, normalised_phi_ref; atol = 2e-1) - pt_test = isapprox(jet.pt, pt_ref; rtol = 2e0) + rap_test = isapprox(jet.rap, rap_ref; atol=1e-4) + phi_test = isapprox(norm_phi, normalised_phi_ref;atol=1e-4) + pt_test = isapprox(jet.pt, pt_ref; rtol=1e-5) if !rap_test || !phi_test || !pt_test println("Jet mismatch in Event $(ievt), Jet $(ijet):") println(" Failing Jet: pt=$(jet.pt), rap=$(jet.rap), phi=$(norm_phi)") diff --git a/test/data/jet-collections-fastjet-valencia-exclusive-d500-beta1.0-gamma1.0-R1.0-eeH.json.zst b/test/data/jet-collections-fastjet-valencia-exclusive-d500-beta1.0-gamma1.0-R1.0-eeH.json.zst index b69fc0acfb0e10c3ec32e53b4d5586e2386cd422..e366cf135adea59fe38b2e157a8ba2389363dbb3 100644 GIT binary patch literal 5262 zcmV;96mjb)wJ-f-i%12e0IIIhF&uy?m2?{Qh{fSROC-z&wNlC?Q<5ofg)I_8oDenx z2?GZM6$(NP&J0Oo0{<9^iRBw&D0!BJqGmy2=5;svnHDydd1ouZv`@*#QH`&C7>DDf zs}Vx%c2g1<)XCZ21HH{7BSBWH#TgVx^5>Pq%i#D<3?ta_#=e>)hMXmkbka=Ez}HBv z5kg~$CXfsY^B*UHa(PL|FrUILhSxo{>1M)-a;RbLhK}sWWUeL)|=( zOQc#gvIuSUcW-)CdRanI?nj8fusi4?nWv&rki;RS8-$j*qmrp&2x3re>+9|zf>yLT*J7><1G~-NKAEIGAdS5Ca3tv`Qk7 zXC5J`g|e)>%AW0P_;7OKLUM70lc(H>hOvffp9v=`9n(S{COt| zxx9_QpmuXg)8L1M#7%#271fv;7&z=GwV?~UCN{D8>Z=t1N|dgj7{B9i&+GH}2GHDW_v31I+%0|Xd=0tXBnP~I@MSeO9>4k#di0~|1L zz-z9~Iw3-V0}B9v0R#>pAOQ&!-~bvx;6MTlkbnXbIAFkl0Rsjsz<>b*01y;(8i&cH zA*WDcS&f*5$3k8X^L({6$Z&1M0^Oo0i^J;C7d3|9yakEFf+3k7_-;ef)lxc(EbuExYC?RIt$wMJ7T}j31z_DW!GjbB5!yv!Hf++f^ zJVht;|5nu~ zQiz5(#7!%K9ijLma40D3i*KZnU`}L6jzA9er34b1h&Dlo#mcRD-l#H0p+z(@JrerT zQJrxVJJrCNX^&&7rP|2kw3rIyn3`B@;fu0;O;v&#PE|w_saK%utBG2fH3rk5(iD7wiU=2DnmA%>>G2{)9u;_36+%t&;kvJi4s1tl!Js6CMT zx~3SUD58Si)}27(W2(`$LU`_n!h#`*=ZfGFayS(+iLb>4l8VAyWG-SPpD`B}E8x6N>3fDA@)V6c$vyeS{dM?n!#)K}MxTVzt@1sz5XIcq}1C zE;Dlky2dDag}OoEBqYS5BzpscpqUe`AZiA%?QFl$Y@bJv3M41MWOrVm|ILvtal~g)u z@JOejrb|qQ4cDMgxJ#|i$)(_rtmQD8acL-gl#!ZOIe{zD!%0|B7fpF##+pa3B#`9g zv>FzWQKn0naXQq6DR`7ACxkfj5lSH@OEnxkZ)h*@AMyWiiWA*)hnl3dv%uiudY>?# zR7e~StC^A7Bd}~+HDRBKQ-dcpu=QqMGDH4oI4xx4Xo6`z3kj<`CvvdRLbF8}Oz%i| zfkD40*Bst(g|%P=St}EAk_ahQFhtDb=V=XWMVz+I*HMIqjJzQsVmJxi@;Sz&P#6|w zPc4Q9X?RzJ$waUeF~M{ERgQ`Vb~urmu%Ms%t0)MKOg;@B*8NQwy+i_Yb|QG}^n4Ja zM}#$Id{G#3d<=7n78(tX)wL`$*_P%CVoqZV3e>rZv+mxi(pu^WGb8z zA&O$S$kcAJ7Hdz#x+9CBo2o`Vh8|+3>UvEOjlW1xiC-≪9k7@H^~HQY*^ez_Hp8 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 UM!49H7Sg6Hqdy9kHVQdA%YMCifdBvi literal 3441 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 literal 3876 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 literal 5441 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 diff --git a/test/data/jet-collections-fastjet-valencia-inclusive20-beta1.2-gamma1.2-R0.8-eeH.json.zst b/test/data/jet-collections-fastjet-valencia-inclusive20-beta1.2-gamma1.2-R0.8-eeH.json.zst index 84f125168aa4d7c8a380aedcc7f7dc7c4f3c5b7f..871a1793c3a1ddc0fdb00246cab8e3a0ee1aa294 100644 GIT binary patch literal 5179 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 literal 3266 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 Ah5!Hn From fddedf26150069d89752cfa2f9fc0547bab0b428 Mon Sep 17 00:00:00 2001 From: Matt LeBlanc Date: Tue, 12 Aug 2025 20:00:17 -0400 Subject: [PATCH 05/32] Julia formatter --- test/_common.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/_common.jl b/test/_common.jl index 94d4cbe4..cad44b71 100644 --- a/test/_common.jl +++ b/test/_common.jl @@ -251,9 +251,9 @@ function run_reco_test(test::ComparisonTestValencia; testname = nothing) phi_ref = rjet["phi"] pt_ref = rjet["pt"] normalised_phi_ref = phi_ref < 0.0 ? phi_ref + 2π : phi_ref - rap_test = isapprox(jet.rap, rap_ref; atol=1e-4) - phi_test = isapprox(norm_phi, normalised_phi_ref;atol=1e-4) - pt_test = isapprox(jet.pt, pt_ref; rtol=1e-5) + rap_test = isapprox(jet.rap, rap_ref; atol = 1e-4) + phi_test = isapprox(norm_phi, normalised_phi_ref; atol = 1e-4) + pt_test = isapprox(jet.pt, pt_ref; rtol = 1e-5) if !rap_test || !phi_test || !pt_test println("Jet mismatch in Event $(ievt), Jet $(ijet):") println(" Failing Jet: pt=$(jet.pt), rap=$(jet.rap), phi=$(norm_phi)") From fcdfc13eae7064abd1cec607a889ecaa42fb1273 Mon Sep 17 00:00:00 2001 From: Matt LeBlanc Date: Tue, 12 Aug 2025 22:07:41 -0400 Subject: [PATCH 06/32] Missing coverity --- test/test-valencia.jl | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/test/test-valencia.jl b/test/test-valencia.jl index 3a2fa6d7..6d46e98f 100644 --- a/test/test-valencia.jl +++ b/test/test-valencia.jl @@ -7,12 +7,7 @@ Validates clustering results and cluster sequence against FastJet C++ reference using jet matching to handle numerical precision differences. """ -# Include common test utilities include("common.jl") -import JetReconstruction: pt, rapidity, PseudoJet - -using Test -using JetReconstruction # Reference outputs for R=0.8, beta=1.2, gamma=1.2 valencia_inclusive = ComparisonTestValencia(events_file_ee, @@ -69,3 +64,34 @@ valencia_exclusive_d500_b1g1 = ComparisonTestValencia(events_file_ee, cs -> exclusive_jets(cs; dcut = 500.0), "exclusive dcut=500 beta=1.0 gamma=1.0 R=1.0") run_reco_test(valencia_exclusive_d500_b1g1) + +# Bring required types/functions into scope for targeted unit tests +using JetReconstruction: EERecoJet, PseudoJet, EEJet, ee_genkt_algorithm, dij_dist, + valencia_distance + +# Test dij_dist for Valencia algorithm +@testset "dij_dist Valencia" begin + # Minimal eereco with two reco jets; only nx,ny,nz,E2p are used by valencia_distance + eereco = EERecoJet[EERecoJet(1, 0, Inf, Inf, 1.0, 0.0, 0.0, 1.0), + EERecoJet(2, 0, Inf, Inf, 0.0, 1.0, 0.0, 2.0)] + dij = dij_dist(eereco, 1, 2, 1.0, JetAlgorithm.Valencia, 0.8) + @test dij ≈ valencia_distance(eereco, 1, 2, 0.8) +end + +# Test ee_genkt_algorithm for Valencia algorithm (covers β override and dij_factor selection) +@testset "ee_genkt_algorithm Valencia" begin + particles = [PseudoJet(1.0, 0.0, 0.0, 1.0)] + cs = ee_genkt_algorithm(particles; algorithm = JetAlgorithm.Valencia, β = 1.2, γ = 1.2, + R = 0.8) + @test cs isa JetReconstruction.ClusterSequence +end + +# Test internal _ee_genkt_algorithm entry (touches StructArray init path too) +@testset "_ee_genkt_algorithm Valencia dij_factor" begin + # Ensure cluster_hist_index(i) == i for initial jets + jets = [EEJet(1.0, 0.0, 0.0, 1.0; cluster_hist_index = 1)] + cs = JetReconstruction._ee_genkt_algorithm(particles = jets, + algorithm = JetAlgorithm.Valencia, p = 1.2, + R = 0.8, γ = 1.2) + @test cs isa JetReconstruction.ClusterSequence +end From 251077e4a47b8ed25a0695134747293ac6323eb5 Mon Sep 17 00:00:00 2001 From: Matt LeBlanc Date: Tue, 12 Aug 2025 23:40:13 -0400 Subject: [PATCH 07/32] Comments from Jerry, try to fix branching --- src/EEAlgorithm.jl | 67 +++++++++++++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 25 deletions(-) diff --git a/src/EEAlgorithm.jl b/src/EEAlgorithm.jl index f6ef8bd1..fc437faa 100644 --- a/src/EEAlgorithm.jl +++ b/src/EEAlgorithm.jl @@ -61,39 +61,56 @@ for unit direction cosines. Since ``sin^2 θ = 1 - nz^2``, we implement # Returns - `Float64`: The Valencia beam distance for jet `i`. """ -@inline function valencia_beam_distance(eereco, i, γ, β) +Base.@propagate_inbounds function valencia_beam_distance(eereco, i, γ, β) nz = @inbounds eereco[i].nz # sin^2(theta) = 1 - nz^2; beam distance independent of R sin2 = 1 - nz * nz - @inbounds eereco[i].E2p * sin2^γ + return eereco[i].E2p * sin2^γ end """ - dij_dist(eereco, i, j, dij_factor, algorithm = JetAlgorithm.Durham, R = 4.0) + dij_dist(eereco, i, j, dij_factor, algorithm::JetAlgorithm.Algorithm, R = 4.0) -Calculate the dij distance between two ``e^+e^-``jets. +Calculate the dij distance between two e⁺e⁻ jets. This is the public entry point. +Internally, this forwards to a Val-based method for the given algorithm, which +allows the compiler to specialize away branches when `algorithm` is a constant. +""" +@inline function dij_dist(eereco, i, j, dij_factor, algorithm::JetAlgorithm.Algorithm, + R = 4.0) + return dij_dist(eereco, i, j, dij_factor, Val(algorithm), R) +end -# Arguments -- `eereco`: The array of `EERecoJet` objects. -- `i`: The first jet. -- `j`: The second jet. -- `dij_factor`: The scaling factor to multiply the dij distance by. -- `algorithm`: The jet algorithm being used. -- `R`: the radius or resolution parameter +""" + dij_dist(eereco, i, j, dij_factor, ::Val{JetAlgorithm.Durham}, R = 4.0) + dij_dist(eereco, i, j, dij_factor, ::Val{JetAlgorithm.EEKt}, R = 4.0) -# Returns -- The dij distance between `i` and `j`. +Durham/EEKt dij distance: +min(E_i^{2p}, E_j^{2p}) * dij_factor * (angular NN metric stored in nndist). +For EEKt, dij_factor encodes the R-dependent normalization. """ -@inline function dij_dist(eereco, i, j, dij_factor, algorithm = JetAlgorithm.Durham, - R = 4.0) - # Calculate the dij distance for jet i from jet j +@inline function dij_dist(eereco, i, j, dij_factor, ::Val{JetAlgorithm.Durham}, R = 4.0) j == 0 && return large_dij + @inbounds min(eereco[i].E2p, eereco[j].E2p) * dij_factor * eereco[i].nndist +end - if algorithm == JetAlgorithm.Valencia - @inbounds valencia_distance(eereco, i, j, R) - else - @inbounds min(eereco[i].E2p, eereco[j].E2p) * dij_factor * eereco[i].nndist - end +@inline function dij_dist(eereco, i, j, dij_factor, ::Val{JetAlgorithm.EEKt}, R = 4.0) + j == 0 && return large_dij + @inbounds min(eereco[i].E2p, eereco[j].E2p) * dij_factor * eereco[i].nndist +end + +""" + dij_dist(eereco, i, j, dij_factor, ::Val{JetAlgorithm.Valencia}, R) + +Valencia dij distance uses the full Valencia metric, including the 2*(1-cosθ)/R² factor. +""" +@inline function dij_dist(eereco, i, j, dij_factor, ::Val{JetAlgorithm.Valencia}, R) + j == 0 && return large_dij + @inbounds valencia_distance(eereco, i, j, R) +end + +# Fallback if a non-Algorithm token is passed +@inline function dij_dist(eereco, i, j, dij_factor, algorithm, R = 4.0) + throw(ArgumentError("Algorithm $algorithm not supported for dij_dist")) end function get_angular_nearest_neighbours!(eereco, algorithm, dij_factor, p, γ = 1.0, R = 4.0) @@ -141,7 +158,7 @@ function get_angular_nearest_neighbours!(eereco, algorithm, dij_factor, p, γ = end elseif algorithm == JetAlgorithm.Valencia @inbounds for i in 1:N - valencia_beam_dist = valencia_beam_distance(eereco, i, γ, p) + valencia_beam_dist = @inbounds valencia_beam_distance(eereco, i, γ, p) beam_closer = valencia_beam_dist < eereco[i].dijdist eereco.dijdist[i] = beam_closer ? valencia_beam_dist : eereco.dijdist[i] eereco.nni[i] = beam_closer ? 0 : eereco.nni[i] @@ -174,7 +191,7 @@ function update_nn_no_cross!(eereco, i, N, algorithm, dij_factor, β = 1.0, γ = eereco.dijdist[i] = beam_close ? eereco[i].E2p : eereco.dijdist[i] eereco.nni[i] = beam_close ? 0 : eereco.nni[i] elseif algorithm == JetAlgorithm.Valencia - valencia_beam_dist = valencia_beam_distance(eereco, i, γ, β) + valencia_beam_dist = @inbounds valencia_beam_distance(eereco, i, γ, β) beam_close = valencia_beam_dist < eereco[i].dijdist eereco.dijdist[i] = beam_close ? valencia_beam_dist : eereco.dijdist[i] eereco.nni[i] = beam_close ? 0 : eereco.nni[i] @@ -200,7 +217,7 @@ function update_nn_cross!(eereco, i, N, algorithm, dij_factor, β = 1.0, γ = 1. eereco.nni[j] = i # j will not be revisited, so update metric distance here if algorithm == JetAlgorithm.Valencia - eereco.dijdist[j] = @inbounds valencia_distance(eereco, j, i, R) + eereco.dijdist[j] = valencia_distance(eereco, j, i, R) else eereco.dijdist[j] = dij_dist(eereco, j, i, dij_factor, algorithm, R) end @@ -229,7 +246,7 @@ function update_nn_cross!(eereco, i, N, algorithm, dij_factor, β = 1.0, γ = 1. eereco.dijdist[i] = beam_close ? eereco[i].E2p : eereco.dijdist[i] eereco.nni[i] = beam_close ? 0 : eereco.nni[i] elseif algorithm == JetAlgorithm.Valencia - valencia_beam_dist = valencia_beam_distance(eereco, i, γ, β) + valencia_beam_dist = @inbounds valencia_beam_distance(eereco, i, γ, β) beam_close = valencia_beam_dist < eereco[i].dijdist eereco.dijdist[i] = beam_close ? valencia_beam_dist : eereco.dijdist[i] eereco.nni[i] = beam_close ? 0 : eereco.nni[i] From 23530a3cb9a630491e6c29bdc720380b21376911 Mon Sep 17 00:00:00 2001 From: Matt LeBlanc Date: Tue, 12 Aug 2025 23:48:44 -0400 Subject: [PATCH 08/32] Clean up inbounds throughout VLC code --- src/EEAlgorithm.jl | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/EEAlgorithm.jl b/src/EEAlgorithm.jl index fc437faa..81b14ce7 100644 --- a/src/EEAlgorithm.jl +++ b/src/EEAlgorithm.jl @@ -17,9 +17,9 @@ Calculate the angular distance between two jets `i` and `j` using the formula - `Float64`: The angular distance between `i` and `j`, which is ``1 - cos\theta``. """ -@inline function angular_distance(eereco, i, j) - @inbounds @muladd 1.0 - eereco[i].nx * eereco[j].nx - eereco[i].ny * eereco[j].ny - - eereco[i].nz * eereco[j].nz +Base.@propagate_inbounds @inline function angular_distance(eereco, i, j) + @muladd 1.0 - eereco[i].nx * eereco[j].nx - eereco[i].ny * eereco[j].ny - + eereco[i].nz * eereco[j].nz end """ @@ -37,7 +37,7 @@ Calculate the Valencia distance between two jets `i` and `j` as # Returns - `Float64`: The Valencia distance between `i` and `j`. """ -Base.@propagate_inbounds function valencia_distance(eereco, i, j, R) +Base.@propagate_inbounds @inline function valencia_distance(eereco, i, j, R) angular_dist = angular_distance(eereco, i, j) # Valencia dij : min(E_i^{2β}, E_j^{2β}) * 2 * (1 - cos θ) / R² # Note that β plays the role of p in other algorithms, so E2p can be used. @@ -61,11 +61,12 @@ for unit direction cosines. Since ``sin^2 θ = 1 - nz^2``, we implement # Returns - `Float64`: The Valencia beam distance for jet `i`. """ -Base.@propagate_inbounds function valencia_beam_distance(eereco, i, γ, β) - nz = @inbounds eereco[i].nz +Base.@propagate_inbounds @inline function valencia_beam_distance(eereco, i, γ, β) + nz = eereco[i].nz # sin^2(theta) = 1 - nz^2; beam distance independent of R sin2 = 1 - nz * nz - return eereco[i].E2p * sin2^γ + E2p = eereco[i].E2p + return E2p * sin2^γ end """ @@ -141,9 +142,9 @@ function get_angular_nearest_neighbours!(eereco, algorithm, dij_factor, p, γ = end end # Nearest neighbour dij distance - for i in 1:N + @inbounds for i in 1:N if algorithm == JetAlgorithm.Valencia - eereco.dijdist[i] = @inbounds valencia_distance(eereco, i, eereco[i].nni, R) + eereco.dijdist[i] = valencia_distance(eereco, i, eereco[i].nni, R) else eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, algorithm, R) end @@ -158,7 +159,7 @@ function get_angular_nearest_neighbours!(eereco, algorithm, dij_factor, p, γ = end elseif algorithm == JetAlgorithm.Valencia @inbounds for i in 1:N - valencia_beam_dist = @inbounds valencia_beam_distance(eereco, i, γ, p) + valencia_beam_dist = valencia_beam_distance(eereco, i, γ, p) beam_closer = valencia_beam_dist < eereco[i].dijdist eereco.dijdist[i] = beam_closer ? valencia_beam_dist : eereco.dijdist[i] eereco.nni[i] = beam_closer ? 0 : eereco.nni[i] @@ -182,7 +183,7 @@ function update_nn_no_cross!(eereco, i, N, algorithm, dij_factor, β = 1.0, γ = end end if algorithm == JetAlgorithm.Valencia - eereco.dijdist[i] = @inbounds valencia_distance(eereco, i, eereco[i].nni, R) + eereco.dijdist[i] = valencia_distance(eereco, i, eereco[i].nni, R) else eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, algorithm, R) end @@ -191,7 +192,7 @@ function update_nn_no_cross!(eereco, i, N, algorithm, dij_factor, β = 1.0, γ = eereco.dijdist[i] = beam_close ? eereco[i].E2p : eereco.dijdist[i] eereco.nni[i] = beam_close ? 0 : eereco.nni[i] elseif algorithm == JetAlgorithm.Valencia - valencia_beam_dist = @inbounds valencia_beam_distance(eereco, i, γ, β) + valencia_beam_dist = valencia_beam_distance(eereco, i, γ, β) beam_close = valencia_beam_dist < eereco[i].dijdist eereco.dijdist[i] = beam_close ? valencia_beam_dist : eereco.dijdist[i] eereco.nni[i] = beam_close ? 0 : eereco.nni[i] From 57c6b60a18875d994a7572b631b5a859aeb7a3cb Mon Sep 17 00:00:00 2001 From: Matt LeBlanc Date: Wed, 13 Aug 2025 00:11:16 -0400 Subject: [PATCH 09/32] Clean up inbounds throughout EEAlgorithm.jl, switch to Val-based forwarders to avoid branching, fast path when p isa int --- src/EEAlgorithm.jl | 310 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 286 insertions(+), 24 deletions(-) diff --git a/src/EEAlgorithm.jl b/src/EEAlgorithm.jl index 81b14ce7..d7b58a26 100644 --- a/src/EEAlgorithm.jl +++ b/src/EEAlgorithm.jl @@ -37,11 +37,15 @@ Calculate the Valencia distance between two jets `i` and `j` as # Returns - `Float64`: The Valencia distance between `i` and `j`. """ -Base.@propagate_inbounds @inline function valencia_distance(eereco, i, j, R) +Base.@propagate_inbounds @inline function valencia_distance_inv(eereco, i, j, invR2) angular_dist = angular_distance(eereco, i, j) - # Valencia dij : min(E_i^{2β}, E_j^{2β}) * 2 * (1 - cos θ) / R² + # Valencia dij : min(E_i^{2β}, E_j^{2β}) * 2 * (1 - cos θ) * invR2 # Note that β plays the role of p in other algorithms, so E2p can be used. - min(eereco[i].E2p, eereco[j].E2p) * 2 * angular_dist / (R * R) + min(eereco[i].E2p, eereco[j].E2p) * 2 * angular_dist * invR2 +end + +Base.@propagate_inbounds @inline function valencia_distance(eereco, i, j, R) + return valencia_distance_inv(eereco, i, j, inv(R * R)) end """ @@ -167,6 +171,90 @@ function get_angular_nearest_neighbours!(eereco, algorithm, dij_factor, p, γ = end end +# Val-specialized nearest neighbour search (removes runtime branches in hot loops) +@inline function get_angular_nearest_neighbours!(eereco, ::Val{JetAlgorithm.Durham}, + dij_factor, p, γ = 1.0, R = 4.0) + N = length(eereco) + # Nearest neighbour search using angular metric + @inbounds for i in 1:N + @inbounds for j in (i + 1):N + this_metric = angular_distance(eereco, i, j) + better_nndist_i = this_metric < eereco[i].nndist + eereco.nndist[i] = better_nndist_i ? this_metric : eereco.nndist[i] + eereco.nni[i] = better_nndist_i ? j : eereco.nni[i] + better_nndist_j = this_metric < eereco[j].nndist + eereco.nndist[j] = better_nndist_j ? this_metric : eereco.nndist[j] + eereco.nni[j] = better_nndist_j ? i : eereco.nni[j] + end + end + @inbounds for i in 1:N + eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, + Val(JetAlgorithm.Durham), R) + end +end + +@inline function get_angular_nearest_neighbours!(eereco, ::Val{JetAlgorithm.EEKt}, + dij_factor, p, γ = 1.0, R = 4.0) + N = length(eereco) + @inbounds for i in 1:N + @inbounds for j in (i + 1):N + this_metric = angular_distance(eereco, i, j) + better_nndist_i = this_metric < eereco[i].nndist + eereco.nndist[i] = better_nndist_i ? this_metric : eereco.nndist[i] + eereco.nni[i] = better_nndist_i ? j : eereco.nni[i] + better_nndist_j = this_metric < eereco[j].nndist + eereco.nndist[j] = better_nndist_j ? this_metric : eereco.nndist[j] + eereco.nni[j] = better_nndist_j ? i : eereco.nni[j] + end + end + @inbounds for i in 1:N + eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, + Val(JetAlgorithm.EEKt), R) + end + @inbounds for i in 1:N + beam_closer = eereco[i].E2p < eereco[i].dijdist + eereco.dijdist[i] = beam_closer ? eereco[i].E2p : eereco.dijdist[i] + eereco.nni[i] = beam_closer ? 0 : eereco.nni[i] + end +end + +@inline function get_angular_nearest_neighbours!(eereco, ::Val{JetAlgorithm.Valencia}, + dij_factor, p, γ = 1.0, R = 4.0) + N = length(eereco) + invR2 = inv(R * R) + @inbounds for i in 1:N + eereco.nndist[i] = Inf + eereco.nni[i] = i + end + @inbounds for i in 1:N + @inbounds for j in (i + 1):N + this_metric = valencia_distance_inv(eereco, i, j, invR2) + better_nndist_i = this_metric < eereco[i].nndist + eereco.nndist[i] = better_nndist_i ? this_metric : eereco.nndist[i] + eereco.nni[i] = better_nndist_i ? j : eereco.nni[i] + better_nndist_j = this_metric < eereco[j].nndist + eereco.nndist[j] = better_nndist_j ? this_metric : eereco.nndist[j] + eereco.nni[j] = better_nndist_j ? i : eereco.nni[j] + end + end + @inbounds for i in 1:N + eereco.dijdist[i] = valencia_distance_inv(eereco, i, eereco[i].nni, invR2) + end + @inbounds for i in 1:N + valencia_beam_dist = valencia_beam_distance(eereco, i, γ, p) + beam_closer = valencia_beam_dist < eereco[i].dijdist + eereco.dijdist[i] = beam_closer ? valencia_beam_dist : eereco.dijdist[i] + eereco.nni[i] = beam_closer ? 0 : eereco.nni[i] + end +end + +# Forwarder to Val-specialized version +@inline function get_angular_nearest_neighbours!(eereco, + algorithm::JetAlgorithm.Algorithm, + dij_factor, p, γ = 1.0, R = 4.0) + return get_angular_nearest_neighbours!(eereco, Val(algorithm), dij_factor, p, γ, R) +end + # Update the nearest neighbour for jet i, w.r.t. all other active jets function update_nn_no_cross!(eereco, i, N, algorithm, dij_factor, β = 1.0, γ = 1.0, R = 4.0) eereco.nndist[i] = algorithm == JetAlgorithm.Valencia ? Inf : large_distance @@ -199,6 +287,68 @@ function update_nn_no_cross!(eereco, i, N, algorithm, dij_factor, β = 1.0, γ = end end +# Val-specialized no-cross update +@inline function update_nn_no_cross!(eereco, i, N, ::Val{JetAlgorithm.Durham}, dij_factor, + β = 1.0, γ = 1.0, R = 4.0) + eereco.nndist[i] = large_distance + eereco.nni[i] = i + @inbounds for j in 1:N + if j != i + this_metric = angular_distance(eereco, i, j) + better_nndist_i = this_metric < eereco[i].nndist + eereco.nndist[i] = better_nndist_i ? this_metric : eereco.nndist[i] + eereco.nni[i] = better_nndist_i ? j : eereco.nni[i] + end + end + eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, + Val(JetAlgorithm.Durham), R) +end + +@inline function update_nn_no_cross!(eereco, i, N, ::Val{JetAlgorithm.EEKt}, dij_factor, + β = 1.0, γ = 1.0, R = 4.0) + eereco.nndist[i] = large_distance + eereco.nni[i] = i + @inbounds for j in 1:N + if j != i + this_metric = angular_distance(eereco, i, j) + better_nndist_i = this_metric < eereco[i].nndist + eereco.nndist[i] = better_nndist_i ? this_metric : eereco.nndist[i] + eereco.nni[i] = better_nndist_i ? j : eereco.nni[i] + end + end + eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, + Val(JetAlgorithm.EEKt), R) + beam_close = eereco[i].E2p < eereco[i].dijdist + eereco.dijdist[i] = beam_close ? eereco[i].E2p : eereco.dijdist[i] + eereco.nni[i] = beam_close ? 0 : eereco.nni[i] +end + +@inline function update_nn_no_cross!(eereco, i, N, ::Val{JetAlgorithm.Valencia}, + dij_factor, β = 1.0, γ = 1.0, R = 4.0) + eereco.nndist[i] = Inf + eereco.nni[i] = i + invR2 = inv(R * R) + @inbounds for j in 1:N + if j != i + this_metric = valencia_distance_inv(eereco, i, j, invR2) + better_nndist_i = this_metric < eereco[i].nndist + eereco.nndist[i] = better_nndist_i ? this_metric : eereco.nndist[i] + eereco.nni[i] = better_nndist_i ? j : eereco.nni[i] + end + end + eereco.dijdist[i] = valencia_distance_inv(eereco, i, eereco[i].nni, invR2) + valencia_beam_dist = valencia_beam_distance(eereco, i, γ, β) + beam_close = valencia_beam_dist < eereco[i].dijdist + eereco.dijdist[i] = beam_close ? valencia_beam_dist : eereco.dijdist[i] + eereco.nni[i] = beam_close ? 0 : eereco.nni[i] +end + +# Forwarder +@inline function update_nn_no_cross!(eereco, i, N, algorithm::JetAlgorithm.Algorithm, + dij_factor, β = 1.0, γ = 1.0, R = 4.0) + return update_nn_no_cross!(eereco, i, N, Val(algorithm), dij_factor, β, γ, R) +end + function update_nn_cross!(eereco, i, N, algorithm, dij_factor, β = 1.0, γ = 1.0, R = 4.0) # Update the nearest neighbour for jet i, w.r.t. all other active jets # also doing the cross check for the other jet @@ -254,6 +404,93 @@ function update_nn_cross!(eereco, i, N, algorithm, dij_factor, β = 1.0, γ = 1. end end +# Val-specialized cross update +@inline function update_nn_cross!(eereco, i, N, ::Val{JetAlgorithm.Durham}, dij_factor, + β = 1.0, γ = 1.0, R = 4.0) + eereco.nndist[i] = large_distance + eereco.nni[i] = i + @inbounds for j in 1:N + if j != i + this_metric = angular_distance(eereco, i, j) + better_nndist_i = this_metric < eereco[i].nndist + eereco.nndist[i] = better_nndist_i ? this_metric : eereco.nndist[i] + eereco.nni[i] = better_nndist_i ? j : eereco.nni[i] + if this_metric < eereco[j].nndist + eereco.nndist[j] = this_metric + eereco.nni[j] = i + eereco.dijdist[j] = dij_dist(eereco, j, i, dij_factor, + Val(JetAlgorithm.Durham), R) + end + end + end + eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, + Val(JetAlgorithm.Durham), R) +end + +@inline function update_nn_cross!(eereco, i, N, ::Val{JetAlgorithm.EEKt}, dij_factor, + β = 1.0, γ = 1.0, R = 4.0) + eereco.nndist[i] = large_distance + eereco.nni[i] = i + @inbounds for j in 1:N + if j != i + this_metric = angular_distance(eereco, i, j) + better_nndist_i = this_metric < eereco[i].nndist + eereco.nndist[i] = better_nndist_i ? this_metric : eereco.nndist[i] + eereco.nni[i] = better_nndist_i ? j : eereco.nni[i] + if this_metric < eereco[j].nndist + eereco.nndist[j] = this_metric + eereco.nni[j] = i + eereco.dijdist[j] = dij_dist(eereco, j, i, dij_factor, + Val(JetAlgorithm.EEKt), R) + if eereco[j].E2p < eereco[j].dijdist + eereco.dijdist[j] = eereco[j].E2p + eereco.nni[j] = 0 + end + end + end + end + eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, + Val(JetAlgorithm.EEKt), R) + beam_close = eereco[i].E2p < eereco[i].dijdist + eereco.dijdist[i] = beam_close ? eereco[i].E2p : eereco.dijdist[i] + eereco.nni[i] = beam_close ? 0 : eereco.nni[i] +end + +@inline function update_nn_cross!(eereco, i, N, ::Val{JetAlgorithm.Valencia}, dij_factor, + β = 1.0, γ = 1.0, R = 4.0) + eereco.nndist[i] = Inf + eereco.nni[i] = i + invR2 = inv(R * R) + @inbounds for j in 1:N + if j != i + this_metric = valencia_distance_inv(eereco, i, j, invR2) + better_nndist_i = this_metric < eereco[i].nndist + eereco.nndist[i] = better_nndist_i ? this_metric : eereco.nndist[i] + eereco.nni[i] = better_nndist_i ? j : eereco.nni[i] + if this_metric < eereco[j].nndist + eereco.nndist[j] = this_metric + eereco.nni[j] = i + eereco.dijdist[j] = valencia_distance_inv(eereco, j, i, invR2) + valencia_beam_dist = valencia_beam_distance(eereco, j, γ, β) + if valencia_beam_dist < eereco[j].dijdist + eereco.dijdist[j] = valencia_beam_dist + eereco.nni[j] = 0 + end + end + end + end + eereco.dijdist[i] = valencia_distance_inv(eereco, i, eereco[i].nni, invR2) + valencia_beam_dist = valencia_beam_distance(eereco, i, γ, β) + beam_close = valencia_beam_dist < eereco[i].dijdist + eereco.dijdist[i] = beam_close ? valencia_beam_dist : eereco.dijdist[i] + eereco.nni[i] = beam_close ? 0 : eereco.nni[i] +end + +@inline function update_nn_cross!(eereco, i, N, algorithm::JetAlgorithm.Algorithm, + dij_factor, β = 1.0, γ = 1.0, R = 4.0) + return update_nn_cross!(eereco, i, N, Val(algorithm), dij_factor, β, γ, R) +end + function ee_check_consistency(clusterseq, eereco, N) # Check the consistency of the reconstruction state for i in 1:N @@ -268,11 +505,11 @@ function ee_check_consistency(clusterseq, eereco, N) end end end - @debug "Consistency check passed at $msg" + @debug "Consistency check passed" end -function fill_reco_array!(eereco, particles, R2, p) - for i in eachindex(particles) +Base.@propagate_inbounds @inline function fill_reco_array!(eereco, particles, R2, p) + @inbounds for i in eachindex(particles) eereco.index[i] = i eereco.nni[i] = 0 eereco.nndist[i] = R2 @@ -280,18 +517,41 @@ function fill_reco_array!(eereco, particles, R2, p) eereco.nx[i] = nx(particles[i]) eereco.ny[i] = ny(particles[i]) eereco.nz[i] = nz(particles[i]) - eereco.E2p[i] = energy(particles[i])^(2p) + E = energy(particles[i]) + if p isa Int + if p == 1 + eereco.E2p[i] = E * E + else + E2 = E * E + eereco.E2p[i] = E2^p + end + else + eereco.E2p[i] = E^(2p) + end end end -@inline function insert_new_jet!(eereco, i, newjet_k, R2, merged_jet, p) - eereco.index[i] = newjet_k - eereco.nni[i] = 0 - eereco.nndist[i] = R2 - eereco.nx[i] = nx(merged_jet) - eereco.ny[i] = ny(merged_jet) - eereco.nz[i] = nz(merged_jet) - eereco.E2p[i] = energy(merged_jet)^(2p) +Base.@propagate_inbounds @inline function insert_new_jet!(eereco, i, newjet_k, R2, + merged_jet, p) + @inbounds begin + eereco.index[i] = newjet_k + eereco.nni[i] = 0 + eereco.nndist[i] = R2 + eereco.nx[i] = nx(merged_jet) + eereco.ny[i] = ny(merged_jet) + eereco.nz[i] = nz(merged_jet) + E = energy(merged_jet) + if p isa Int + if p == 1 + eereco.E2p[i] = E * E + else + E2 = E * E + eereco.E2p[i] = E2^p + end + else + eereco.E2p[i] = E^(2p) + end + end end """ @@ -299,15 +559,17 @@ end Copy the contents of slot `i` in the `eereco` array to slot `j`. """ -@inline function copy_to_slot!(eereco, i, j) - eereco.index[j] = eereco.index[i] - eereco.nni[j] = eereco.nni[i] - eereco.nndist[j] = eereco.nndist[i] - eereco.dijdist[j] = eereco.dijdist[i] - eereco.nx[j] = eereco.nx[i] - eereco.ny[j] = eereco.ny[i] - eereco.nz[j] = eereco.nz[i] - eereco.E2p[j] = eereco.E2p[i] +Base.@propagate_inbounds @inline function copy_to_slot!(eereco, i, j) + @inbounds begin + eereco.index[j] = eereco.index[i] + eereco.nni[j] = eereco.nni[i] + eereco.nndist[j] = eereco.nndist[i] + eereco.dijdist[j] = eereco.dijdist[i] + eereco.nx[j] = eereco.nx[i] + eereco.ny[j] = eereco.ny[i] + eereco.nz[j] = eereco.nz[i] + eereco.E2p[j] = eereco.E2p[i] + end end """ From 69abb66bc4e12822d95111cac0239c8d8c884cbf Mon Sep 17 00:00:00 2001 From: Matt LeBlanc Date: Wed, 13 Aug 2025 00:38:45 -0400 Subject: [PATCH 10/32] Coverity --- test/_common.jl | 5 ++++ test/test-ee-reconstruction.jl | 54 ++++++++++++++++++++++++++++++++++ test/test-valencia.jl | 15 +++++++--- 3 files changed, 70 insertions(+), 4 deletions(-) diff --git a/test/_common.jl b/test/_common.jl index cad44b71..5fb13c1e 100644 --- a/test/_common.jl +++ b/test/_common.jl @@ -10,6 +10,11 @@ using Logging using LorentzVectorHEP using JSON using Test +using StructArrays + +using JetReconstruction: EERecoJet, + fill_reco_array!, insert_new_jet!, copy_to_slot!, + dij_dist, valencia_distance, valencia_distance_inv logger = ConsoleLogger(stdout, Logging.Warn) global_logger(logger) diff --git a/test/test-ee-reconstruction.jl b/test/test-ee-reconstruction.jl index 0a942eec..83a583f3 100644 --- a/test/test-ee-reconstruction.jl +++ b/test/test-ee-reconstruction.jl @@ -26,3 +26,57 @@ for r in [2.0, 4.0] (cs) -> exclusive_jets(cs; njets = 4), "exclusive njets") run_reco_test(eekt_njets) end + +# Optimization/helper coverage + +@testset "E2p integer fast paths in fill/insert (e+e-)" begin + E1, E2 = 3.0, 2.0 + # PseudoJet signature is (px, py, pz, E); give nonzero momentum to avoid 1/0 in EEJet + pj1 = PseudoJet(1.0, 0.0, 0.0, E1) + pj2 = PseudoJet(1.0, 0.0, 0.0, E2) + particles = EEJet[] + push!(particles, EEJet(pj1; cluster_hist_index = 1)) + push!(particles, EEJet(pj2; cluster_hist_index = 2)) + + eereco = StructArray{EERecoJet}(undef, 2) + R = 1.0 + R2 = R^2 + + fill_reco_array!(eereco, particles, R2, 1) + @test eereco.E2p[1] ≈ E1^2 + @test eereco.E2p[2] ≈ E2^2 + + fill_reco_array!(eereco, particles, R2, 2) + @test eereco.E2p[1] ≈ E1^4 + @test eereco.E2p[2] ≈ E2^4 + + merged = EEJet(1.0, 0.0, 0.0, 5.0; cluster_hist_index = 0) + insert_new_jet!(eereco, 1, 3, R2, merged, 1) + @test eereco.E2p[1] ≈ 5.0^2 + + insert_new_jet!(eereco, 2, 4, R2, merged, 2) + @test eereco.E2p[2] ≈ 5.0^4 +end + +@testset "copy_to_slot! copies fields" begin + eereco = StructArray{EERecoJet}(undef, 2) + eereco.index[1] = 10 + eereco.nni[1] = 1 + eereco.nndist[1] = 3.14 + eereco.dijdist[1] = 2.71 + eereco.nx[1] = 0.1 + eereco.ny[1] = 0.2 + eereco.nz[1] = 0.3 + eereco.E2p[1] = 7.0 + copy_to_slot!(eereco, 1, 2) + @test eereco.index[2] == 10 + @test eereco.dijdist[2] == 2.71 + @test eereco.E2p[2] == 7.0 +end + +@testset "dij_dist fallback error (non-Algorithm)" begin + eereco = StructArray{EERecoJet}(undef, 1) + eereco.nndist[1] = 0.0 + eereco.E2p[1] = 1.0 + @test_throws ArgumentError dij_dist(eereco, 1, 0, 1.0, :Foo, 1.0) +end diff --git a/test/test-valencia.jl b/test/test-valencia.jl index 6d46e98f..8431cd11 100644 --- a/test/test-valencia.jl +++ b/test/test-valencia.jl @@ -65,10 +65,6 @@ valencia_exclusive_d500_b1g1 = ComparisonTestValencia(events_file_ee, "exclusive dcut=500 beta=1.0 gamma=1.0 R=1.0") run_reco_test(valencia_exclusive_d500_b1g1) -# Bring required types/functions into scope for targeted unit tests -using JetReconstruction: EERecoJet, PseudoJet, EEJet, ee_genkt_algorithm, dij_dist, - valencia_distance - # Test dij_dist for Valencia algorithm @testset "dij_dist Valencia" begin # Minimal eereco with two reco jets; only nx,ny,nz,E2p are used by valencia_distance @@ -78,6 +74,17 @@ using JetReconstruction: EERecoJet, PseudoJet, EEJet, ee_genkt_algorithm, dij_di @test dij ≈ valencia_distance(eereco, 1, 2, 0.8) end +# Valencia distance wrapper coverage +@testset "Valencia distance wrappers" begin + # Minimal eereco with two reco jets and identical directions so angle=0 + eereco = EERecoJet[EERecoJet(1, 0, Inf, Inf, 1.0, 0.0, 0.0, 9.0), + EERecoJet(2, 0, Inf, Inf, 1.0, 0.0, 0.0, 4.0)] + R = 2.0 + invR2 = inv(R * R) + @test valencia_distance_inv(eereco, 1, 2, invR2) == 0.0 + @test valencia_distance(eereco, 1, 2, R) == 0.0 +end + # Test ee_genkt_algorithm for Valencia algorithm (covers β override and dij_factor selection) @testset "ee_genkt_algorithm Valencia" begin particles = [PseudoJet(1.0, 0.0, 0.0, 1.0)] From 98d5aa15a2d2e5a93bdd9b143fd34c20d1e02e56 Mon Sep 17 00:00:00 2001 From: Matt LeBlanc Date: Sun, 17 Aug 2025 10:01:03 -0400 Subject: [PATCH 11/32] Move to specialised path implementations for different algorithms to recover performance --- src/EEAlgorithm.jl | 939 +++++++++++++++++++++++++++++++++------------ src/EEJet.jl | 3 + 2 files changed, 707 insertions(+), 235 deletions(-) diff --git a/src/EEAlgorithm.jl b/src/EEAlgorithm.jl index d7b58a26..6a703b5f 100644 --- a/src/EEAlgorithm.jl +++ b/src/EEAlgorithm.jl @@ -18,8 +18,13 @@ Calculate the angular distance between two jets `i` and `j` using the formula cos\theta``. """ Base.@propagate_inbounds @inline function angular_distance(eereco, i, j) - @muladd 1.0 - eereco[i].nx * eereco[j].nx - eereco[i].ny * eereco[j].ny - - eereco[i].nz * eereco[j].nz + if hasproperty(eereco, :nx) + nx = eereco.nx; ny = eereco.ny; nz = eereco.nz + @muladd 1.0 - nx[i] * nx[j] - ny[i] * ny[j] - nz[i] * nz[j] + else + # Fallback for Array-of-structs (AoS) + @muladd 1.0 - eereco[i].nx * eereco[j].nx - eereco[i].ny * eereco[j].ny - eereco[i].nz * eereco[j].nz + end end """ @@ -38,16 +43,33 @@ Calculate the Valencia distance between two jets `i` and `j` as - `Float64`: The Valencia distance between `i` and `j`. """ Base.@propagate_inbounds @inline function valencia_distance_inv(eereco, i, j, invR2) - angular_dist = angular_distance(eereco, i, j) - # Valencia dij : min(E_i^{2β}, E_j^{2β}) * 2 * (1 - cos θ) * invR2 - # Note that β plays the role of p in other algorithms, so E2p can be used. - min(eereco[i].E2p, eereco[j].E2p) * 2 * angular_dist * invR2 + if hasproperty(eereco, :nx) + nx = eereco.nx; ny = eereco.ny; nz = eereco.nz; E2p = eereco.E2p + angular_dist = angular_distance_arrays(nx, ny, nz, i, j) + # Valencia dij : min(E_i^{2β}, E_j^{2β}) * 2 * (1 - cos θ) * invR2 + min(E2p[i], E2p[j]) * 2 * angular_dist * invR2 + else + # Fallback for Array-of-structs + angular_dist = 1.0 - eereco[i].nx * eereco[j].nx - eereco[i].ny * eereco[j].ny - eereco[i].nz * eereco[j].nz + min(eereco[i].E2p, eereco[j].E2p) * 2 * angular_dist * invR2 + end end Base.@propagate_inbounds @inline function valencia_distance(eereco, i, j, R) return valencia_distance_inv(eereco, i, j, inv(R * R)) end +# Array-based helpers: operate directly on field vectors from StructArray to avoid +# repeated eereco[i] indexing which can be slower. +Base.@propagate_inbounds @inline function angular_distance_arrays(nx, ny, nz, i, j) + @muladd 1.0 - nx[i] * nx[j] - ny[i] * ny[j] - nz[i] * nz[j] +end + +Base.@propagate_inbounds @inline function valencia_distance_inv_arrays(E2p, nx, ny, nz, i, j, invR2) + angular_dist = angular_distance_arrays(nx, ny, nz, i, j) + min(E2p[i], E2p[j]) * 2 * angular_dist * invR2 +end + """ valencia_beam_distance(eereco, i, γ, β) -> Float64 @@ -66,11 +88,39 @@ for unit direction cosines. Since ``sin^2 θ = 1 - nz^2``, we implement - `Float64`: The Valencia beam distance for jet `i`. """ Base.@propagate_inbounds @inline function valencia_beam_distance(eereco, i, γ, β) - nz = eereco[i].nz - # sin^2(theta) = 1 - nz^2; beam distance independent of R - sin2 = 1 - nz * nz - E2p = eereco[i].E2p - return E2p * sin2^γ + if hasproperty(eereco, :nz) + nzv = eereco.nz; E2pv = eereco.E2p + nz_i = nzv[i] + sin2 = 1 - nz_i * nz_i + E2p = E2pv[i] + else + nz_i = eereco[i].nz + sin2 = 1 - nz_i * nz_i + E2p = eereco[i].E2p + end + # Fast-paths for common γ values to avoid pow in hot loop + if γ == 1.0 + return E2p * sin2 + elseif γ == 2.0 + return E2p * (sin2 * sin2) + else + return E2p * sin2^γ + end +end + +# Array-based helper for Valencia beam distance to avoid StructArray getindex in hot loops +Base.@propagate_inbounds @inline function valencia_beam_distance_arrays(E2p, nz, i, γ, β) + nz_i = nz[i] + sin2 = 1 - nz_i * nz_i + E2p_i = E2p[i] + # Fast-paths for common γ values to avoid pow in hot loop + if γ == 1.0 + return E2p_i * sin2 + elseif γ == 2.0 + return E2p_i * (sin2 * sin2) + else + return E2p_i * sin2^γ + end end """ @@ -122,19 +172,22 @@ function get_angular_nearest_neighbours!(eereco, algorithm, dij_factor, p, γ = # Get the initial nearest neighbours for each jet N = length(eereco) # For Valencia, nearest-neighbour must be chosen on the full dij metric (FastJet NNH behaviour) - if algorithm == JetAlgorithm.Valencia @inbounds for i in 1:N - eereco.nndist[i] = Inf - eereco.nni[i] = i + local_nndist_i = Inf + local_nni_i = i + eereco.nndist[i] = local_nndist_i + eereco.nni[i] = local_nni_i end - end # Nearest neighbour search @inbounds for i in 1:N @inbounds for j in (i + 1):N # Metric used to pick the nearest neighbour - this_metric = algorithm == JetAlgorithm.Valencia ? - valencia_distance(eereco, i, j, R) : - angular_distance(eereco, i, j) + if algorithm == JetAlgorithm.Valencia + # Use array helpers to avoid repeated StructArray getindex + this_metric = valencia_distance_inv_arrays(eereco.E2p, eereco.nx, eereco.ny, eereco.nz, i, j, inv(R * R)) + else + this_metric = angular_distance_arrays(eereco.nx, eereco.ny, eereco.nz, i, j) + end # Using these ternary operators is faster than the if-else block better_nndist_i = this_metric < eereco[i].nndist @@ -175,20 +228,27 @@ end @inline function get_angular_nearest_neighbours!(eereco, ::Val{JetAlgorithm.Durham}, dij_factor, p, γ = 1.0, R = 4.0) N = length(eereco) - # Nearest neighbour search using angular metric + nx = eereco.nx; ny = eereco.ny; nz = eereco.nz + nndist = eereco.nndist; nni = eereco.nni @inbounds for i in 1:N - @inbounds for j in (i + 1):N - this_metric = angular_distance(eereco, i, j) - better_nndist_i = this_metric < eereco[i].nndist - eereco.nndist[i] = better_nndist_i ? this_metric : eereco.nndist[i] - eereco.nni[i] = better_nndist_i ? j : eereco.nni[i] - better_nndist_j = this_metric < eereco[j].nndist - eereco.nndist[j] = better_nndist_j ? this_metric : eereco.nndist[j] - eereco.nni[j] = better_nndist_j ? i : eereco.nni[j] + local_nndist_i = large_distance + local_nni_i = i + @inbounds for j in 1:N + if j != i + this_metric = angular_distance_arrays(nx, ny, nz, i, j) + better_nndist_i = this_metric < local_nndist_i + local_nndist_i = better_nndist_i ? this_metric : local_nndist_i + local_nni_i = better_nndist_i ? j : local_nni_i + better_nndist_j = this_metric < nndist[j] + nndist[j] = better_nndist_j ? this_metric : nndist[j] + nni[j] = better_nndist_j ? i : nni[j] + end end + nndist[i] = local_nndist_i + nni[i] = local_nni_i end @inbounds for i in 1:N - eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, + eereco.dijdist[i] = dij_dist(eereco, i, nni[i], dij_factor, Val(JetAlgorithm.Durham), R) end end @@ -221,24 +281,32 @@ end @inline function get_angular_nearest_neighbours!(eereco, ::Val{JetAlgorithm.Valencia}, dij_factor, p, γ = 1.0, R = 4.0) N = length(eereco) + E2p = eereco.E2p; nx = eereco.nx; ny = eereco.ny; nz = eereco.nz + nndist = eereco.nndist; nni = eereco.nni invR2 = inv(R * R) @inbounds for i in 1:N - eereco.nndist[i] = Inf - eereco.nni[i] = i + nndist[i] = Inf + nni[i] = i end @inbounds for i in 1:N + local_nndist_i = nndist[i] + local_nni_i = nni[i] @inbounds for j in (i + 1):N - this_metric = valencia_distance_inv(eereco, i, j, invR2) - better_nndist_i = this_metric < eereco[i].nndist - eereco.nndist[i] = better_nndist_i ? this_metric : eereco.nndist[i] - eereco.nni[i] = better_nndist_i ? j : eereco.nni[i] - better_nndist_j = this_metric < eereco[j].nndist - eereco.nndist[j] = better_nndist_j ? this_metric : eereco.nndist[j] - eereco.nni[j] = better_nndist_j ? i : eereco.nni[j] + this_metric = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, j, invR2) + if this_metric < local_nndist_i + local_nndist_i = this_metric + local_nni_i = j + end + if this_metric < nndist[j] + nndist[j] = this_metric + nni[j] = i + end end + nndist[i] = local_nndist_i + nni[i] = local_nni_i end @inbounds for i in 1:N - eereco.dijdist[i] = valencia_distance_inv(eereco, i, eereco[i].nni, invR2) + eereco.dijdist[i] = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, nni[i], invR2) end @inbounds for i in 1:N valencia_beam_dist = valencia_beam_distance(eereco, i, γ, p) @@ -256,239 +324,205 @@ end end # Update the nearest neighbour for jet i, w.r.t. all other active jets -function update_nn_no_cross!(eereco, i, N, algorithm, dij_factor, β = 1.0, γ = 1.0, R = 4.0) - eereco.nndist[i] = algorithm == JetAlgorithm.Valencia ? Inf : large_distance - eereco.nni[i] = i - @inbounds for j in 1:N - if j != i - # Metric for nearest neighbour selection - this_metric = algorithm == JetAlgorithm.Valencia ? - valencia_distance(eereco, i, j, R) : - angular_distance(eereco, i, j) - better_nndist_i = this_metric < eereco[i].nndist - eereco.nndist[i] = better_nndist_i ? this_metric : eereco.nndist[i] - eereco.nni[i] = better_nndist_i ? j : eereco.nni[i] - end - end - if algorithm == JetAlgorithm.Valencia - eereco.dijdist[i] = valencia_distance(eereco, i, eereco[i].nni, R) - else - eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, algorithm, R) - end - if algorithm == JetAlgorithm.EEKt - beam_close = eereco[i].E2p < eereco[i].dijdist - eereco.dijdist[i] = beam_close ? eereco[i].E2p : eereco.dijdist[i] - eereco.nni[i] = beam_close ? 0 : eereco.nni[i] - elseif algorithm == JetAlgorithm.Valencia - valencia_beam_dist = valencia_beam_distance(eereco, i, γ, β) - beam_close = valencia_beam_dist < eereco[i].dijdist - eereco.dijdist[i] = beam_close ? valencia_beam_dist : eereco.dijdist[i] - eereco.nni[i] = beam_close ? 0 : eereco.nni[i] - end +@inline function update_nn_no_cross!(eereco, i, N, algorithm::JetAlgorithm.Algorithm, + dij_factor, β = 1.0, γ = 1.0, R = 4.0) + # Forward to Val-specialized implementations to avoid runtime branches + return update_nn_no_cross!(eereco, i, N, Val(algorithm), dij_factor, β, γ, R) end # Val-specialized no-cross update @inline function update_nn_no_cross!(eereco, i, N, ::Val{JetAlgorithm.Durham}, dij_factor, β = 1.0, γ = 1.0, R = 4.0) - eereco.nndist[i] = large_distance - eereco.nni[i] = i - @inbounds for j in 1:N - if j != i - this_metric = angular_distance(eereco, i, j) - better_nndist_i = this_metric < eereco[i].nndist - eereco.nndist[i] = better_nndist_i ? this_metric : eereco.nndist[i] - eereco.nni[i] = better_nndist_i ? j : eereco.nni[i] - end + nndist = eereco.nndist; nni = eereco.nni + nx = eereco.nx; ny = eereco.ny; nz = eereco.nz + nndist[i] = large_distance + nni[i] = i + @inbounds for j in 1:(i-1) + this_metric = angular_distance_arrays(nx, ny, nz, i, j) + better_nndist_i = this_metric < nndist[i] + nndist[i] = better_nndist_i ? this_metric : nndist[i] + nni[i] = better_nndist_i ? j : nni[i] + end + @inbounds for j in (i+1):N + this_metric = angular_distance_arrays(nx, ny, nz, i, j) + better_nndist_i = this_metric < nndist[i] + nndist[i] = better_nndist_i ? this_metric : nndist[i] + nni[i] = better_nndist_i ? j : nni[i] end - eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, - Val(JetAlgorithm.Durham), R) + # Inline Durham dij computation to avoid function-call overhead + E2p = eereco.E2p + E2p_i = E2p[i] + E2p_nni = E2p[nni[i]] + minE2p = E2p_i < E2p_nni ? E2p_i : E2p_nni + eereco.dijdist[i] = minE2p * dij_factor * nndist[i] end @inline function update_nn_no_cross!(eereco, i, N, ::Val{JetAlgorithm.EEKt}, dij_factor, β = 1.0, γ = 1.0, R = 4.0) - eereco.nndist[i] = large_distance - eereco.nni[i] = i - @inbounds for j in 1:N - if j != i - this_metric = angular_distance(eereco, i, j) - better_nndist_i = this_metric < eereco[i].nndist - eereco.nndist[i] = better_nndist_i ? this_metric : eereco.nndist[i] - eereco.nni[i] = better_nndist_i ? j : eereco.nni[i] - end + nndist = eereco.nndist; nni = eereco.nni + nx = eereco.nx; ny = eereco.ny; nz = eereco.nz + E2p = eereco.E2p + E2p_i = E2p[i] + nndist[i] = large_distance + nni[i] = i + @inbounds for j in 1:(i-1) + this_metric = angular_distance_arrays(nx, ny, nz, i, j) + better_nndist_i = this_metric < nndist[i] + nndist[i] = better_nndist_i ? this_metric : nndist[i] + nni[i] = better_nndist_i ? j : nni[i] + end + @inbounds for j in (i+1):N + this_metric = angular_distance_arrays(nx, ny, nz, i, j) + better_nndist_i = this_metric < nndist[i] + nndist[i] = better_nndist_i ? this_metric : nndist[i] + nni[i] = better_nndist_i ? j : nni[i] end - eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, - Val(JetAlgorithm.EEKt), R) - beam_close = eereco[i].E2p < eereco[i].dijdist - eereco.dijdist[i] = beam_close ? eereco[i].E2p : eereco.dijdist[i] + # Inline EEKt dij computation and beam check + E2p_nni = E2p[nni[i]] + minE2p = E2p_i < E2p_nni ? E2p_i : E2p_nni + eereco.dijdist[i] = minE2p * dij_factor * nndist[i] + beam_close = E2p_i < eereco.dijdist[i] + eereco.dijdist[i] = beam_close ? E2p_i : eereco.dijdist[i] eereco.nni[i] = beam_close ? 0 : eereco.nni[i] end @inline function update_nn_no_cross!(eereco, i, N, ::Val{JetAlgorithm.Valencia}, dij_factor, β = 1.0, γ = 1.0, R = 4.0) - eereco.nndist[i] = Inf - eereco.nni[i] = i + nndist = eereco.nndist; nni = eereco.nni + nx = eereco.nx; ny = eereco.ny; nz = eereco.nz + E2p = eereco.E2p + E2p_i = E2p[i] + nndist[i] = Inf + nni[i] = i invR2 = inv(R * R) - @inbounds for j in 1:N - if j != i - this_metric = valencia_distance_inv(eereco, i, j, invR2) - better_nndist_i = this_metric < eereco[i].nndist - eereco.nndist[i] = better_nndist_i ? this_metric : eereco.nndist[i] - eereco.nni[i] = better_nndist_i ? j : eereco.nni[i] - end + @inbounds for j in 1:(i-1) + this_metric = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, j, invR2) + better_nndist_i = this_metric < nndist[i] + nndist[i] = better_nndist_i ? this_metric : nndist[i] + nni[i] = better_nndist_i ? j : nni[i] + end + @inbounds for j in (i+1):N + this_metric = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, j, invR2) + better_nndist_i = this_metric < nndist[i] + nndist[i] = better_nndist_i ? this_metric : nndist[i] + nni[i] = better_nndist_i ? j : nni[i] end - eereco.dijdist[i] = valencia_distance_inv(eereco, i, eereco[i].nni, invR2) + eereco.dijdist[i] = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, nni[i], invR2) valencia_beam_dist = valencia_beam_distance(eereco, i, γ, β) - beam_close = valencia_beam_dist < eereco[i].dijdist + beam_close = valencia_beam_dist < eereco.dijdist[i] eereco.dijdist[i] = beam_close ? valencia_beam_dist : eereco.dijdist[i] eereco.nni[i] = beam_close ? 0 : eereco.nni[i] end -# Forwarder -@inline function update_nn_no_cross!(eereco, i, N, algorithm::JetAlgorithm.Algorithm, - dij_factor, β = 1.0, γ = 1.0, R = 4.0) - return update_nn_no_cross!(eereco, i, N, Val(algorithm), dij_factor, β, γ, R) -end +# (Forwarding handled earlier; avoid duplicate definition) -function update_nn_cross!(eereco, i, N, algorithm, dij_factor, β = 1.0, γ = 1.0, R = 4.0) - # Update the nearest neighbour for jet i, w.r.t. all other active jets - # also doing the cross check for the other jet - eereco.nndist[i] = algorithm == JetAlgorithm.Valencia ? Inf : large_distance - eereco.nni[i] = i - @inbounds for j in 1:N - if j != i - # Metric for nearest neighbour selection - this_metric = algorithm == JetAlgorithm.Valencia ? - valencia_distance(eereco, i, j, R) : - angular_distance(eereco, i, j) - better_nndist_i = this_metric < eereco[i].nndist - eereco.nndist[i] = better_nndist_i ? this_metric : eereco.nndist[i] - eereco.nni[i] = better_nndist_i ? j : eereco.nni[i] - if this_metric < eereco[j].nndist - eereco.nndist[j] = this_metric - eereco.nni[j] = i - # j will not be revisited, so update metric distance here - if algorithm == JetAlgorithm.Valencia - eereco.dijdist[j] = valencia_distance(eereco, j, i, R) - else - eereco.dijdist[j] = dij_dist(eereco, j, i, dij_factor, algorithm, R) - end - if algorithm == JetAlgorithm.EEKt - if eereco[j].E2p < eereco[j].dijdist - eereco.dijdist[j] = eereco[j].E2p - eereco.nni[j] = 0 - end - elseif algorithm == JetAlgorithm.Valencia - valencia_beam_dist = valencia_beam_distance(eereco, j, γ, β) - if valencia_beam_dist < eereco[j].dijdist - eereco.dijdist[j] = valencia_beam_dist - eereco.nni[j] = 0 - end - end - end - end - end - if algorithm == JetAlgorithm.Valencia - eereco.dijdist[i] = @inbounds valencia_distance(eereco, i, eereco[i].nni, R) - else - eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, algorithm, R) - end - if algorithm == JetAlgorithm.EEKt - beam_close = eereco[i].E2p < eereco[i].dijdist - eereco.dijdist[i] = beam_close ? eereco[i].E2p : eereco.dijdist[i] - eereco.nni[i] = beam_close ? 0 : eereco.nni[i] - elseif algorithm == JetAlgorithm.Valencia - valencia_beam_dist = @inbounds valencia_beam_distance(eereco, i, γ, β) - beam_close = valencia_beam_dist < eereco[i].dijdist - eereco.dijdist[i] = beam_close ? valencia_beam_dist : eereco.dijdist[i] - eereco.nni[i] = beam_close ? 0 : eereco.nni[i] - end +@inline function update_nn_cross!(eereco, i, N, algorithm::JetAlgorithm.Algorithm, + dij_factor, β = 1.0, γ = 1.0, R = 4.0) + # Forward to Val-specialized implementations to avoid runtime branches + return update_nn_cross!(eereco, i, N, Val(algorithm), dij_factor, β, γ, R) end # Val-specialized cross update @inline function update_nn_cross!(eereco, i, N, ::Val{JetAlgorithm.Durham}, dij_factor, β = 1.0, γ = 1.0, R = 4.0) - eereco.nndist[i] = large_distance - eereco.nni[i] = i + nndist = eereco.nndist; nni = eereco.nni + nx = eereco.nx; ny = eereco.ny; nz = eereco.nz + nndist[i] = large_distance + nni[i] = i + E2p = eereco.E2p + E2p_i = E2p[i] @inbounds for j in 1:N if j != i - this_metric = angular_distance(eereco, i, j) - better_nndist_i = this_metric < eereco[i].nndist - eereco.nndist[i] = better_nndist_i ? this_metric : eereco.nndist[i] - eereco.nni[i] = better_nndist_i ? j : eereco.nni[i] - if this_metric < eereco[j].nndist - eereco.nndist[j] = this_metric - eereco.nni[j] = i - eereco.dijdist[j] = dij_dist(eereco, j, i, dij_factor, - Val(JetAlgorithm.Durham), R) + this_metric = angular_distance_arrays(nx, ny, nz, i, j) + better_nndist_i = this_metric < nndist[i] + nndist[i] = better_nndist_i ? this_metric : nndist[i] + nni[i] = better_nndist_i ? j : nni[i] + if this_metric < nndist[j] + nndist[j] = this_metric + nni[j] = i + # Hoist E2p_j and compute new dij once + E2p_j = E2p[j] + new_dij = (E2p_i < E2p_j ? E2p_i : E2p_j) * dij_factor * this_metric + eereco.dijdist[j] = new_dij end end end - eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, - Val(JetAlgorithm.Durham), R) + # Inline dij for i + E2p_nni = E2p[nni[i]] + minE2p_i = E2p_i < E2p_nni ? E2p_i : E2p_nni + eereco.dijdist[i] = minE2p_i * dij_factor * nndist[i] end @inline function update_nn_cross!(eereco, i, N, ::Val{JetAlgorithm.EEKt}, dij_factor, β = 1.0, γ = 1.0, R = 4.0) - eereco.nndist[i] = large_distance - eereco.nni[i] = i + nndist = eereco.nndist; nni = eereco.nni + nx = eereco.nx; ny = eereco.ny; nz = eereco.nz + E2p = eereco.E2p + E2p_i = E2p[i] + nndist[i] = large_distance + nni[i] = i @inbounds for j in 1:N if j != i - this_metric = angular_distance(eereco, i, j) - better_nndist_i = this_metric < eereco[i].nndist - eereco.nndist[i] = better_nndist_i ? this_metric : eereco.nndist[i] - eereco.nni[i] = better_nndist_i ? j : eereco.nni[i] - if this_metric < eereco[j].nndist - eereco.nndist[j] = this_metric - eereco.nni[j] = i - eereco.dijdist[j] = dij_dist(eereco, j, i, dij_factor, - Val(JetAlgorithm.EEKt), R) - if eereco[j].E2p < eereco[j].dijdist - eereco.dijdist[j] = eereco[j].E2p - eereco.nni[j] = 0 + this_metric = angular_distance_arrays(nx, ny, nz, i, j) + better_nndist_i = this_metric < nndist[i] + nndist[i] = better_nndist_i ? this_metric : nndist[i] + nni[i] = better_nndist_i ? j : nni[i] + if this_metric < nndist[j] + nndist[j] = this_metric + nni[j] = i + # Hoist E2p_j and compute new dij once, then beam-check + E2p_j = E2p[j] + new_dij = (E2p_i < E2p_j ? E2p_i : E2p_j) * dij_factor * this_metric + if E2p_j < new_dij + eereco.dijdist[j] = E2p_j + nni[j] = 0 + else + eereco.dijdist[j] = new_dij end end end end - eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, - Val(JetAlgorithm.EEKt), R) - beam_close = eereco[i].E2p < eereco[i].dijdist - eereco.dijdist[i] = beam_close ? eereco[i].E2p : eereco.dijdist[i] - eereco.nni[i] = beam_close ? 0 : eereco.nni[i] + # Inline dij for i and beam check + E2p_nni = E2p[nni[i]] + minE2p_i = E2p_i < E2p_nni ? E2p_i : E2p_nni + eereco.dijdist[i] = minE2p_i * dij_factor * nndist[i] + beam_close = E2p_i < eereco.dijdist[i] + eereco.dijdist[i] = beam_close ? E2p_i : eereco.dijdist[i] + nni[i] = beam_close ? 0 : nni[i] end @inline function update_nn_cross!(eereco, i, N, ::Val{JetAlgorithm.Valencia}, dij_factor, β = 1.0, γ = 1.0, R = 4.0) - eereco.nndist[i] = Inf - eereco.nni[i] = i + nndist = eereco.nndist; nni = eereco.nni + nx = eereco.nx; ny = eereco.ny; nz = eereco.nz + E2p = eereco.E2p + nndist[i] = Inf + nni[i] = i invR2 = inv(R * R) @inbounds for j in 1:N if j != i - this_metric = valencia_distance_inv(eereco, i, j, invR2) - better_nndist_i = this_metric < eereco[i].nndist - eereco.nndist[i] = better_nndist_i ? this_metric : eereco.nndist[i] - eereco.nni[i] = better_nndist_i ? j : eereco.nni[i] - if this_metric < eereco[j].nndist - eereco.nndist[j] = this_metric - eereco.nni[j] = i - eereco.dijdist[j] = valencia_distance_inv(eereco, j, i, invR2) - valencia_beam_dist = valencia_beam_distance(eereco, j, γ, β) - if valencia_beam_dist < eereco[j].dijdist + this_metric = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, j, invR2) + better_nndist_i = this_metric < nndist[i] + nndist[i] = better_nndist_i ? this_metric : nndist[i] + nni[i] = better_nndist_i ? j : nni[i] + if this_metric < nndist[j] + nndist[j] = this_metric + nni[j] = i + # Use the already-computed metric for dij (Valencia uses full dij here) + eereco.dijdist[j] = this_metric + valencia_beam_dist = valencia_beam_distance_arrays(E2p, nz, j, γ, β) + if valencia_beam_dist < eereco.dijdist[j] eereco.dijdist[j] = valencia_beam_dist - eereco.nni[j] = 0 + nni[j] = 0 end end end end - eereco.dijdist[i] = valencia_distance_inv(eereco, i, eereco[i].nni, invR2) - valencia_beam_dist = valencia_beam_distance(eereco, i, γ, β) - beam_close = valencia_beam_dist < eereco[i].dijdist + eereco.dijdist[i] = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, nni[i], invR2) + valencia_beam_dist = valencia_beam_distance_arrays(E2p, nz, i, γ, β) + beam_close = valencia_beam_dist < eereco.dijdist[i] eereco.dijdist[i] = beam_close ? valencia_beam_dist : eereco.dijdist[i] - eereco.nni[i] = beam_close ? 0 : eereco.nni[i] -end - -@inline function update_nn_cross!(eereco, i, N, algorithm::JetAlgorithm.Algorithm, - dij_factor, β = 1.0, γ = 1.0, R = 4.0) - return update_nn_cross!(eereco, i, N, Val(algorithm), dij_factor, β, γ, R) + nni[i] = beam_close ? 0 : nni[i] end function ee_check_consistency(clusterseq, eereco, N) @@ -508,6 +542,231 @@ function ee_check_consistency(clusterseq, eereco, N) @debug "Consistency check passed" end +################################################################################ +# Array-based nearest-neighbour update helpers (operate on raw vectors) +################################################################################ + +@inline function update_nn_no_cross_arrays!(nndist::AbstractVector, nni::AbstractVector, + nx::AbstractVector, ny::AbstractVector, nz::AbstractVector, + E2p::AbstractVector, dijdist::AbstractVector, + i::Integer, N::Integer, algorithm::JetAlgorithm.Algorithm, + dij_factor, β = 1.0, γ = 1.0, R = 4.0) + return update_nn_no_cross_arrays!(nndist, nni, nx, ny, nz, E2p, dijdist, i, N, Val(algorithm), dij_factor, β, γ, R) +end + +@inline function update_nn_no_cross_arrays!(nndist::AbstractVector, nni::AbstractVector, + nx::AbstractVector, ny::AbstractVector, nz::AbstractVector, + E2p::AbstractVector, dijdist::AbstractVector, + i::Integer, N::Integer, ::Val{JetAlgorithm.Durham}, + dij_factor, β = 1.0, γ = 1.0, R = 4.0) + nndist[i] = large_distance + nni[i] = i + @inbounds for j in 1:N + if j != i + this_metric = angular_distance_arrays(nx, ny, nz, i, j) + better_nndist_i = this_metric < nndist[i] + nndist[i] = better_nndist_i ? this_metric : nndist[i] + nni[i] = better_nndist_i ? j : nni[i] + end + end + # Inline Durham dij computation + E2p_i = E2p[i] + E2p_nni = E2p[nni[i]] + minE2p = E2p_i < E2p_nni ? E2p_i : E2p_nni + dijdist[i] = minE2p * dij_factor * nndist[i] +end + +@inline function update_nn_no_cross_arrays!(nndist::AbstractVector, nni::AbstractVector, + nx::AbstractVector, ny::AbstractVector, nz::AbstractVector, + E2p::AbstractVector, dijdist::AbstractVector, + i::Integer, N::Integer, ::Val{JetAlgorithm.EEKt}, + dij_factor, β = 1.0, γ = 1.0, R = 4.0) + nndist[i] = large_distance + nni[i] = i + E2p_i = E2p[i] + @inbounds for j in 1:N + if j != i + this_metric = angular_distance_arrays(nx, ny, nz, i, j) + better_nndist_i = this_metric < nndist[i] + nndist[i] = better_nndist_i ? this_metric : nndist[i] + nni[i] = better_nndist_i ? j : nni[i] + end + end + E2p_nni = E2p[nni[i]] + minE2p = E2p_i < E2p_nni ? E2p_i : E2p_nni + dijdist[i] = minE2p * dij_factor * nndist[i] + beam_close = E2p_i < dijdist[i] + dijdist[i] = beam_close ? E2p_i : dijdist[i] + nni[i] = beam_close ? 0 : nni[i] +end + +@inline function update_nn_no_cross_arrays!(nndist::AbstractVector, nni::AbstractVector, + nx::AbstractVector, ny::AbstractVector, nz::AbstractVector, + E2p::AbstractVector, dijdist::AbstractVector, + i::Integer, N::Integer, ::Val{JetAlgorithm.Valencia}, + dij_factor, β = 1.0, γ = 1.0, R = 4.0) + nndist[i] = Inf + nni[i] = i + invR2 = inv(R * R) + @inbounds for j in 1:N + if j != i + this_metric = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, j, invR2) + better_nndist_i = this_metric < nndist[i] + nndist[i] = better_nndist_i ? this_metric : nndist[i] + nni[i] = better_nndist_i ? j : nni[i] + end + end + dijdist[i] = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, nni[i], invR2) + valencia_beam_dist = valencia_beam_distance_arrays(E2p, nz, i, γ, β) + beam_close = valencia_beam_dist < dijdist[i] + dijdist[i] = beam_close ? valencia_beam_dist : dijdist[i] + nni[i] = beam_close ? 0 : nni[i] +end + +@inline function update_nn_cross_arrays!(nndist::AbstractVector, nni::AbstractVector, + nx::AbstractVector, ny::AbstractVector, nz::AbstractVector, + E2p::AbstractVector, dijdist::AbstractVector, + i::Integer, N::Integer, algorithm::JetAlgorithm.Algorithm, + dij_factor, β = 1.0, γ = 1.0, R = 4.0) + return update_nn_cross_arrays!(nndist, nni, nx, ny, nz, E2p, dijdist, i, N, Val(algorithm), dij_factor, β, γ, R) +end + +@inline function update_nn_cross_arrays!(nndist::AbstractVector, nni::AbstractVector, + nx::AbstractVector, ny::AbstractVector, nz::AbstractVector, + E2p::AbstractVector, dijdist::AbstractVector, + i::Integer, N::Integer, ::Val{JetAlgorithm.Durham}, + dij_factor, β = 1.0, γ = 1.0, R = 4.0) + nndist[i] = large_distance + nni[i] = i + E2p_i = E2p[i] + @inbounds for j in 1:(i-1) + this_metric = angular_distance_arrays(nx, ny, nz, i, j) + better_nndist_i = this_metric < nndist[i] + nndist[i] = better_nndist_i ? this_metric : nndist[i] + nni[i] = better_nndist_i ? j : nni[i] + if this_metric < nndist[j] + nndist[j] = this_metric + nni[j] = i + E2p_j = E2p[j] + dijdist[j] = (E2p_i < E2p_j ? E2p_i : E2p_j) * dij_factor * this_metric + end + end + @inbounds for j in (i+1):N + this_metric = angular_distance_arrays(nx, ny, nz, i, j) + better_nndist_i = this_metric < nndist[i] + nndist[i] = better_nndist_i ? this_metric : nndist[i] + nni[i] = better_nndist_i ? j : nni[i] + if this_metric < nndist[j] + nndist[j] = this_metric + nni[j] = i + E2p_j = E2p[j] + minE2p = E2p_i < E2p_j ? E2p_i : E2p_j + dijdist[j] = minE2p * dij_factor * this_metric + end + end + E2p_nni = E2p[nni[i]] + minE2p_i = E2p_i < E2p_nni ? E2p_i : E2p_nni + dijdist[i] = minE2p_i * dij_factor * nndist[i] +end + +@inline function update_nn_cross_arrays!(nndist::AbstractVector, nni::AbstractVector, + nx::AbstractVector, ny::AbstractVector, nz::AbstractVector, + E2p::AbstractVector, dijdist::AbstractVector, + i::Integer, N::Integer, ::Val{JetAlgorithm.EEKt}, + dij_factor, β = 1.0, γ = 1.0, R = 4.0) + E2p_i = E2p[i] + nndist[i] = large_distance + nni[i] = i + @inbounds for j in 1:(i-1) + this_metric = angular_distance_arrays(nx, ny, nz, i, j) + better_nndist_i = this_metric < nndist[i] + nndist[i] = better_nndist_i ? this_metric : nndist[i] + nni[i] = better_nndist_i ? j : nni[i] + if this_metric < nndist[j] + nndist[j] = this_metric + nni[j] = i + E2p_j = E2p[j] + new_dij = (E2p_i < E2p_j ? E2p_i : E2p_j) * dij_factor * this_metric + if E2p_j < new_dij + dijdist[j] = E2p_j + nni[j] = 0 + else + dijdist[j] = new_dij + end + end + end + @inbounds for j in (i+1):N + this_metric = angular_distance_arrays(nx, ny, nz, i, j) + better_nndist_i = this_metric < nndist[i] + nndist[i] = better_nndist_i ? this_metric : nndist[i] + nni[i] = better_nndist_i ? j : nni[i] + if this_metric < nndist[j] + nndist[j] = this_metric + nni[j] = i + E2p_j = E2p[j] + minE2p = E2p_i < E2p_j ? E2p_i : E2p_j + dijdist[j] = minE2p * dij_factor * this_metric + if E2p_j < dijdist[j] + dijdist[j] = E2p_j + nni[j] = 0 + end + end + end + E2p_nni = E2p[nni[i]] + minE2p_i = E2p_i < E2p_nni ? E2p_i : E2p_nni + dijdist[i] = minE2p_i * dij_factor * nndist[i] + beam_close = E2p_i < dijdist[i] + dijdist[i] = beam_close ? E2p_i : dijdist[i] + nni[i] = beam_close ? 0 : nni[i] +end + +@inline function update_nn_cross_arrays!(nndist::AbstractVector, nni::AbstractVector, + nx::AbstractVector, ny::AbstractVector, nz::AbstractVector, + E2p::AbstractVector, dijdist::AbstractVector, + i::Integer, N::Integer, ::Val{JetAlgorithm.Valencia}, + dij_factor, β = 1.0, γ = 1.0, R = 4.0) + nndist[i] = Inf + nni[i] = i + invR2 = inv(R * R) + @inbounds for j in 1:(i-1) + this_metric = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, j, invR2) + better_nndist_i = this_metric < nndist[i] + nndist[i] = better_nndist_i ? this_metric : nndist[i] + nni[i] = better_nndist_i ? j : nni[i] + if this_metric < nndist[j] + nndist[j] = this_metric + nni[j] = i + dijdist[j] = this_metric + valencia_beam_dist = valencia_beam_distance_arrays(E2p, nz, j, γ, β) + if valencia_beam_dist < dijdist[j] + dijdist[j] = valencia_beam_dist + nni[j] = 0 + end + end + end + @inbounds for j in (i+1):N + this_metric = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, j, invR2) + better_nndist_i = this_metric < nndist[i] + nndist[i] = better_nndist_i ? this_metric : nndist[i] + nni[i] = better_nndist_i ? j : nni[i] + if this_metric < nndist[j] + nndist[j] = this_metric + nni[j] = i + dijdist[j] = this_metric + valencia_beam_dist = valencia_beam_distance_arrays(E2p, nz, j, γ, β) + if valencia_beam_dist < dijdist[j] + dijdist[j] = valencia_beam_dist + nni[j] = 0 + end + end + end + dijdist[i] = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, nni[i], invR2) + valencia_beam_dist = valencia_beam_distance_arrays(E2p, nz, i, γ, β) + beam_close = valencia_beam_dist < dijdist[i] + dijdist[i] = beam_close ? valencia_beam_dist : dijdist[i] + nni[i] = beam_close ? 0 : nni[i] +end + Base.@propagate_inbounds @inline function fill_reco_array!(eereco, particles, R2, p) @inbounds for i in eachindex(particles) eereco.index[i] = i @@ -517,17 +776,8 @@ Base.@propagate_inbounds @inline function fill_reco_array!(eereco, particles, R2 eereco.nx[i] = nx(particles[i]) eereco.ny[i] = ny(particles[i]) eereco.nz[i] = nz(particles[i]) - E = energy(particles[i]) - if p isa Int - if p == 1 - eereco.E2p[i] = E * E - else - E2 = E * E - eereco.E2p[i] = E2^p - end - else - eereco.E2p[i] = E^(2p) - end + eereco.E2p[i] = energy(particles[i])^(2p) + # No precomputed beam factor; compute on demand to preserve previous behaviour end end @@ -655,9 +905,227 @@ function ee_genkt_algorithm(particles::AbstractVector{T}; algorithm::JetAlgorith end # Now call the actual reconstruction method, tuned for our internal EDM - _ee_genkt_algorithm(particles = recombination_particles, p = p, R = R, - algorithm = algorithm, - recombine = recombine, γ = γ) + # Dispatch once on the algorithm to call a small number of specialised + # internal implementations. This avoids per-iteration branching/Val() in + # the hot inner loops. + if algorithm == JetAlgorithm.Valencia + return _ee_genkt_algorithm_valencia(particles = recombination_particles, p = p, R = R, + algorithm = algorithm, + recombine = recombine, γ = γ) + elseif algorithm == JetAlgorithm.Durham + return _ee_genkt_algorithm_durham(particles = recombination_particles, p = p, R = R, + algorithm = algorithm, + recombine = recombine, γ = γ) + else + return _ee_genkt_algorithm(particles = recombination_particles, p = p, R = R, + algorithm = algorithm, + recombine = recombine, γ = γ) + end +end + +################################################################################ +# Durham-specialised implementation (optimized inner loops using array helpers) +################################################################################ +function _ee_genkt_algorithm_durham(; particles::AbstractVector{EEJet}, + algorithm::JetAlgorithm.Algorithm, p::Real, R = 4.0, + recombine = addjets, γ::Real = 1.0, + beta::Union{Real, Nothing} = nothing) + # Bounds + N::Int = length(particles) + + R2 = R^2 + if algorithm == JetAlgorithm.Valencia && beta !== nothing + p = beta + end + + # Durham dij factor + dij_factor = 2.0 + + # Prepare SoA + eereco = StructArray{EERecoJet}(undef, N) + fill_reco_array!(eereco, particles, R2, p) + + # Setup history + history, Qtot = initial_history(particles) + clusterseq = ClusterSequence(algorithm, p, R, RecoStrategy.N2Plain, particles, history, + Qtot) + + # Initial nearest neighbours (Durham-specialised) + get_angular_nearest_neighbours!(eereco, Val(JetAlgorithm.Durham), dij_factor, p, γ, R) + + # Alias StructArray fields to local vectors to avoid repeated getindex + index = eereco.index; nni = eereco.nni; nndist = eereco.nndist + dijdist = eereco.dijdist; nx = eereco.nx; ny = eereco.ny; nz = eereco.nz + E2p = eereco.E2p + + # Main loop + iter = 0 + while N != 0 + iter += 1 + + dij_min, ijetA = fast_findmin(dijdist, N) + ijetB = nni[ijetA] + + if ijetB == 0 + ijetB = ijetA + add_step_to_history!(clusterseq, + clusterseq.jets[index[ijetA]]._cluster_hist_index, + BeamJet, Invalid, dij_min) + elseif N == 1 + ijetB = ijetA + add_step_to_history!(clusterseq, + clusterseq.jets[eereco[ijetA].index]._cluster_hist_index, + BeamJet, Invalid, dij_min) + else + if ijetB < ijetA + ijetA, ijetB = ijetB, ijetA + end + jetA = clusterseq.jets[index[ijetA]] + jetB = clusterseq.jets[index[ijetB]] + merged_jet = recombine(jetA, jetB; + cluster_hist_index = length(clusterseq.history) + 1) + push!(clusterseq.jets, merged_jet) + newjet_k = length(clusterseq.jets) + add_step_to_history!(clusterseq, + minmax(cluster_hist_index(jetA), + cluster_hist_index(jetB))..., + newjet_k, dij_min) + insert_new_jet!(eereco, ijetA, newjet_k, R2, merged_jet, p) + end + + if ijetB != N + copy_to_slot!(eereco, N, ijetB) + end + + N -= 1 + + # Update nearest neighbours step using array-based helpers specialised for Durham + for i in 1:N + if (ijetB != N + 1) && (nni[i] == N + 1) + nni[i] = ijetB + else + if (nni[i] == ijetA) || (nni[i] == ijetB) || (nni[i] > N) + update_nn_no_cross_arrays!(nndist, nni, nx, ny, nz, E2p, dijdist, + i, N, Val(JetAlgorithm.Durham), dij_factor, p, γ, R) + end + end + end + + if ijetA != ijetB + update_nn_cross_arrays!(nndist, nni, nx, ny, nz, E2p, dijdist, + ijetA, N, Val(JetAlgorithm.Durham), dij_factor, p, γ, R) + end + end + + clusterseq +end + +################################################################################ +# Valencia-specialised implementation +################################################################################ +function _ee_genkt_algorithm_valencia(; particles::AbstractVector{EEJet}, + algorithm::JetAlgorithm.Algorithm, p::Real, R = 4.0, + recombine = addjets, γ::Real = 1.0, + beta::Union{Real, Nothing} = nothing) + # Bounds + N::Int = length(particles) + + R2 = R^2 + # Valencia uses p as β when passed through + if algorithm == JetAlgorithm.Valencia && beta !== nothing + p = beta + end + + # Constant factor for the Valencia dij metric + dij_factor = 1.0 + + # For optimised reconstruction generate an SoA containing the necessary + # jet information and populate it accordingly + eereco = StructArray{EERecoJet}(undef, N) + + fill_reco_array!(eereco, particles, R2, p) + + # Setup the initial history and get the total energy + history, Qtot = initial_history(particles) + + clusterseq = ClusterSequence(algorithm, p, R, RecoStrategy.N2Plain, particles, history, + Qtot) + + # Run over initial pairs of jets to find nearest neighbours + # Call Valencia-specialised nearest-neighbour search once + get_angular_nearest_neighbours!(eereco, Val(JetAlgorithm.Valencia), dij_factor, p, γ, R) + + # Alias StructArray fields to local vectors to avoid repeated getindex + index = eereco.index; nni = eereco.nni; nndist = eereco.nndist + dijdist = eereco.dijdist; nx = eereco.nx; ny = eereco.ny; nz = eereco.nz + E2p = eereco.E2p + + # Now we can start the main loop + iter = 0 + while N != 0 + iter += 1 + + dij_min, ijetA = fast_findmin(dijdist, N) + ijetB = nni[ijetA] + + # Now we check if there is a "beam" merge possibility + if ijetB == 0 + # Shouldn't happen for Valencia (beam handled via valencia_beam checks) + ijetB = ijetA + add_step_to_history!(clusterseq, + clusterseq.jets[index[ijetA]]._cluster_hist_index, + BeamJet, Invalid, dij_min) + elseif N == 1 + ijetB = ijetA + add_step_to_history!(clusterseq, + clusterseq.jets[eereco[ijetA].index]._cluster_hist_index, + BeamJet, Invalid, dij_min) + else + if ijetB < ijetA + ijetA, ijetB = ijetB, ijetA + end + + jetA = clusterseq.jets[index[ijetA]] + jetB = clusterseq.jets[index[ijetB]] + + merged_jet = recombine(jetA, jetB; + cluster_hist_index = length(clusterseq.history) + 1) + + push!(clusterseq.jets, merged_jet) + newjet_k = length(clusterseq.jets) + add_step_to_history!(clusterseq, + minmax(cluster_hist_index(jetA), + cluster_hist_index(jetB))..., + newjet_k, dij_min) + + insert_new_jet!(eereco, ijetA, newjet_k, R2, merged_jet, p) + end + + if ijetB != N + copy_to_slot!(eereco, N, ijetB) + end + + N -= 1 + + # Update nearest neighbours step + for i in 1:N + if (ijetB != N + 1) && (nni[i] == N + 1) + nni[i] = ijetB + else + if (nni[i] == ijetA) || (nni[i] == ijetB) || (nni[i] > N) + update_nn_no_cross_arrays!(nndist, nni, nx, ny, nz, E2p, dijdist, + i, N, Val(JetAlgorithm.Valencia), dij_factor, p, γ, R) + end + end + end + + if ijetA != ijetB + update_nn_cross_arrays!(nndist, nni, nx, ny, nz, E2p, dijdist, + ijetA, N, Val(JetAlgorithm.Valencia), dij_factor, p, γ, R) + end + end + + clusterseq end """ @@ -694,6 +1162,13 @@ function _ee_genkt_algorithm(; particles::AbstractVector{EEJet}, # Bounds N::Int = length(particles) + # If Valencia requested, forward to the Valencia-specialised path + if algorithm == JetAlgorithm.Valencia + return _ee_genkt_algorithm_valencia(particles = particles, algorithm = algorithm, + p = p, R = R, recombine = recombine, + γ = γ, beta = beta) + end + R2 = R^2 if algorithm == JetAlgorithm.Valencia && beta !== nothing p = beta @@ -708,8 +1183,6 @@ function _ee_genkt_algorithm(; particles::AbstractVector{EEJet}, else dij_factor = 1 / (3 + cos(R)) end - elseif algorithm == JetAlgorithm.Valencia - dij_factor = 1.0 # Valencia distance function contains complete formula with /R² division else throw(ArgumentError("Algorithm $algorithm not supported for e+e-")) end @@ -777,11 +1250,7 @@ function _ee_genkt_algorithm(; particles::AbstractVector{EEJet}, newjet_k, dij_min) # Update the compact arrays, reusing the JetA slot - if algorithm == JetAlgorithm.Valencia - insert_new_jet!(eereco, ijetA, newjet_k, R2, merged_jet, p) # Use p (β) for Valencia energy powers - else - insert_new_jet!(eereco, ijetA, newjet_k, R2, merged_jet, p) # Use p for other algorithms - end + insert_new_jet!(eereco, ijetA, newjet_k, R2, merged_jet, p) end # Squash step - copy the final jet's compact data into the jetB slot diff --git a/src/EEJet.jl b/src/EEJet.jl index a28aeee1..50a19903 100644 --- a/src/EEJet.jl +++ b/src/EEJet.jl @@ -22,6 +22,8 @@ struct EEJet <: FourMomentum _cluster_hist_index::Int end + # Compatibility constructor was moved below the EERecoJet type definition + """ EEJet(px::Real, py::Real, pz::Real, E::Real, cluster_hist_index::Int) @@ -109,3 +111,4 @@ mutable struct EERecoJet nz::Float64 E2p::Float64 end + From 740396d80eb9adcc6dd4e2d6420ebd43ce8a6b0f Mon Sep 17 00:00:00 2001 From: Matt LeBlanc Date: Sun, 17 Aug 2025 14:37:20 -0400 Subject: [PATCH 12/32] Optimization for VLC --- src/EEAlgorithm.jl | 152 +++++++++++++++++++++++++++++++-------------- 1 file changed, 104 insertions(+), 48 deletions(-) diff --git a/src/EEAlgorithm.jl b/src/EEAlgorithm.jl index 6a703b5f..f9dcd2e4 100644 --- a/src/EEAlgorithm.jl +++ b/src/EEAlgorithm.jl @@ -215,9 +215,12 @@ function get_angular_nearest_neighbours!(eereco, algorithm, dij_factor, p, γ = eereco.nni[i] = beam_closer ? 0 : eereco.nni[i] end elseif algorithm == JetAlgorithm.Valencia + # Use array-based helper to avoid StructArray property checks and + # reduce per-iteration overhead in the hot loop. + E2p = eereco.E2p; nz = eereco.nz @inbounds for i in 1:N - valencia_beam_dist = valencia_beam_distance(eereco, i, γ, p) - beam_closer = valencia_beam_dist < eereco[i].dijdist + valencia_beam_dist = valencia_beam_distance_arrays(E2p, nz, i, γ, p) + beam_closer = valencia_beam_dist < eereco.dijdist[i] eereco.dijdist[i] = beam_closer ? valencia_beam_dist : eereco.dijdist[i] eereco.nni[i] = beam_closer ? 0 : eereco.nni[i] end @@ -605,22 +608,29 @@ end E2p::AbstractVector, dijdist::AbstractVector, i::Integer, N::Integer, ::Val{JetAlgorithm.Valencia}, dij_factor, β = 1.0, γ = 1.0, R = 4.0) - nndist[i] = Inf - nni[i] = i + # Use locals for hot per-slot values to reduce repeated setindex! traffic invR2 = inv(R * R) + nndist_i = Inf + nni_i = i @inbounds for j in 1:N if j != i this_metric = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, j, invR2) - better_nndist_i = this_metric < nndist[i] - nndist[i] = better_nndist_i ? this_metric : nndist[i] - nni[i] = better_nndist_i ? j : nni[i] + if this_metric < nndist_i + nndist_i = this_metric + nni_i = j + end end end - dijdist[i] = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, nni[i], invR2) + # compute dijdist and beam check using locals then write back once + dijdist_i = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, nni_i, invR2) valencia_beam_dist = valencia_beam_distance_arrays(E2p, nz, i, γ, β) - beam_close = valencia_beam_dist < dijdist[i] - dijdist[i] = beam_close ? valencia_beam_dist : dijdist[i] - nni[i] = beam_close ? 0 : nni[i] + if valencia_beam_dist < dijdist_i + dijdist_i = valencia_beam_dist + nni_i = 0 + end + nndist[i] = nndist_i + nni[i] = nni_i + dijdist[i] = dijdist_i end @inline function update_nn_cross_arrays!(nndist::AbstractVector, nni::AbstractVector, @@ -725,14 +735,17 @@ end E2p::AbstractVector, dijdist::AbstractVector, i::Integer, N::Integer, ::Val{JetAlgorithm.Valencia}, dij_factor, β = 1.0, γ = 1.0, R = 4.0) - nndist[i] = Inf - nni[i] = i + # Operate on locals for slot i to reduce setindex traffic; updates to other + # slots (j) still write directly since they modify different indices. invR2 = inv(R * R) + nndist_i = Inf + nni_i = i @inbounds for j in 1:(i-1) this_metric = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, j, invR2) - better_nndist_i = this_metric < nndist[i] - nndist[i] = better_nndist_i ? this_metric : nndist[i] - nni[i] = better_nndist_i ? j : nni[i] + if this_metric < nndist_i + nndist_i = this_metric + nni_i = j + end if this_metric < nndist[j] nndist[j] = this_metric nni[j] = i @@ -746,9 +759,10 @@ end end @inbounds for j in (i+1):N this_metric = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, j, invR2) - better_nndist_i = this_metric < nndist[i] - nndist[i] = better_nndist_i ? this_metric : nndist[i] - nni[i] = better_nndist_i ? j : nni[i] + if this_metric < nndist_i + nndist_i = this_metric + nni_i = j + end if this_metric < nndist[j] nndist[j] = this_metric nni[j] = i @@ -760,11 +774,16 @@ end end end end - dijdist[i] = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, nni[i], invR2) + # Finalize slot i + dijdist_i = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, nni_i, invR2) valencia_beam_dist = valencia_beam_distance_arrays(E2p, nz, i, γ, β) - beam_close = valencia_beam_dist < dijdist[i] - dijdist[i] = beam_close ? valencia_beam_dist : dijdist[i] - nni[i] = beam_close ? 0 : nni[i] + if valencia_beam_dist < dijdist_i + dijdist_i = valencia_beam_dist + nni_i = 0 + end + nndist[i] = nndist_i + nni[i] = nni_i + dijdist[i] = dijdist_i end Base.@propagate_inbounds @inline function fill_reco_array!(eereco, particles, R2, p) @@ -960,11 +979,11 @@ function _ee_genkt_algorithm_durham(; particles::AbstractVector{EEJet}, # Main loop iter = 0 - while N != 0 + @inbounds while N != 0 iter += 1 - dij_min, ijetA = fast_findmin(dijdist, N) - ijetB = nni[ijetA] + dij_min, ijetA = fast_findmin(dijdist, N) + ijetB = nni[ijetA] if ijetB == 0 ijetB = ijetA @@ -1055,38 +1074,46 @@ function _ee_genkt_algorithm_valencia(; particles::AbstractVector{EEJet}, # Call Valencia-specialised nearest-neighbour search once get_angular_nearest_neighbours!(eereco, Val(JetAlgorithm.Valencia), dij_factor, p, γ, R) - # Alias StructArray fields to local vectors to avoid repeated getindex - index = eereco.index; nni = eereco.nni; nndist = eereco.nndist - dijdist = eereco.dijdist; nx = eereco.nx; ny = eereco.ny; nz = eereco.nz - E2p = eereco.E2p + # Alias StructArray fields into local vectors to avoid allocations from + # copying while still avoiding repeated StructArray field lookups in + # hot loops. These locals point directly at the underlying vectors, so + # no explicit writeback is required at the end. + indexv = eereco.index + nni_v = eereco.nni + nndist_v = eereco.nndist + dijdist_v = eereco.dijdist + nxv = eereco.nx + nyv = eereco.ny + nzv = eereco.nz + E2pv = eereco.E2p # Now we can start the main loop iter = 0 while N != 0 iter += 1 - dij_min, ijetA = fast_findmin(dijdist, N) - ijetB = nni[ijetA] + dij_min, ijetA = fast_findmin(dijdist_v, N) + ijetB = nni_v[ijetA] # Now we check if there is a "beam" merge possibility - if ijetB == 0 + if ijetB == 0 # Shouldn't happen for Valencia (beam handled via valencia_beam checks) ijetB = ijetA - add_step_to_history!(clusterseq, - clusterseq.jets[index[ijetA]]._cluster_hist_index, - BeamJet, Invalid, dij_min) + add_step_to_history!(clusterseq, + clusterseq.jets[indexv[ijetA]]._cluster_hist_index, + BeamJet, Invalid, dij_min) elseif N == 1 ijetB = ijetA add_step_to_history!(clusterseq, - clusterseq.jets[eereco[ijetA].index]._cluster_hist_index, + clusterseq.jets[indexv[ijetA]]._cluster_hist_index, BeamJet, Invalid, dij_min) else if ijetB < ijetA ijetA, ijetB = ijetB, ijetA end - jetA = clusterseq.jets[index[ijetA]] - jetB = clusterseq.jets[index[ijetB]] + jetA = clusterseq.jets[indexv[ijetA]] + jetB = clusterseq.jets[indexv[ijetB]] merged_jet = recombine(jetA, jetB; cluster_hist_index = length(clusterseq.history) + 1) @@ -1098,33 +1125,62 @@ function _ee_genkt_algorithm_valencia(; particles::AbstractVector{EEJet}, cluster_hist_index(jetB))..., newjet_k, dij_min) - insert_new_jet!(eereco, ijetA, newjet_k, R2, merged_jet, p) + # Insert merged jet into our local SoA (avoid writing StructArray) + indexv[ijetA] = newjet_k + nni_v[ijetA] = 0 + nndist_v[ijetA] = R2 + nxv[ijetA] = nx(merged_jet) + nyv[ijetA] = ny(merged_jet) + nzv[ijetA] = nz(merged_jet) + # Compute E2p like insert_new_jet! would + E = energy(merged_jet) + if p isa Int + if p == 1 + E2pv[ijetA] = E * E + else + E2 = E * E + E2pv[ijetA] = E2^p + end + else + E2pv[ijetA] = E^(2p) + end end if ijetB != N - copy_to_slot!(eereco, N, ijetB) + # Local copy from slot N -> slot ijetB (avoid StructArray ops) + indexv[ijetB] = indexv[N] + nni_v[ijetB] = nni_v[N] + nndist_v[ijetB] = nndist_v[N] + dijdist_v[ijetB] = dijdist_v[N] + nxv[ijetB] = nxv[N] + nyv[ijetB] = nyv[N] + nzv[ijetB] = nzv[N] + E2pv[ijetB] = E2pv[N] end N -= 1 - # Update nearest neighbours step - for i in 1:N - if (ijetB != N + 1) && (nni[i] == N + 1) - nni[i] = ijetB + # Update nearest neighbours step + @inbounds for i in 1:N + if (ijetB != N + 1) && (nni_v[i] == N + 1) + nni_v[i] = ijetB else - if (nni[i] == ijetA) || (nni[i] == ijetB) || (nni[i] > N) - update_nn_no_cross_arrays!(nndist, nni, nx, ny, nz, E2p, dijdist, + if (nni_v[i] == ijetA) || (nni_v[i] == ijetB) || (nni_v[i] > N) + update_nn_no_cross_arrays!(nndist_v, nni_v, nxv, nyv, nzv, E2pv, dijdist_v, i, N, Val(JetAlgorithm.Valencia), dij_factor, p, γ, R) end end end if ijetA != ijetB - update_nn_cross_arrays!(nndist, nni, nx, ny, nz, E2p, dijdist, + update_nn_cross_arrays!(nndist_v, nni_v, nxv, nyv, nzv, E2pv, dijdist_v, ijetA, N, Val(JetAlgorithm.Valencia), dij_factor, p, γ, R) end end + # Locals alias the StructArray fields directly, so there is no separate + # writeback step required here. + clusterseq end From d17cdd71406422dce810f992c2fe18d6ba5ed50d Mon Sep 17 00:00:00 2001 From: Matt LeBlanc Date: Sun, 17 Aug 2025 15:44:14 -0400 Subject: [PATCH 13/32] VLC optimization --- src/EEAlgorithm.jl | 188 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 177 insertions(+), 11 deletions(-) diff --git a/src/EEAlgorithm.jl b/src/EEAlgorithm.jl index f9dcd2e4..43b00f3a 100644 --- a/src/EEAlgorithm.jl +++ b/src/EEAlgorithm.jl @@ -70,6 +70,12 @@ Base.@propagate_inbounds @inline function valencia_distance_inv_arrays(E2p, nx, min(E2p[i], E2p[j]) * 2 * angular_dist * invR2 end +# Scaled variant: accepts pre-multiplied E2p_scaled = E2p * (2 * invR2) +Base.@propagate_inbounds @inline function valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, j) + angular_dist = angular_distance_arrays(nx, ny, nz, i, j) + min(E2p_scaled[i], E2p_scaled[j]) * angular_dist +end + """ valencia_beam_distance(eereco, i, γ, β) -> Float64 @@ -283,13 +289,15 @@ end @inline function get_angular_nearest_neighbours!(eereco, ::Val{JetAlgorithm.Valencia}, dij_factor, p, γ = 1.0, R = 4.0) + # Fallback Val-specialised implementation kept for non-precomputed use. + # The Valencia entrypoint uses a precomputed path (see _ee_genkt_algorithm_valencia) N = length(eereco) E2p = eereco.E2p; nx = eereco.nx; ny = eereco.ny; nz = eereco.nz nndist = eereco.nndist; nni = eereco.nni invR2 = inv(R * R) - @inbounds for i in 1:N - nndist[i] = Inf - nni[i] = i + @inbounds for i in 1:N + nndist[i] = Inf + nni[i] = i end @inbounds for i in 1:N local_nndist_i = nndist[i] @@ -319,6 +327,45 @@ end end end +## Precomputed Valencia nearest-neighbour initializer using precomputed arrays +Base.@propagate_inbounds @inline function get_angular_nearest_neighbours_valencia_precomputed!(eereco, + E2p_scaled::AbstractVector, + beam_term::AbstractVector, + p, γ = 1.0, R = 4.0) + N = length(eereco) + nx = eereco.nx; ny = eereco.ny; nz = eereco.nz + nndist = eereco.nndist; nni = eereco.nni + @inbounds for i in 1:N + nndist[i] = Inf + nni[i] = i + end + @inbounds for i in 1:N + local_nndist_i = nndist[i] + local_nni_i = nni[i] + @inbounds for j in (i + 1):N + this_metric = valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, j) + if this_metric < local_nndist_i + local_nndist_i = this_metric + local_nni_i = j + end + if this_metric < nndist[j] + nndist[j] = this_metric + nni[j] = i + end + end + nndist[i] = local_nndist_i + nni[i] = local_nni_i + end + @inbounds for i in 1:N + eereco.dijdist[i] = valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, nni[i]) + end + @inbounds for i in 1:N + beam_closer = beam_term[i] < eereco.dijdist[i] + eereco.dijdist[i] = beam_closer ? beam_term[i] : eereco.dijdist[i] + eereco.nni[i] = beam_closer ? 0 : eereco.nni[i] + end +end + # Forwarder to Val-specialized version @inline function get_angular_nearest_neighbours!(eereco, algorithm::JetAlgorithm.Algorithm, @@ -608,7 +655,7 @@ end E2p::AbstractVector, dijdist::AbstractVector, i::Integer, N::Integer, ::Val{JetAlgorithm.Valencia}, dij_factor, β = 1.0, γ = 1.0, R = 4.0) - # Use locals for hot per-slot values to reduce repeated setindex! traffic + # Precomputed variant lives in a separate function; keep this fallback here. invR2 = inv(R * R) nndist_i = Inf nni_i = i @@ -633,6 +680,34 @@ end dijdist[i] = dijdist_i end +## Precomputed Valencia no-cross update using E2p_scaled and beam_term +@inline function update_nn_no_cross_arrays_precomputed!(nndist::AbstractVector, nni::AbstractVector, + nx::AbstractVector, ny::AbstractVector, nz::AbstractVector, + E2p_scaled::AbstractVector, beam_term::AbstractVector, + dijdist::AbstractVector, + i::Integer, N::Integer, + dij_factor, β = 1.0, γ = 1.0, R = 4.0) + nndist_i = Inf + nni_i = i + @inbounds for j in 1:N + if j != i + this_metric = valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, j) + if this_metric < nndist_i + nndist_i = this_metric + nni_i = j + end + end + end + dijdist_i = valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, nni_i) + if beam_term[i] < dijdist_i + dijdist_i = beam_term[i] + nni_i = 0 + end + nndist[i] = nndist_i + nni[i] = nni_i + dijdist[i] = dijdist_i +end + @inline function update_nn_cross_arrays!(nndist::AbstractVector, nni::AbstractVector, nx::AbstractVector, ny::AbstractVector, nz::AbstractVector, E2p::AbstractVector, dijdist::AbstractVector, @@ -737,6 +812,8 @@ end dij_factor, β = 1.0, γ = 1.0, R = 4.0) # Operate on locals for slot i to reduce setindex traffic; updates to other # slots (j) still write directly since they modify different indices. + # Precomputed variant fallback uses the non-precomputed helpers; there is + # a separate precomputed cross-update below. invR2 = inv(R * R) nndist_i = Inf nni_i = i @@ -786,6 +863,58 @@ end dijdist[i] = dijdist_i end +## Precomputed Valencia cross-update using E2p_scaled and beam_term +@inline function update_nn_cross_arrays_precomputed!(nndist::AbstractVector, nni::AbstractVector, + nx::AbstractVector, ny::AbstractVector, nz::AbstractVector, + E2p_scaled::AbstractVector, beam_term::AbstractVector, + dijdist::AbstractVector, + i::Integer, N::Integer, + dij_factor, β = 1.0, γ = 1.0, R = 4.0) + nndist_i = Inf + nni_i = i + @inbounds for j in 1:(i-1) + this_metric = valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, j) + if this_metric < nndist_i + nndist_i = this_metric + nni_i = j + end + if this_metric < nndist[j] + nndist[j] = this_metric + nni[j] = i + dijdist[j] = this_metric + if beam_term[j] < dijdist[j] + dijdist[j] = beam_term[j] + nni[j] = 0 + end + end + end + @inbounds for j in (i+1):N + this_metric = valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, j) + if this_metric < nndist_i + nndist_i = this_metric + nni_i = j + end + if this_metric < nndist[j] + nndist[j] = this_metric + nni[j] = i + dijdist[j] = this_metric + if beam_term[j] < dijdist[j] + dijdist[j] = beam_term[j] + nni[j] = 0 + end + end + end + # Finalize slot i + dijdist_i = valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, nni_i) + if beam_term[i] < dijdist_i + dijdist_i = beam_term[i] + nni_i = 0 + end + nndist[i] = nndist_i + nni[i] = nni_i + dijdist[i] = dijdist_i +end + Base.@propagate_inbounds @inline function fill_reco_array!(eereco, particles, R2, p) @inbounds for i in eachindex(particles) eereco.index[i] = i @@ -1070,9 +1199,9 @@ function _ee_genkt_algorithm_valencia(; particles::AbstractVector{EEJet}, clusterseq = ClusterSequence(algorithm, p, R, RecoStrategy.N2Plain, particles, history, Qtot) - # Run over initial pairs of jets to find nearest neighbours - # Call Valencia-specialised nearest-neighbour search once - get_angular_nearest_neighbours!(eereco, Val(JetAlgorithm.Valencia), dij_factor, p, γ, R) + # Run over initial pairs of jets to find nearest neighbours (precomputed path) + # prepare precomputed arrays below before calling the initialized helper + # Alias StructArray fields into local vectors to avoid allocations from # copying while still avoiding repeated StructArray field lookups in @@ -1087,6 +1216,27 @@ function _ee_genkt_algorithm_valencia(; particles::AbstractVector{EEJet}, nzv = eereco.nz E2pv = eereco.E2p + # Precompute scaled E2p and beam_term for Valencia to avoid repeated work + invR2 = inv(R * R) + factor = 2 * invR2 + E2p_scaled = similar(E2pv) + beam_term = similar(E2pv) + @inbounds for k in 1:N + E2p_scaled[k] = E2pv[k] * factor + nz_k = nzv[k] + sin2 = 1.0 - nz_k * nz_k + if γ == 1.0 + beam_term[k] = E2pv[k] * sin2 + elseif γ == 2.0 + beam_term[k] = E2pv[k] * (sin2 * sin2) + else + beam_term[k] = E2pv[k] * sin2^γ + end + end + + # Now run NN init using precomputed helpers + get_angular_nearest_neighbours_valencia_precomputed!(eereco, E2p_scaled, beam_term, p, γ, R) + # Now we can start the main loop iter = 0 while N != 0 @@ -1144,6 +1294,17 @@ function _ee_genkt_algorithm_valencia(; particles::AbstractVector{EEJet}, else E2pv[ijetA] = E^(2p) end + # Recompute precomputed derived arrays for the merged slot + E2p_scaled[ijetA] = E2pv[ijetA] * factor + nz_k = nzv[ijetA] + sin2_k = 1.0 - nz_k * nz_k + if γ == 1.0 + beam_term[ijetA] = E2pv[ijetA] * sin2_k + elseif γ == 2.0 + beam_term[ijetA] = E2pv[ijetA] * (sin2_k * sin2_k) + else + beam_term[ijetA] = E2pv[ijetA] * sin2_k^γ + end end if ijetB != N @@ -1156,6 +1317,9 @@ function _ee_genkt_algorithm_valencia(; particles::AbstractVector{EEJet}, nyv[ijetB] = nyv[N] nzv[ijetB] = nzv[N] E2pv[ijetB] = E2pv[N] + # Also copy precomputed derived arrays + E2p_scaled[ijetB] = E2p_scaled[N] + beam_term[ijetB] = beam_term[N] end N -= 1 @@ -1166,15 +1330,17 @@ function _ee_genkt_algorithm_valencia(; particles::AbstractVector{EEJet}, nni_v[i] = ijetB else if (nni_v[i] == ijetA) || (nni_v[i] == ijetB) || (nni_v[i] > N) - update_nn_no_cross_arrays!(nndist_v, nni_v, nxv, nyv, nzv, E2pv, dijdist_v, - i, N, Val(JetAlgorithm.Valencia), dij_factor, p, γ, R) + update_nn_no_cross_arrays_precomputed!(nndist_v, nni_v, nxv, nyv, nzv, + E2p_scaled, beam_term, dijdist_v, + i, N, dij_factor, p, γ, R) end end end if ijetA != ijetB - update_nn_cross_arrays!(nndist_v, nni_v, nxv, nyv, nzv, E2pv, dijdist_v, - ijetA, N, Val(JetAlgorithm.Valencia), dij_factor, p, γ, R) + update_nn_cross_arrays_precomputed!(nndist_v, nni_v, nxv, nyv, nzv, + E2p_scaled, beam_term, dijdist_v, + ijetA, N, dij_factor, p, γ, R) end end From 0cd3d88690a8bcb2a59acd42bd7f12e03f50c3b5 Mon Sep 17 00:00:00 2001 From: Matt LeBlanc Date: Sun, 17 Aug 2025 16:31:43 -0400 Subject: [PATCH 14/32] VLC docstrings --- src/EEAlgorithm.jl | 160 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) diff --git a/src/EEAlgorithm.jl b/src/EEAlgorithm.jl index 43b00f3a..73d0ba93 100644 --- a/src/EEAlgorithm.jl +++ b/src/EEAlgorithm.jl @@ -70,6 +70,23 @@ Base.@propagate_inbounds @inline function valencia_distance_inv_arrays(E2p, nx, min(E2p[i], E2p[j]) * 2 * angular_dist * invR2 end +""" + valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, j) -> Float64 + +Compute the Valencia pairwise metric using a pre-multiplied energy factor. + +Arguments +- `E2p_scaled::AbstractVector`: precomputed per-jet values equal to E2p * (2 * invR2). +- `nx, ny, nz::AbstractVector`: direction cosine arrays for jets. +- `i, j::Integer`: indices of the two jets to compare. + +Returns +- `Float64`: the Valencia dij metric computed as min(E2p_scaled[i], E2p_scaled[j]) * (1 - cos θ_{ij}). + +Notes +- This helper assumes `E2p_scaled` already includes the factor `2 * inv(R^2)`, so the + function avoids multiplying by those constants inside hot inner loops. +""" # Scaled variant: accepts pre-multiplied E2p_scaled = E2p * (2 * invR2) Base.@propagate_inbounds @inline function valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, j) angular_dist = angular_distance_arrays(nx, ny, nz, i, j) @@ -327,6 +344,28 @@ end end end +""" + get_angular_nearest_neighbours_valencia_precomputed!(eereco, E2p_scaled, beam_term, p, γ, R) + +Initialize nearest-neighbour data for a Valencia clustering using precomputed +per-jet derived arrays. + +Arguments +- `eereco::StructArray{EERecoJet}`: the SoA of reconstruction slots to initialize. +- `E2p_scaled::AbstractVector`: per-jet pre-multiplied energy factor (E2p * 2 * inv(R^2)). +- `beam_term::AbstractVector`: per-jet Valencia beam-term precomputed as E2p * (1 - nz^2)^γ. +- `p`: energy exponent (β for Valencia). +- `γ::Real`: angular exponent used in the Valencia beam-term. +- `R::Real`: jet radius parameter (used indirectly via E2p_scaled construction). + +Behavior +- Sets `eereco.nndist`, `eereco.nni`, and `eereco.dijdist` using the provided + precomputed arrays. The function avoids repeated pow() and constant multiplications + inside the hot O(N²) initialization loop. + +This function is Valencia-only and intended to be called from the Valencia entrypoint +after `E2p_scaled` and `beam_term` have been populated. +""" ## Precomputed Valencia nearest-neighbour initializer using precomputed arrays Base.@propagate_inbounds @inline function get_angular_nearest_neighbours_valencia_precomputed!(eereco, E2p_scaled::AbstractVector, @@ -366,6 +405,29 @@ Base.@propagate_inbounds @inline function get_angular_nearest_neighbours_valenci end end +""" + get_angular_nearest_neighbours_valencia_precomputed!(eereco, E2p_scaled, beam_term, p, γ, R) + +Initialize nearest-neighbour data for a Valencia clustering using precomputed +per-jet derived arrays. + +Arguments +- `eereco::StructArray{EERecoJet}`: the SoA of reconstruction slots to initialize. +- `E2p_scaled::AbstractVector`: per-jet pre-multiplied energy factor (E2p * 2 * inv(R^2)). +- `beam_term::AbstractVector`: per-jet Valencia beam-term precomputed as E2p * (1 - nz^2)^γ. +- `p`: energy exponent (β for Valencia). +- `γ::Real`: angular exponent used in the Valencia beam-term. +- `R::Real`: jet radius parameter (used indirectly via E2p_scaled construction). + +Behavior +- Sets `eereco.nndist`, `eereco.nni`, and `eereco.dijdist` using the provided + precomputed arrays. The function avoids repeated pow() and constant multiplications + inside the hot O(N²) initialization loop. + +This function is Valencia-only and intended to be called from the Valencia entrypoint +after `E2p_scaled` and `beam_term` have been populated. +""" + # Forwarder to Val-specialized version @inline function get_angular_nearest_neighbours!(eereco, algorithm::JetAlgorithm.Algorithm, @@ -680,6 +742,30 @@ end dijdist[i] = dijdist_i end +""" + update_nn_no_cross_arrays_precomputed!(nndist, nni, nx, ny, nz, E2p_scaled, beam_term, dijdist, i, N, ...) + +Update nearest-neighbour information for slot `i` without attempting cross-updates +using precomputed per-jet derived arrays. + +Arguments +- `nndist, nni, dijdist::AbstractVector`: arrays representing current nearest-neighbour + distances, indices, and dij distances respectively. +- `nx, ny, nz::AbstractVector`: direction cosine arrays. +- `E2p_scaled::AbstractVector`: per-jet E2p pre-multiplied by 2 * inv(R^2). +- `beam_term::AbstractVector`: precomputed beam-term per jet. +- `i::Integer`: index of the slot to update. +- `N::Integer`: current number of active slots. +- additional params: `dij_factor`, `β`, `γ`, `R` (kept for compatibility/signature parity). + +Behavior +- Scans other active slots to find the nearest neighbour for slot `i` using + `valencia_distance_inv_scaled_arrays` (avoids repeated multiplies), applies the beam-term + comparison using `beam_term[i]`, and writes back `nndist[i]`, `nni[i]`, and `dijdist[i]`. + +This is a Valencia-only optimized path intended for the hot update loops inside +the Valencia reconstruction entrypoint. +""" ## Precomputed Valencia no-cross update using E2p_scaled and beam_term @inline function update_nn_no_cross_arrays_precomputed!(nndist::AbstractVector, nni::AbstractVector, nx::AbstractVector, ny::AbstractVector, nz::AbstractVector, @@ -708,6 +794,31 @@ end dijdist[i] = dijdist_i end +""" + update_nn_no_cross_arrays_precomputed!(nndist, nni, nx, ny, nz, E2p_scaled, beam_term, dijdist, i, N, ...) + +Update nearest-neighbour information for slot `i` without attempting cross-updates +using precomputed per-jet derived arrays. + +Arguments +- `nndist, nni, dijdist::AbstractVector`: arrays representing current nearest-neighbour + distances, indices, and dij distances respectively. +- `nx, ny, nz::AbstractVector`: direction cosine arrays. +- `E2p_scaled::AbstractVector`: per-jet E2p pre-multiplied by 2 * inv(R^2). +- `beam_term::AbstractVector`: precomputed beam-term per jet. +- `i::Integer`: index of the slot to update. +- `N::Integer`: current number of active slots. +- additional params: `dij_factor`, `β`, `γ`, `R` (kept for compatibility/signature parity). + +Behavior +- Scans other active slots to find the nearest neighbour for slot `i` using + `valencia_distance_inv_scaled_arrays` (avoids repeated multiplies), applies the beam-term + comparison using `beam_term[i]`, and writes back `nndist[i]`, `nni[i]`, and `dijdist[i]`. + +This is a Valencia-only optimized path intended for the hot update loops inside +the Valencia reconstruction entrypoint. +""" + @inline function update_nn_cross_arrays!(nndist::AbstractVector, nni::AbstractVector, nx::AbstractVector, ny::AbstractVector, nz::AbstractVector, E2p::AbstractVector, dijdist::AbstractVector, @@ -863,6 +974,30 @@ end dijdist[i] = dijdist_i end +""" + update_nn_cross_arrays_precomputed!(nndist, nni, nx, ny, nz, E2p_scaled, beam_term, dijdist, i, N, ...) + +Perform a cross-update of nearest-neighbour information for slot `i` using +precomputed per-jet derived arrays. This both finds the nearest neighbour for +`i` and updates other slots `j` if `i` becomes their nearest neighbour. + +Arguments +- `nndist, nni, dijdist::AbstractVector`: current nn-distances, nn-indices, dij-distances. +- `nx, ny, nz::AbstractVector`: direction-cosine arrays. +- `E2p_scaled::AbstractVector`: per-jet E2p scaled by 2 * inv(R^2). +- `beam_term::AbstractVector`: per-jet Valencia beam-term. +- `i::Integer`: index of the slot to cross-update. +- `N::Integer`: number of active slots. + +Behavior +- For each other slot `j`, computes the Valencia pair metric using the precomputed + scaled energies; if `i` provides a better neighbor for `j` the function updates + `nndist[j]`, `nni[j]`, and `dijdist[j]` (with beam-term check). Finally, it + computes and writes back the nearest-neighbour info for `i`. + +This function is Valencia-only and intended to replace the generic cross-update +in the hot merge loop to reduce repeated arithmetic and pow() calls. +""" ## Precomputed Valencia cross-update using E2p_scaled and beam_term @inline function update_nn_cross_arrays_precomputed!(nndist::AbstractVector, nni::AbstractVector, nx::AbstractVector, ny::AbstractVector, nz::AbstractVector, @@ -915,6 +1050,31 @@ end dijdist[i] = dijdist_i end +""" + update_nn_cross_arrays_precomputed!(nndist, nni, nx, ny, nz, E2p_scaled, beam_term, dijdist, i, N, ...) + +Perform a cross-update of nearest-neighbour information for slot `i` using +precomputed per-jet derived arrays. This both finds the nearest neighbour for +`i` and updates other slots `j` if `i` becomes their nearest neighbour. + +Arguments +- `nndist, nni, dijdist::AbstractVector`: current nn-distances, nn-indices, dij-distances. +- `nx, ny, nz::AbstractVector`: direction-cosine arrays. +- `E2p_scaled::AbstractVector`: per-jet E2p scaled by 2 * inv(R^2). +- `beam_term::AbstractVector`: per-jet Valencia beam-term. +- `i::Integer`: index of the slot to cross-update. +- `N::Integer`: number of active slots. + +Behavior +- For each other slot `j`, computes the Valencia pair metric using the precomputed + scaled energies; if `i` provides a better neighbor for `j` the function updates + `nndist[j]`, `nni[j]`, and `dijdist[j]` (with beam-term check). Finally, it + computes and writes back the nearest-neighbour info for `i`. + +This function is Valencia-only and intended to replace the generic cross-update +in the hot merge loop to reduce repeated arithmetic and pow() calls. +""" + Base.@propagate_inbounds @inline function fill_reco_array!(eereco, particles, R2, p) @inbounds for i in eachindex(particles) eereco.index[i] = i From be18a108a9b7ac5259733f841905d1a25f091b5d Mon Sep 17 00:00:00 2001 From: Matt LeBlanc Date: Sun, 17 Aug 2025 17:38:15 -0400 Subject: [PATCH 15/32] Cleanup -- factorize VLC code and improve test coverage --- src/EEAlgorithm.jl | 891 ++----------------------------------- src/EEAlgorithmValencia.jl | 327 ++++++++++++++ test/test-valencia.jl | 55 +++ 3 files changed, 430 insertions(+), 843 deletions(-) create mode 100644 src/EEAlgorithmValencia.jl diff --git a/src/EEAlgorithm.jl b/src/EEAlgorithm.jl index 73d0ba93..c4289fd9 100644 --- a/src/EEAlgorithm.jl +++ b/src/EEAlgorithm.jl @@ -27,125 +27,11 @@ Base.@propagate_inbounds @inline function angular_distance(eereco, i, j) end end -""" - valencia_distance(eereco, i, j, R) -> Float64 - -Calculate the Valencia distance between two jets `i` and `j` as -``min(E_i^{2β}, E_j^{2β}) * 2 * (1 - cos(θ_{ij})) / R²``. - -# Arguments -- `eereco`: The array of `EERecoJet` objects. -- `i`: The first jet. -- `j`: The second jet. -- `R`: The jet radius parameter. - -# Returns -- `Float64`: The Valencia distance between `i` and `j`. -""" -Base.@propagate_inbounds @inline function valencia_distance_inv(eereco, i, j, invR2) - if hasproperty(eereco, :nx) - nx = eereco.nx; ny = eereco.ny; nz = eereco.nz; E2p = eereco.E2p - angular_dist = angular_distance_arrays(nx, ny, nz, i, j) - # Valencia dij : min(E_i^{2β}, E_j^{2β}) * 2 * (1 - cos θ) * invR2 - min(E2p[i], E2p[j]) * 2 * angular_dist * invR2 - else - # Fallback for Array-of-structs - angular_dist = 1.0 - eereco[i].nx * eereco[j].nx - eereco[i].ny * eereco[j].ny - eereco[i].nz * eereco[j].nz - min(eereco[i].E2p, eereco[j].E2p) * 2 * angular_dist * invR2 - end -end - -Base.@propagate_inbounds @inline function valencia_distance(eereco, i, j, R) - return valencia_distance_inv(eereco, i, j, inv(R * R)) -end - -# Array-based helpers: operate directly on field vectors from StructArray to avoid -# repeated eereco[i] indexing which can be slower. +# Array-based angular distance helper (operates on direction-cosine arrays) Base.@propagate_inbounds @inline function angular_distance_arrays(nx, ny, nz, i, j) @muladd 1.0 - nx[i] * nx[j] - ny[i] * ny[j] - nz[i] * nz[j] end -Base.@propagate_inbounds @inline function valencia_distance_inv_arrays(E2p, nx, ny, nz, i, j, invR2) - angular_dist = angular_distance_arrays(nx, ny, nz, i, j) - min(E2p[i], E2p[j]) * 2 * angular_dist * invR2 -end - -""" - valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, j) -> Float64 - -Compute the Valencia pairwise metric using a pre-multiplied energy factor. - -Arguments -- `E2p_scaled::AbstractVector`: precomputed per-jet values equal to E2p * (2 * invR2). -- `nx, ny, nz::AbstractVector`: direction cosine arrays for jets. -- `i, j::Integer`: indices of the two jets to compare. - -Returns -- `Float64`: the Valencia dij metric computed as min(E2p_scaled[i], E2p_scaled[j]) * (1 - cos θ_{ij}). - -Notes -- This helper assumes `E2p_scaled` already includes the factor `2 * inv(R^2)`, so the - function avoids multiplying by those constants inside hot inner loops. -""" -# Scaled variant: accepts pre-multiplied E2p_scaled = E2p * (2 * invR2) -Base.@propagate_inbounds @inline function valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, j) - angular_dist = angular_distance_arrays(nx, ny, nz, i, j) - min(E2p_scaled[i], E2p_scaled[j]) * angular_dist -end - -""" - valencia_beam_distance(eereco, i, γ, β) -> Float64 - -Calculate the Valencia beam distance for jet `i` using the FastJet ValenciaPlugin -definition: ``d_iB = E_i^{2β} * (sin θ_i)^{2γ}``, where ``cos θ_i = nz`` -for unit direction cosines. Since ``sin^2 θ = 1 - nz^2``, we implement -``d_iB = E_i^{2β} * (1 - nz^2)^γ``. - -# Arguments -- `eereco`: The array of `EERecoJet` objects. -- `i`: The jet index. -- `γ`: The angular exponent parameter used in the Valencia beam distance. -- `β`: The energy exponent (same as `p` in our implementation). - -# Returns -- `Float64`: The Valencia beam distance for jet `i`. -""" -Base.@propagate_inbounds @inline function valencia_beam_distance(eereco, i, γ, β) - if hasproperty(eereco, :nz) - nzv = eereco.nz; E2pv = eereco.E2p - nz_i = nzv[i] - sin2 = 1 - nz_i * nz_i - E2p = E2pv[i] - else - nz_i = eereco[i].nz - sin2 = 1 - nz_i * nz_i - E2p = eereco[i].E2p - end - # Fast-paths for common γ values to avoid pow in hot loop - if γ == 1.0 - return E2p * sin2 - elseif γ == 2.0 - return E2p * (sin2 * sin2) - else - return E2p * sin2^γ - end -end - -# Array-based helper for Valencia beam distance to avoid StructArray getindex in hot loops -Base.@propagate_inbounds @inline function valencia_beam_distance_arrays(E2p, nz, i, γ, β) - nz_i = nz[i] - sin2 = 1 - nz_i * nz_i - E2p_i = E2p[i] - # Fast-paths for common γ values to avoid pow in hot loop - if γ == 1.0 - return E2p_i * sin2 - elseif γ == 2.0 - return E2p_i * (sin2 * sin2) - else - return E2p_i * sin2^γ - end -end - """ dij_dist(eereco, i, j, dij_factor, algorithm::JetAlgorithm.Algorithm, R = 4.0) @@ -176,80 +62,11 @@ end @inbounds min(eereco[i].E2p, eereco[j].E2p) * dij_factor * eereco[i].nndist end -""" - dij_dist(eereco, i, j, dij_factor, ::Val{JetAlgorithm.Valencia}, R) - -Valencia dij distance uses the full Valencia metric, including the 2*(1-cosθ)/R² factor. -""" -@inline function dij_dist(eereco, i, j, dij_factor, ::Val{JetAlgorithm.Valencia}, R) - j == 0 && return large_dij - @inbounds valencia_distance(eereco, i, j, R) -end - # Fallback if a non-Algorithm token is passed @inline function dij_dist(eereco, i, j, dij_factor, algorithm, R = 4.0) throw(ArgumentError("Algorithm $algorithm not supported for dij_dist")) end -function get_angular_nearest_neighbours!(eereco, algorithm, dij_factor, p, γ = 1.0, R = 4.0) - # Get the initial nearest neighbours for each jet - N = length(eereco) - # For Valencia, nearest-neighbour must be chosen on the full dij metric (FastJet NNH behaviour) - @inbounds for i in 1:N - local_nndist_i = Inf - local_nni_i = i - eereco.nndist[i] = local_nndist_i - eereco.nni[i] = local_nni_i - end - # Nearest neighbour search - @inbounds for i in 1:N - @inbounds for j in (i + 1):N - # Metric used to pick the nearest neighbour - if algorithm == JetAlgorithm.Valencia - # Use array helpers to avoid repeated StructArray getindex - this_metric = valencia_distance_inv_arrays(eereco.E2p, eereco.nx, eereco.ny, eereco.nz, i, j, inv(R * R)) - else - this_metric = angular_distance_arrays(eereco.nx, eereco.ny, eereco.nz, i, j) - end - - # Using these ternary operators is faster than the if-else block - better_nndist_i = this_metric < eereco[i].nndist - eereco.nndist[i] = better_nndist_i ? this_metric : eereco.nndist[i] - eereco.nni[i] = better_nndist_i ? j : eereco.nni[i] - better_nndist_j = this_metric < eereco[j].nndist - eereco.nndist[j] = better_nndist_j ? this_metric : eereco.nndist[j] - eereco.nni[j] = better_nndist_j ? i : eereco.nni[j] - end - end - # Nearest neighbour dij distance - @inbounds for i in 1:N - if algorithm == JetAlgorithm.Valencia - eereco.dijdist[i] = valencia_distance(eereco, i, eereco[i].nni, R) - else - eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, algorithm, R) - end - end - # For the EEKt algorithm, we need to check the beam distance as well - # (This is structured to only check for EEKt once) - if algorithm == JetAlgorithm.EEKt - @inbounds for i in 1:N - beam_closer = eereco[i].E2p < eereco[i].dijdist - eereco.dijdist[i] = beam_closer ? eereco[i].E2p : eereco.dijdist[i] - eereco.nni[i] = beam_closer ? 0 : eereco.nni[i] - end - elseif algorithm == JetAlgorithm.Valencia - # Use array-based helper to avoid StructArray property checks and - # reduce per-iteration overhead in the hot loop. - E2p = eereco.E2p; nz = eereco.nz - @inbounds for i in 1:N - valencia_beam_dist = valencia_beam_distance_arrays(E2p, nz, i, γ, p) - beam_closer = valencia_beam_dist < eereco.dijdist[i] - eereco.dijdist[i] = beam_closer ? valencia_beam_dist : eereco.dijdist[i] - eereco.nni[i] = beam_closer ? 0 : eereco.nni[i] - end - end -end - # Val-specialized nearest neighbour search (removes runtime branches in hot loops) @inline function get_angular_nearest_neighbours!(eereco, ::Val{JetAlgorithm.Durham}, dij_factor, p, γ = 1.0, R = 4.0) @@ -304,130 +121,6 @@ end end end -@inline function get_angular_nearest_neighbours!(eereco, ::Val{JetAlgorithm.Valencia}, - dij_factor, p, γ = 1.0, R = 4.0) - # Fallback Val-specialised implementation kept for non-precomputed use. - # The Valencia entrypoint uses a precomputed path (see _ee_genkt_algorithm_valencia) - N = length(eereco) - E2p = eereco.E2p; nx = eereco.nx; ny = eereco.ny; nz = eereco.nz - nndist = eereco.nndist; nni = eereco.nni - invR2 = inv(R * R) - @inbounds for i in 1:N - nndist[i] = Inf - nni[i] = i - end - @inbounds for i in 1:N - local_nndist_i = nndist[i] - local_nni_i = nni[i] - @inbounds for j in (i + 1):N - this_metric = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, j, invR2) - if this_metric < local_nndist_i - local_nndist_i = this_metric - local_nni_i = j - end - if this_metric < nndist[j] - nndist[j] = this_metric - nni[j] = i - end - end - nndist[i] = local_nndist_i - nni[i] = local_nni_i - end - @inbounds for i in 1:N - eereco.dijdist[i] = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, nni[i], invR2) - end - @inbounds for i in 1:N - valencia_beam_dist = valencia_beam_distance(eereco, i, γ, p) - beam_closer = valencia_beam_dist < eereco[i].dijdist - eereco.dijdist[i] = beam_closer ? valencia_beam_dist : eereco.dijdist[i] - eereco.nni[i] = beam_closer ? 0 : eereco.nni[i] - end -end - -""" - get_angular_nearest_neighbours_valencia_precomputed!(eereco, E2p_scaled, beam_term, p, γ, R) - -Initialize nearest-neighbour data for a Valencia clustering using precomputed -per-jet derived arrays. - -Arguments -- `eereco::StructArray{EERecoJet}`: the SoA of reconstruction slots to initialize. -- `E2p_scaled::AbstractVector`: per-jet pre-multiplied energy factor (E2p * 2 * inv(R^2)). -- `beam_term::AbstractVector`: per-jet Valencia beam-term precomputed as E2p * (1 - nz^2)^γ. -- `p`: energy exponent (β for Valencia). -- `γ::Real`: angular exponent used in the Valencia beam-term. -- `R::Real`: jet radius parameter (used indirectly via E2p_scaled construction). - -Behavior -- Sets `eereco.nndist`, `eereco.nni`, and `eereco.dijdist` using the provided - precomputed arrays. The function avoids repeated pow() and constant multiplications - inside the hot O(N²) initialization loop. - -This function is Valencia-only and intended to be called from the Valencia entrypoint -after `E2p_scaled` and `beam_term` have been populated. -""" -## Precomputed Valencia nearest-neighbour initializer using precomputed arrays -Base.@propagate_inbounds @inline function get_angular_nearest_neighbours_valencia_precomputed!(eereco, - E2p_scaled::AbstractVector, - beam_term::AbstractVector, - p, γ = 1.0, R = 4.0) - N = length(eereco) - nx = eereco.nx; ny = eereco.ny; nz = eereco.nz - nndist = eereco.nndist; nni = eereco.nni - @inbounds for i in 1:N - nndist[i] = Inf - nni[i] = i - end - @inbounds for i in 1:N - local_nndist_i = nndist[i] - local_nni_i = nni[i] - @inbounds for j in (i + 1):N - this_metric = valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, j) - if this_metric < local_nndist_i - local_nndist_i = this_metric - local_nni_i = j - end - if this_metric < nndist[j] - nndist[j] = this_metric - nni[j] = i - end - end - nndist[i] = local_nndist_i - nni[i] = local_nni_i - end - @inbounds for i in 1:N - eereco.dijdist[i] = valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, nni[i]) - end - @inbounds for i in 1:N - beam_closer = beam_term[i] < eereco.dijdist[i] - eereco.dijdist[i] = beam_closer ? beam_term[i] : eereco.dijdist[i] - eereco.nni[i] = beam_closer ? 0 : eereco.nni[i] - end -end - -""" - get_angular_nearest_neighbours_valencia_precomputed!(eereco, E2p_scaled, beam_term, p, γ, R) - -Initialize nearest-neighbour data for a Valencia clustering using precomputed -per-jet derived arrays. - -Arguments -- `eereco::StructArray{EERecoJet}`: the SoA of reconstruction slots to initialize. -- `E2p_scaled::AbstractVector`: per-jet pre-multiplied energy factor (E2p * 2 * inv(R^2)). -- `beam_term::AbstractVector`: per-jet Valencia beam-term precomputed as E2p * (1 - nz^2)^γ. -- `p`: energy exponent (β for Valencia). -- `γ::Real`: angular exponent used in the Valencia beam-term. -- `R::Real`: jet radius parameter (used indirectly via E2p_scaled construction). - -Behavior -- Sets `eereco.nndist`, `eereco.nni`, and `eereco.dijdist` using the provided - precomputed arrays. The function avoids repeated pow() and constant multiplications - inside the hot O(N²) initialization loop. - -This function is Valencia-only and intended to be called from the Valencia entrypoint -after `E2p_scaled` and `beam_term` have been populated. -""" - # Forwarder to Val-specialized version @inline function get_angular_nearest_neighbours!(eereco, algorithm::JetAlgorithm.Algorithm, @@ -444,7 +137,7 @@ end # Val-specialized no-cross update @inline function update_nn_no_cross!(eereco, i, N, ::Val{JetAlgorithm.Durham}, dij_factor, - β = 1.0, γ = 1.0, R = 4.0) + _β = 1.0, _γ = 1.0, R = 4.0) nndist = eereco.nndist; nni = eereco.nni nx = eereco.nx; ny = eereco.ny; nz = eereco.nz nndist[i] = large_distance @@ -470,7 +163,7 @@ end end @inline function update_nn_no_cross!(eereco, i, N, ::Val{JetAlgorithm.EEKt}, dij_factor, - β = 1.0, γ = 1.0, R = 4.0) + _β = 1.0, _γ = 1.0, R = 4.0) nndist = eereco.nndist; nni = eereco.nni nx = eereco.nx; ny = eereco.ny; nz = eereco.nz E2p = eereco.E2p @@ -498,36 +191,6 @@ end eereco.nni[i] = beam_close ? 0 : eereco.nni[i] end -@inline function update_nn_no_cross!(eereco, i, N, ::Val{JetAlgorithm.Valencia}, - dij_factor, β = 1.0, γ = 1.0, R = 4.0) - nndist = eereco.nndist; nni = eereco.nni - nx = eereco.nx; ny = eereco.ny; nz = eereco.nz - E2p = eereco.E2p - E2p_i = E2p[i] - nndist[i] = Inf - nni[i] = i - invR2 = inv(R * R) - @inbounds for j in 1:(i-1) - this_metric = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, j, invR2) - better_nndist_i = this_metric < nndist[i] - nndist[i] = better_nndist_i ? this_metric : nndist[i] - nni[i] = better_nndist_i ? j : nni[i] - end - @inbounds for j in (i+1):N - this_metric = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, j, invR2) - better_nndist_i = this_metric < nndist[i] - nndist[i] = better_nndist_i ? this_metric : nndist[i] - nni[i] = better_nndist_i ? j : nni[i] - end - eereco.dijdist[i] = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, nni[i], invR2) - valencia_beam_dist = valencia_beam_distance(eereco, i, γ, β) - beam_close = valencia_beam_dist < eereco.dijdist[i] - eereco.dijdist[i] = beam_close ? valencia_beam_dist : eereco.dijdist[i] - eereco.nni[i] = beam_close ? 0 : eereco.nni[i] -end - -# (Forwarding handled earlier; avoid duplicate definition) - @inline function update_nn_cross!(eereco, i, N, algorithm::JetAlgorithm.Algorithm, dij_factor, β = 1.0, γ = 1.0, R = 4.0) # Forward to Val-specialized implementations to avoid runtime branches @@ -536,7 +199,7 @@ end # Val-specialized cross update @inline function update_nn_cross!(eereco, i, N, ::Val{JetAlgorithm.Durham}, dij_factor, - β = 1.0, γ = 1.0, R = 4.0) + _β = 1.0, _γ = 1.0, R = 4.0) nndist = eereco.nndist; nni = eereco.nni nx = eereco.nx; ny = eereco.ny; nz = eereco.nz nndist[i] = large_distance @@ -566,7 +229,7 @@ end end @inline function update_nn_cross!(eereco, i, N, ::Val{JetAlgorithm.EEKt}, dij_factor, - β = 1.0, γ = 1.0, R = 4.0) + _β = 1.0, _γ = 1.0, R = 4.0) nndist = eereco.nndist; nni = eereco.nni nx = eereco.nx; ny = eereco.ny; nz = eereco.nz E2p = eereco.E2p @@ -603,40 +266,6 @@ end nni[i] = beam_close ? 0 : nni[i] end -@inline function update_nn_cross!(eereco, i, N, ::Val{JetAlgorithm.Valencia}, dij_factor, - β = 1.0, γ = 1.0, R = 4.0) - nndist = eereco.nndist; nni = eereco.nni - nx = eereco.nx; ny = eereco.ny; nz = eereco.nz - E2p = eereco.E2p - nndist[i] = Inf - nni[i] = i - invR2 = inv(R * R) - @inbounds for j in 1:N - if j != i - this_metric = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, j, invR2) - better_nndist_i = this_metric < nndist[i] - nndist[i] = better_nndist_i ? this_metric : nndist[i] - nni[i] = better_nndist_i ? j : nni[i] - if this_metric < nndist[j] - nndist[j] = this_metric - nni[j] = i - # Use the already-computed metric for dij (Valencia uses full dij here) - eereco.dijdist[j] = this_metric - valencia_beam_dist = valencia_beam_distance_arrays(E2p, nz, j, γ, β) - if valencia_beam_dist < eereco.dijdist[j] - eereco.dijdist[j] = valencia_beam_dist - nni[j] = 0 - end - end - end - end - eereco.dijdist[i] = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, nni[i], invR2) - valencia_beam_dist = valencia_beam_distance_arrays(E2p, nz, i, γ, β) - beam_close = valencia_beam_dist < eereco.dijdist[i] - eereco.dijdist[i] = beam_close ? valencia_beam_dist : eereco.dijdist[i] - nni[i] = beam_close ? 0 : nni[i] -end - function ee_check_consistency(clusterseq, eereco, N) # Check the consistency of the reconstruction state for i in 1:N @@ -670,7 +299,7 @@ end nx::AbstractVector, ny::AbstractVector, nz::AbstractVector, E2p::AbstractVector, dijdist::AbstractVector, i::Integer, N::Integer, ::Val{JetAlgorithm.Durham}, - dij_factor, β = 1.0, γ = 1.0, R = 4.0) + dij_factor, _β = 1.0, _γ = 1.0, R = 4.0) nndist[i] = large_distance nni[i] = i @inbounds for j in 1:N @@ -692,7 +321,7 @@ end nx::AbstractVector, ny::AbstractVector, nz::AbstractVector, E2p::AbstractVector, dijdist::AbstractVector, i::Integer, N::Integer, ::Val{JetAlgorithm.EEKt}, - dij_factor, β = 1.0, γ = 1.0, R = 4.0) + dij_factor, _β = 1.0, _γ = 1.0, R = 4.0) nndist[i] = large_distance nni[i] = i E2p_i = E2p[i] @@ -712,113 +341,6 @@ end nni[i] = beam_close ? 0 : nni[i] end -@inline function update_nn_no_cross_arrays!(nndist::AbstractVector, nni::AbstractVector, - nx::AbstractVector, ny::AbstractVector, nz::AbstractVector, - E2p::AbstractVector, dijdist::AbstractVector, - i::Integer, N::Integer, ::Val{JetAlgorithm.Valencia}, - dij_factor, β = 1.0, γ = 1.0, R = 4.0) - # Precomputed variant lives in a separate function; keep this fallback here. - invR2 = inv(R * R) - nndist_i = Inf - nni_i = i - @inbounds for j in 1:N - if j != i - this_metric = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, j, invR2) - if this_metric < nndist_i - nndist_i = this_metric - nni_i = j - end - end - end - # compute dijdist and beam check using locals then write back once - dijdist_i = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, nni_i, invR2) - valencia_beam_dist = valencia_beam_distance_arrays(E2p, nz, i, γ, β) - if valencia_beam_dist < dijdist_i - dijdist_i = valencia_beam_dist - nni_i = 0 - end - nndist[i] = nndist_i - nni[i] = nni_i - dijdist[i] = dijdist_i -end - -""" - update_nn_no_cross_arrays_precomputed!(nndist, nni, nx, ny, nz, E2p_scaled, beam_term, dijdist, i, N, ...) - -Update nearest-neighbour information for slot `i` without attempting cross-updates -using precomputed per-jet derived arrays. - -Arguments -- `nndist, nni, dijdist::AbstractVector`: arrays representing current nearest-neighbour - distances, indices, and dij distances respectively. -- `nx, ny, nz::AbstractVector`: direction cosine arrays. -- `E2p_scaled::AbstractVector`: per-jet E2p pre-multiplied by 2 * inv(R^2). -- `beam_term::AbstractVector`: precomputed beam-term per jet. -- `i::Integer`: index of the slot to update. -- `N::Integer`: current number of active slots. -- additional params: `dij_factor`, `β`, `γ`, `R` (kept for compatibility/signature parity). - -Behavior -- Scans other active slots to find the nearest neighbour for slot `i` using - `valencia_distance_inv_scaled_arrays` (avoids repeated multiplies), applies the beam-term - comparison using `beam_term[i]`, and writes back `nndist[i]`, `nni[i]`, and `dijdist[i]`. - -This is a Valencia-only optimized path intended for the hot update loops inside -the Valencia reconstruction entrypoint. -""" -## Precomputed Valencia no-cross update using E2p_scaled and beam_term -@inline function update_nn_no_cross_arrays_precomputed!(nndist::AbstractVector, nni::AbstractVector, - nx::AbstractVector, ny::AbstractVector, nz::AbstractVector, - E2p_scaled::AbstractVector, beam_term::AbstractVector, - dijdist::AbstractVector, - i::Integer, N::Integer, - dij_factor, β = 1.0, γ = 1.0, R = 4.0) - nndist_i = Inf - nni_i = i - @inbounds for j in 1:N - if j != i - this_metric = valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, j) - if this_metric < nndist_i - nndist_i = this_metric - nni_i = j - end - end - end - dijdist_i = valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, nni_i) - if beam_term[i] < dijdist_i - dijdist_i = beam_term[i] - nni_i = 0 - end - nndist[i] = nndist_i - nni[i] = nni_i - dijdist[i] = dijdist_i -end - -""" - update_nn_no_cross_arrays_precomputed!(nndist, nni, nx, ny, nz, E2p_scaled, beam_term, dijdist, i, N, ...) - -Update nearest-neighbour information for slot `i` without attempting cross-updates -using precomputed per-jet derived arrays. - -Arguments -- `nndist, nni, dijdist::AbstractVector`: arrays representing current nearest-neighbour - distances, indices, and dij distances respectively. -- `nx, ny, nz::AbstractVector`: direction cosine arrays. -- `E2p_scaled::AbstractVector`: per-jet E2p pre-multiplied by 2 * inv(R^2). -- `beam_term::AbstractVector`: precomputed beam-term per jet. -- `i::Integer`: index of the slot to update. -- `N::Integer`: current number of active slots. -- additional params: `dij_factor`, `β`, `γ`, `R` (kept for compatibility/signature parity). - -Behavior -- Scans other active slots to find the nearest neighbour for slot `i` using - `valencia_distance_inv_scaled_arrays` (avoids repeated multiplies), applies the beam-term - comparison using `beam_term[i]`, and writes back `nndist[i]`, `nni[i]`, and `dijdist[i]`. - -This is a Valencia-only optimized path intended for the hot update loops inside -the Valencia reconstruction entrypoint. -""" - @inline function update_nn_cross_arrays!(nndist::AbstractVector, nni::AbstractVector, nx::AbstractVector, ny::AbstractVector, nz::AbstractVector, E2p::AbstractVector, dijdist::AbstractVector, @@ -831,7 +353,7 @@ end nx::AbstractVector, ny::AbstractVector, nz::AbstractVector, E2p::AbstractVector, dijdist::AbstractVector, i::Integer, N::Integer, ::Val{JetAlgorithm.Durham}, - dij_factor, β = 1.0, γ = 1.0, R = 4.0) + dij_factor, _β = 1.0, _γ = 1.0, R = 4.0) nndist[i] = large_distance nni[i] = i E2p_i = E2p[i] @@ -869,7 +391,7 @@ end nx::AbstractVector, ny::AbstractVector, nz::AbstractVector, E2p::AbstractVector, dijdist::AbstractVector, i::Integer, N::Integer, ::Val{JetAlgorithm.EEKt}, - dij_factor, β = 1.0, γ = 1.0, R = 4.0) + dij_factor, _β = 1.0, _γ = 1.0, R = 4.0) E2p_i = E2p[i] nndist[i] = large_distance nni[i] = i @@ -916,165 +438,6 @@ end nni[i] = beam_close ? 0 : nni[i] end -@inline function update_nn_cross_arrays!(nndist::AbstractVector, nni::AbstractVector, - nx::AbstractVector, ny::AbstractVector, nz::AbstractVector, - E2p::AbstractVector, dijdist::AbstractVector, - i::Integer, N::Integer, ::Val{JetAlgorithm.Valencia}, - dij_factor, β = 1.0, γ = 1.0, R = 4.0) - # Operate on locals for slot i to reduce setindex traffic; updates to other - # slots (j) still write directly since they modify different indices. - # Precomputed variant fallback uses the non-precomputed helpers; there is - # a separate precomputed cross-update below. - invR2 = inv(R * R) - nndist_i = Inf - nni_i = i - @inbounds for j in 1:(i-1) - this_metric = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, j, invR2) - if this_metric < nndist_i - nndist_i = this_metric - nni_i = j - end - if this_metric < nndist[j] - nndist[j] = this_metric - nni[j] = i - dijdist[j] = this_metric - valencia_beam_dist = valencia_beam_distance_arrays(E2p, nz, j, γ, β) - if valencia_beam_dist < dijdist[j] - dijdist[j] = valencia_beam_dist - nni[j] = 0 - end - end - end - @inbounds for j in (i+1):N - this_metric = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, j, invR2) - if this_metric < nndist_i - nndist_i = this_metric - nni_i = j - end - if this_metric < nndist[j] - nndist[j] = this_metric - nni[j] = i - dijdist[j] = this_metric - valencia_beam_dist = valencia_beam_distance_arrays(E2p, nz, j, γ, β) - if valencia_beam_dist < dijdist[j] - dijdist[j] = valencia_beam_dist - nni[j] = 0 - end - end - end - # Finalize slot i - dijdist_i = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, nni_i, invR2) - valencia_beam_dist = valencia_beam_distance_arrays(E2p, nz, i, γ, β) - if valencia_beam_dist < dijdist_i - dijdist_i = valencia_beam_dist - nni_i = 0 - end - nndist[i] = nndist_i - nni[i] = nni_i - dijdist[i] = dijdist_i -end - -""" - update_nn_cross_arrays_precomputed!(nndist, nni, nx, ny, nz, E2p_scaled, beam_term, dijdist, i, N, ...) - -Perform a cross-update of nearest-neighbour information for slot `i` using -precomputed per-jet derived arrays. This both finds the nearest neighbour for -`i` and updates other slots `j` if `i` becomes their nearest neighbour. - -Arguments -- `nndist, nni, dijdist::AbstractVector`: current nn-distances, nn-indices, dij-distances. -- `nx, ny, nz::AbstractVector`: direction-cosine arrays. -- `E2p_scaled::AbstractVector`: per-jet E2p scaled by 2 * inv(R^2). -- `beam_term::AbstractVector`: per-jet Valencia beam-term. -- `i::Integer`: index of the slot to cross-update. -- `N::Integer`: number of active slots. - -Behavior -- For each other slot `j`, computes the Valencia pair metric using the precomputed - scaled energies; if `i` provides a better neighbor for `j` the function updates - `nndist[j]`, `nni[j]`, and `dijdist[j]` (with beam-term check). Finally, it - computes and writes back the nearest-neighbour info for `i`. - -This function is Valencia-only and intended to replace the generic cross-update -in the hot merge loop to reduce repeated arithmetic and pow() calls. -""" -## Precomputed Valencia cross-update using E2p_scaled and beam_term -@inline function update_nn_cross_arrays_precomputed!(nndist::AbstractVector, nni::AbstractVector, - nx::AbstractVector, ny::AbstractVector, nz::AbstractVector, - E2p_scaled::AbstractVector, beam_term::AbstractVector, - dijdist::AbstractVector, - i::Integer, N::Integer, - dij_factor, β = 1.0, γ = 1.0, R = 4.0) - nndist_i = Inf - nni_i = i - @inbounds for j in 1:(i-1) - this_metric = valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, j) - if this_metric < nndist_i - nndist_i = this_metric - nni_i = j - end - if this_metric < nndist[j] - nndist[j] = this_metric - nni[j] = i - dijdist[j] = this_metric - if beam_term[j] < dijdist[j] - dijdist[j] = beam_term[j] - nni[j] = 0 - end - end - end - @inbounds for j in (i+1):N - this_metric = valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, j) - if this_metric < nndist_i - nndist_i = this_metric - nni_i = j - end - if this_metric < nndist[j] - nndist[j] = this_metric - nni[j] = i - dijdist[j] = this_metric - if beam_term[j] < dijdist[j] - dijdist[j] = beam_term[j] - nni[j] = 0 - end - end - end - # Finalize slot i - dijdist_i = valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, nni_i) - if beam_term[i] < dijdist_i - dijdist_i = beam_term[i] - nni_i = 0 - end - nndist[i] = nndist_i - nni[i] = nni_i - dijdist[i] = dijdist_i -end - -""" - update_nn_cross_arrays_precomputed!(nndist, nni, nx, ny, nz, E2p_scaled, beam_term, dijdist, i, N, ...) - -Perform a cross-update of nearest-neighbour information for slot `i` using -precomputed per-jet derived arrays. This both finds the nearest neighbour for -`i` and updates other slots `j` if `i` becomes their nearest neighbour. - -Arguments -- `nndist, nni, dijdist::AbstractVector`: current nn-distances, nn-indices, dij-distances. -- `nx, ny, nz::AbstractVector`: direction-cosine arrays. -- `E2p_scaled::AbstractVector`: per-jet E2p scaled by 2 * inv(R^2). -- `beam_term::AbstractVector`: per-jet Valencia beam-term. -- `i::Integer`: index of the slot to cross-update. -- `N::Integer`: number of active slots. - -Behavior -- For each other slot `j`, computes the Valencia pair metric using the precomputed - scaled energies; if `i` provides a better neighbor for `j` the function updates - `nndist[j]`, `nni[j]`, and `dijdist[j]` (with beam-term check). Finally, it - computes and writes back the nearest-neighbour info for `i`. - -This function is Valencia-only and intended to replace the generic cross-update -in the hot merge loop to reduce repeated arithmetic and pow() calls. -""" - Base.@propagate_inbounds @inline function fill_reco_array!(eereco, particles, R2, p) @inbounds for i in eachindex(particles) eereco.index[i] = i @@ -1167,17 +530,44 @@ function ee_genkt_algorithm(particles::AbstractVector{T}; algorithm::JetAlgorith p::Union{Real, Nothing} = nothing, R = 4.0, recombine = addjets, preprocess = nothing, γ::Real = 1.0, β::Union{Real, Nothing} = nothing) where {T} - - # For Valencia, if β is provided, overwrite p - if algorithm == JetAlgorithm.Valencia && β !== nothing - p = β - end - - # Check for consistency algorithm power + # (β override for Valencia handled inside _ee_genkt_algorithm_valencia) + if algorithm == JetAlgorithm.Valencia + # Apply β override if provided, then obtain validated power + local_p = β === nothing ? p : β + local_p = get_algorithm_power(p = local_p, algorithm = algorithm) + return _ee_genkt_algorithm_valencia(particles = begin + if isnothing(preprocess) + if T == EEJet + recombination_particles = copy(particles) + sizehint!(recombination_particles, length(particles) * 2) + recombination_particles + else + recombination_particles = EEJet[] + sizehint!(recombination_particles, length(particles) * 2) + for (i, particle) in enumerate(particles) + push!(recombination_particles, EEJet(particle; cluster_hist_index = i)) + end + recombination_particles + end + else + recombination_particles = EEJet[] + sizehint!(recombination_particles, length(particles) * 2) + for (i, particle) in enumerate(particles) + push!(recombination_particles, + preprocess(particle, EEJet; cluster_hist_index = i)) + end + recombination_particles + end + end, + algorithm = algorithm, p = local_p, R = R, + recombine = recombine, γ = γ, beta = β) + end + + # Check for consistency algorithm power (non-Valencia path) p = get_algorithm_power(p = p, algorithm = algorithm) - # Integer p if possible, i.e. if not running Valencia - if algorithm != JetAlgorithm.Valencia + # Cast p to Int where possible for algorithms that benefit (Durham/EEKt) + if algorithm == JetAlgorithm.Durham || algorithm == JetAlgorithm.EEKt p = (round(p) == p) ? Int(p) : p end @@ -1242,9 +632,6 @@ function _ee_genkt_algorithm_durham(; particles::AbstractVector{EEJet}, N::Int = length(particles) R2 = R^2 - if algorithm == JetAlgorithm.Valencia && beta !== nothing - p = beta - end # Durham dij factor dij_factor = 2.0 @@ -1329,186 +716,7 @@ function _ee_genkt_algorithm_durham(; particles::AbstractVector{EEJet}, end ################################################################################ -# Valencia-specialised implementation ################################################################################ -function _ee_genkt_algorithm_valencia(; particles::AbstractVector{EEJet}, - algorithm::JetAlgorithm.Algorithm, p::Real, R = 4.0, - recombine = addjets, γ::Real = 1.0, - beta::Union{Real, Nothing} = nothing) - # Bounds - N::Int = length(particles) - - R2 = R^2 - # Valencia uses p as β when passed through - if algorithm == JetAlgorithm.Valencia && beta !== nothing - p = beta - end - - # Constant factor for the Valencia dij metric - dij_factor = 1.0 - - # For optimised reconstruction generate an SoA containing the necessary - # jet information and populate it accordingly - eereco = StructArray{EERecoJet}(undef, N) - - fill_reco_array!(eereco, particles, R2, p) - - # Setup the initial history and get the total energy - history, Qtot = initial_history(particles) - - clusterseq = ClusterSequence(algorithm, p, R, RecoStrategy.N2Plain, particles, history, - Qtot) - - # Run over initial pairs of jets to find nearest neighbours (precomputed path) - # prepare precomputed arrays below before calling the initialized helper - - - # Alias StructArray fields into local vectors to avoid allocations from - # copying while still avoiding repeated StructArray field lookups in - # hot loops. These locals point directly at the underlying vectors, so - # no explicit writeback is required at the end. - indexv = eereco.index - nni_v = eereco.nni - nndist_v = eereco.nndist - dijdist_v = eereco.dijdist - nxv = eereco.nx - nyv = eereco.ny - nzv = eereco.nz - E2pv = eereco.E2p - - # Precompute scaled E2p and beam_term for Valencia to avoid repeated work - invR2 = inv(R * R) - factor = 2 * invR2 - E2p_scaled = similar(E2pv) - beam_term = similar(E2pv) - @inbounds for k in 1:N - E2p_scaled[k] = E2pv[k] * factor - nz_k = nzv[k] - sin2 = 1.0 - nz_k * nz_k - if γ == 1.0 - beam_term[k] = E2pv[k] * sin2 - elseif γ == 2.0 - beam_term[k] = E2pv[k] * (sin2 * sin2) - else - beam_term[k] = E2pv[k] * sin2^γ - end - end - - # Now run NN init using precomputed helpers - get_angular_nearest_neighbours_valencia_precomputed!(eereco, E2p_scaled, beam_term, p, γ, R) - - # Now we can start the main loop - iter = 0 - while N != 0 - iter += 1 - - dij_min, ijetA = fast_findmin(dijdist_v, N) - ijetB = nni_v[ijetA] - - # Now we check if there is a "beam" merge possibility - if ijetB == 0 - # Shouldn't happen for Valencia (beam handled via valencia_beam checks) - ijetB = ijetA - add_step_to_history!(clusterseq, - clusterseq.jets[indexv[ijetA]]._cluster_hist_index, - BeamJet, Invalid, dij_min) - elseif N == 1 - ijetB = ijetA - add_step_to_history!(clusterseq, - clusterseq.jets[indexv[ijetA]]._cluster_hist_index, - BeamJet, Invalid, dij_min) - else - if ijetB < ijetA - ijetA, ijetB = ijetB, ijetA - end - - jetA = clusterseq.jets[indexv[ijetA]] - jetB = clusterseq.jets[indexv[ijetB]] - - merged_jet = recombine(jetA, jetB; - cluster_hist_index = length(clusterseq.history) + 1) - - push!(clusterseq.jets, merged_jet) - newjet_k = length(clusterseq.jets) - add_step_to_history!(clusterseq, - minmax(cluster_hist_index(jetA), - cluster_hist_index(jetB))..., - newjet_k, dij_min) - - # Insert merged jet into our local SoA (avoid writing StructArray) - indexv[ijetA] = newjet_k - nni_v[ijetA] = 0 - nndist_v[ijetA] = R2 - nxv[ijetA] = nx(merged_jet) - nyv[ijetA] = ny(merged_jet) - nzv[ijetA] = nz(merged_jet) - # Compute E2p like insert_new_jet! would - E = energy(merged_jet) - if p isa Int - if p == 1 - E2pv[ijetA] = E * E - else - E2 = E * E - E2pv[ijetA] = E2^p - end - else - E2pv[ijetA] = E^(2p) - end - # Recompute precomputed derived arrays for the merged slot - E2p_scaled[ijetA] = E2pv[ijetA] * factor - nz_k = nzv[ijetA] - sin2_k = 1.0 - nz_k * nz_k - if γ == 1.0 - beam_term[ijetA] = E2pv[ijetA] * sin2_k - elseif γ == 2.0 - beam_term[ijetA] = E2pv[ijetA] * (sin2_k * sin2_k) - else - beam_term[ijetA] = E2pv[ijetA] * sin2_k^γ - end - end - - if ijetB != N - # Local copy from slot N -> slot ijetB (avoid StructArray ops) - indexv[ijetB] = indexv[N] - nni_v[ijetB] = nni_v[N] - nndist_v[ijetB] = nndist_v[N] - dijdist_v[ijetB] = dijdist_v[N] - nxv[ijetB] = nxv[N] - nyv[ijetB] = nyv[N] - nzv[ijetB] = nzv[N] - E2pv[ijetB] = E2pv[N] - # Also copy precomputed derived arrays - E2p_scaled[ijetB] = E2p_scaled[N] - beam_term[ijetB] = beam_term[N] - end - - N -= 1 - - # Update nearest neighbours step - @inbounds for i in 1:N - if (ijetB != N + 1) && (nni_v[i] == N + 1) - nni_v[i] = ijetB - else - if (nni_v[i] == ijetA) || (nni_v[i] == ijetB) || (nni_v[i] > N) - update_nn_no_cross_arrays_precomputed!(nndist_v, nni_v, nxv, nyv, nzv, - E2p_scaled, beam_term, dijdist_v, - i, N, dij_factor, p, γ, R) - end - end - end - - if ijetA != ijetB - update_nn_cross_arrays_precomputed!(nndist_v, nni_v, nxv, nyv, nzv, - E2p_scaled, beam_term, dijdist_v, - ijetA, N, dij_factor, p, γ, R) - end - end - - # Locals alias the StructArray fields directly, so there is no separate - # writeback step required here. - - clusterseq -end """ _ee_genkt_algorithm!(particles::AbstractVector{EEJet}; @@ -1544,17 +752,14 @@ function _ee_genkt_algorithm(; particles::AbstractVector{EEJet}, # Bounds N::Int = length(particles) - # If Valencia requested, forward to the Valencia-specialised path + # Forward Valencia directly to specialised implementation (tests call this internal API) if algorithm == JetAlgorithm.Valencia return _ee_genkt_algorithm_valencia(particles = particles, algorithm = algorithm, - p = p, R = R, recombine = recombine, - γ = γ, beta = beta) + p = (beta === nothing ? p : beta), R = R, + recombine = recombine, γ = γ, beta = beta) end R2 = R^2 - if algorithm == JetAlgorithm.Valencia && beta !== nothing - p = beta - end # Constant factor for the dij metric and the beam distance function if algorithm == JetAlgorithm.Durham diff --git a/src/EEAlgorithmValencia.jl b/src/EEAlgorithmValencia.jl new file mode 100644 index 00000000..0e6c2982 --- /dev/null +++ b/src/EEAlgorithmValencia.jl @@ -0,0 +1,327 @@ +################################################################################ +# Valencia-specialised helpers and implementation +################################################################################ + +# Valencia distance helpers +Base.@propagate_inbounds @inline function valencia_distance_inv(eereco, i, j, invR2) + if hasproperty(eereco, :nx) + nx = eereco.nx; ny = eereco.ny; nz = eereco.nz; E2p = eereco.E2p + angular_dist = angular_distance_arrays(nx, ny, nz, i, j) + # Valencia dij : min(E_i^{2\u03b2}, E_j^{2\u03b2}) * 2 * (1 - cos \u03b8) * invR2 + min(E2p[i], E2p[j]) * 2 * angular_dist * invR2 + else + # Fallback for Array-of-structs + angular_dist = 1.0 - eereco[i].nx * eereco[j].nx - eereco[i].ny * eereco[j].ny - eereco[i].nz * eereco[j].nz + min(eereco[i].E2p, eereco[j].E2p) * 2 * angular_dist * invR2 + end +end + +""" + dij_dist(eereco, i, j, dij_factor, ::Val{JetAlgorithm.Valencia}, R) + +Valencia dij distance uses the full Valencia metric, including the 2*(1-cosθ)/R² factor. +""" +@inline function dij_dist(eereco, i, j, dij_factor, ::Val{JetAlgorithm.Valencia}, R) + j == 0 && return large_dij + @inbounds valencia_distance(eereco, i, j, R) +end + +Base.@propagate_inbounds @inline function valencia_distance(eereco, i, j, R) + return valencia_distance_inv(eereco, i, j, inv(R * R)) +end + +# Array-based helpers +Base.@propagate_inbounds @inline function valencia_distance_inv_arrays(E2p, nx, ny, nz, i, j, invR2) + angular_dist = angular_distance_arrays(nx, ny, nz, i, j) + min(E2p[i], E2p[j]) * 2 * angular_dist * invR2 +end + +# Scaled variant +Base.@propagate_inbounds @inline function valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, j) + angular_dist = angular_distance_arrays(nx, ny, nz, i, j) + min(E2p_scaled[i], E2p_scaled[j]) * angular_dist +end + +# Beam distance helpers +Base.@propagate_inbounds @inline function valencia_beam_distance(eereco, i, γ, β) + if hasproperty(eereco, :nz) + nzv = eereco.nz; E2pv = eereco.E2p + nz_i = nzv[i] + sin2 = 1 - nz_i * nz_i + E2p = E2pv[i] + else + nz_i = eereco[i].nz + sin2 = 1 - nz_i * nz_i + E2p = eereco[i].E2p + end + if γ == 1.0 + return E2p * sin2 + elseif γ == 2.0 + return E2p * (sin2 * sin2) + else + return E2p * sin2^γ + end +end + +Base.@propagate_inbounds @inline function valencia_beam_distance_arrays(E2p, nz, i, γ, β) + nz_i = nz[i] + sin2 = 1 - nz_i * nz_i + E2p_i = E2p[i] + if γ == 1.0 + return E2p_i * sin2 + elseif γ == 2.0 + return E2p_i * (sin2 * sin2) + else + return E2p_i * sin2^γ + end +end + +## Precomputed Valencia nearest-neighbour initializer using precomputed arrays +Base.@propagate_inbounds @inline function get_angular_nearest_neighbours_valencia_precomputed!(eereco, + E2p_scaled::AbstractVector, + beam_term::AbstractVector, + p, γ = 1.0, R = 4.0) + N = length(eereco) + nx = eereco.nx; ny = eereco.ny; nz = eereco.nz + nndist = eereco.nndist; nni = eereco.nni + @inbounds for i in 1:N + nndist[i] = Inf + nni[i] = i + end + @inbounds for i in 1:N + local_nndist_i = nndist[i] + local_nni_i = nni[i] + @inbounds for j in (i + 1):N + this_metric = valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, j) + if this_metric < local_nndist_i + local_nndist_i = this_metric + local_nni_i = j + end + if this_metric < nndist[j] + nndist[j] = this_metric + nni[j] = i + end + end + nndist[i] = local_nndist_i + nni[i] = local_nni_i + end + @inbounds for i in 1:N + eereco.dijdist[i] = valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, nni[i]) + end + @inbounds for i in 1:N + beam_closer = beam_term[i] < eereco.dijdist[i] + eereco.dijdist[i] = beam_closer ? beam_term[i] : eereco.dijdist[i] + eereco.nni[i] = beam_closer ? 0 : eereco.nni[i] + end +end + +## Precomputed Valencia no-cross update using E2p_scaled and beam_term +@inline function update_nn_no_cross_arrays_precomputed!(nndist::AbstractVector, nni::AbstractVector, + nx::AbstractVector, ny::AbstractVector, nz::AbstractVector, + E2p_scaled::AbstractVector, beam_term::AbstractVector, + dijdist::AbstractVector, + i::Integer, N::Integer, + dij_factor, β = 1.0, γ = 1.0, R = 4.0) + nndist_i = Inf + nni_i = i + @inbounds for j in 1:N + if j != i + this_metric = valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, j) + if this_metric < nndist_i + nndist_i = this_metric + nni_i = j + end + end + end + dijdist_i = valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, nni_i) + if beam_term[i] < dijdist_i + dijdist_i = beam_term[i] + nni_i = 0 + end + nndist[i] = nndist_i + nni[i] = nni_i + dijdist[i] = dijdist_i +end + +## Precomputed Valencia cross-update using E2p_scaled and beam_term +@inline function update_nn_cross_arrays_precomputed!(nndist::AbstractVector, nni::AbstractVector, + nx::AbstractVector, ny::AbstractVector, nz::AbstractVector, + E2p_scaled::AbstractVector, beam_term::AbstractVector, + dijdist::AbstractVector, + i::Integer, N::Integer, + dij_factor, β = 1.0, γ = 1.0, R = 4.0) + nndist_i = Inf + nni_i = i + @inbounds for j in 1:(i-1) + this_metric = valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, j) + if this_metric < nndist_i + nndist_i = this_metric + nni_i = j + end + if this_metric < nndist[j] + nndist[j] = this_metric + nni[j] = i + dijdist[j] = this_metric + if beam_term[j] < dijdist[j] + dijdist[j] = beam_term[j] + nni[j] = 0 + end + end + end + @inbounds for j in (i+1):N + this_metric = valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, j) + if this_metric < nndist_i + nndist_i = this_metric + nni_i = j + end + if this_metric < nndist[j] + nndist[j] = this_metric + nni[j] = i + dijdist[j] = this_metric + if beam_term[j] < dijdist[j] + dijdist[j] = beam_term[j] + nni[j] = 0 + end + end + end + # Finalize slot i + dijdist_i = valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, nni_i) + if beam_term[i] < dijdist_i + dijdist_i = beam_term[i] + nni_i = 0 + end + nndist[i] = nndist_i + nni[i] = nni_i + dijdist[i] = dijdist_i +end + +# Valencia-specialised implementation of the main algorithm +function _ee_genkt_algorithm_valencia(; particles::AbstractVector{EEJet}, + algorithm::JetAlgorithm.Algorithm, p::Real, R = 4.0, + recombine = addjets, γ::Real = 1.0, + beta::Union{Real, Nothing} = nothing) + N::Int = length(particles) + R2 = R^2 + if algorithm == JetAlgorithm.Valencia && beta !== nothing + p = beta + end + dij_factor = 1.0 + eereco = StructArray{EERecoJet}(undef, N) + fill_reco_array!(eereco, particles, R2, p) + history, Qtot = initial_history(particles) + clusterseq = ClusterSequence(algorithm, p, R, RecoStrategy.N2Plain, particles, history, + Qtot) + indexv = eereco.index + nni_v = eereco.nni + nndist_v = eereco.nndist + dijdist_v = eereco.dijdist + nxv = eereco.nx + nyv = eereco.ny + nzv = eereco.nz + E2pv = eereco.E2p + invR2 = inv(R * R) + factor = 2 * invR2 + E2p_scaled = similar(E2pv) + beam_term = similar(E2pv) + @inbounds for k in 1:N + E2p_scaled[k] = E2pv[k] * factor + nz_k = nzv[k] + sin2 = 1.0 - nz_k * nz_k + if γ == 1.0 + beam_term[k] = E2pv[k] * sin2 + elseif γ == 2.0 + beam_term[k] = E2pv[k] * (sin2 * sin2) + else + beam_term[k] = E2pv[k] * sin2^γ + end + end + get_angular_nearest_neighbours_valencia_precomputed!(eereco, E2p_scaled, beam_term, p, γ, R) + iter = 0 + while N != 0 + iter += 1 + dij_min, ijetA = fast_findmin(dijdist_v, N) + ijetB = nni_v[ijetA] + if ijetB == 0 + ijetB = ijetA + add_step_to_history!(clusterseq, + clusterseq.jets[indexv[ijetA]]._cluster_hist_index, + BeamJet, Invalid, dij_min) + elseif N == 1 + ijetB = ijetA + add_step_to_history!(clusterseq, + clusterseq.jets[indexv[ijetA]]._cluster_hist_index, + BeamJet, Invalid, dij_min) + else + if ijetB < ijetA + ijetA, ijetB = ijetB, ijetA + end + jetA = clusterseq.jets[indexv[ijetA]] + jetB = clusterseq.jets[indexv[ijetB]] + merged_jet = recombine(jetA, jetB; + cluster_hist_index = length(clusterseq.history) + 1) + push!(clusterseq.jets, merged_jet) + newjet_k = length(clusterseq.jets) + add_step_to_history!(clusterseq, + minmax(cluster_hist_index(jetA), + cluster_hist_index(jetB))..., + newjet_k, dij_min) + indexv[ijetA] = newjet_k + nni_v[ijetA] = 0 + nndist_v[ijetA] = R2 + nxv[ijetA] = nx(merged_jet) + nyv[ijetA] = ny(merged_jet) + nzv[ijetA] = nz(merged_jet) + E = energy(merged_jet) + if p isa Int + if p == 1 + E2pv[ijetA] = E * E + else + E2 = E * E + E2pv[ijetA] = E2^p + end + else + E2pv[ijetA] = E^(2p) + end + E2p_scaled[ijetA] = E2pv[ijetA] * factor + nz_k = nzv[ijetA] + sin2_k = 1.0 - nz_k * nz_k + if γ == 1.0 + beam_term[ijetA] = E2pv[ijetA] * sin2_k + elseif γ == 2.0 + beam_term[ijetA] = E2pv[ijetA] * (sin2_k * sin2_k) + else + beam_term[ijetA] = E2pv[ijetA] * sin2_k^γ + end + end + if ijetB != N + indexv[ijetB] = indexv[N] + nni_v[ijetB] = nni_v[N] + nndist_v[ijetB] = nndist_v[N] + dijdist_v[ijetB] = dijdist_v[N] + nxv[ijetB] = nxv[N] + nyv[ijetB] = nyv[N] + nzv[ijetB] = nzv[N] + E2pv[ijetB] = E2pv[N] + E2p_scaled[ijetB] = E2p_scaled[N] + beam_term[ijetB] = beam_term[N] + end + N -= 1 + @inbounds for i in 1:N + if (ijetB != N + 1) && (nni_v[i] == N + 1) + nni_v[i] = ijetB + else + if (nni_v[i] == ijetA) || (nni_v[i] == ijetB) || (nni_v[i] > N) + update_nn_no_cross_arrays_precomputed!(nndist_v, nni_v, nxv, nyv, nzv, + E2p_scaled, beam_term, dijdist_v, + i, N, dij_factor, p, γ, R) + end + end + end + if ijetA != ijetB + update_nn_cross_arrays_precomputed!(nndist_v, nni_v, nxv, nyv, nzv, + E2p_scaled, beam_term, dijdist_v, + ijetA, N, dij_factor, p, γ, R) + end + end + clusterseq +end diff --git a/test/test-valencia.jl b/test/test-valencia.jl index 8431cd11..10bbe09e 100644 --- a/test/test-valencia.jl +++ b/test/test-valencia.jl @@ -102,3 +102,58 @@ end R = 0.8, γ = 1.2) @test cs isa JetReconstruction.ClusterSequence end + + + @testset "Valencia precomputed helpers (unit)" begin + # Construct a tiny set of EEJet with controlled directions and energies + # Ensure all jets have non-zero momentum to avoid undefined direction cosines + p1 = EEJet(0.0, 0.0, 1.0, 10.0; cluster_hist_index = 1) + p2 = EEJet(0.0, 0.1, 0.5, 5.0; cluster_hist_index = 2) + p3 = EEJet(0.1, 0.0, -1.0, 2.0; cluster_hist_index = 3) + parts = [p1, p2, p3] + N = length(parts) + R = 1.0 + pwr = 1.2 + γ = 1.0 + + # Prepare SoA like Valencia entrypoint + eereco = StructArray{EERecoJet}(undef, N) + fill_reco_array!(eereco, parts, R^2, pwr) + + nx = eereco.nx; ny = eereco.ny; nz = eereco.nz; E2p = eereco.E2p + invR2 = inv(R * R) + factor = 2 * invR2 + E2p_scaled = similar(E2p) + beam_term = similar(E2p) + for k in 1:N + E2p_scaled[k] = E2p[k] * factor + sin2 = 1.0 - nz[k] * nz[k] + beam_term[k] = E2p[k] * sin2^γ + end + + # valencia_distance_inv_scaled_arrays: compare to manual computation + i, j = 1, 2 + ang = 1.0 - nx[i]*nx[j] - ny[i]*ny[j] - nz[i]*nz[j] + @test JetReconstruction.valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, j) ≈ min(E2p_scaled[i], E2p_scaled[j]) * ang + + # beam distance arrays + @test JetReconstruction.valencia_beam_distance_arrays(E2p, nz, 1, γ, pwr) ≈ beam_term[1] + + # initializer: should populate nni/nndist/dijdist without error + JetReconstruction.get_angular_nearest_neighbours_valencia_precomputed!(eereco, E2p_scaled, beam_term, pwr, γ, R) + @test all(isfinite, eereco.nndist) + @test all(isfinite, eereco.dijdist) + @test all(i -> isa(i, Int), eereco.nni) + + # array-based no-cross and cross update helpers should execute and produce finite outputs + nnd = copy(eereco.nndist); nni = copy(eereco.nni); dijd = copy(eereco.dijdist) + JetReconstruction.update_nn_no_cross_arrays_precomputed!(nnd, nni, nx, ny, nz, E2p_scaled, beam_term, dijd, 2, N, 1.0, pwr, γ, R) + @test isfinite(nnd[2]) + @test isfinite(dijd[2]) + + # cross update (should update other slots if appropriate) — assert runs and outputs are finite + nnd2 = copy(nnd); nni2 = copy(nni); dijd2 = copy(dijd) + JetReconstruction.update_nn_cross_arrays_precomputed!(nnd2, nni2, nx, ny, nz, E2p_scaled, beam_term, dijd2, 2, N, 1.0, pwr, γ, R) + @test all(isfinite, nnd2) + @test all(isfinite, dijd2) + end From 6f0e4554d83548c7c6668152dfff82b5f15731a8 Mon Sep 17 00:00:00 2001 From: Matt LeBlanc Date: Sun, 17 Aug 2025 17:40:52 -0400 Subject: [PATCH 16/32] Cleanup -- factorize VLC code and improve test coverage --- src/JetReconstruction.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/JetReconstruction.jl b/src/JetReconstruction.jl index dd171900..b83de2b8 100644 --- a/src/JetReconstruction.jl +++ b/src/JetReconstruction.jl @@ -79,6 +79,7 @@ export tiled_jet_reconstruct ## E+E- algorithms include("EEAlgorithm.jl") export ee_genkt_algorithm +include("EEAlgorithmValencia.jl") ## SoftKiller include("SoftKiller.jl") From 486e0b0b9901ff510a8f847ad3cbab8c7863147b Mon Sep 17 00:00:00 2001 From: Matt LeBlanc Date: Sun, 17 Aug 2025 17:46:23 -0400 Subject: [PATCH 17/32] Docstrings --- src/EEAlgorithm.jl | 83 +++++++++++++++++++++++++++++++++++--- src/EEAlgorithmValencia.jl | 72 +++++++++++++++++++++++++++++---- 2 files changed, 143 insertions(+), 12 deletions(-) diff --git a/src/EEAlgorithm.jl b/src/EEAlgorithm.jl index c4289fd9..5699ba7d 100644 --- a/src/EEAlgorithm.jl +++ b/src/EEAlgorithm.jl @@ -27,7 +27,15 @@ Base.@propagate_inbounds @inline function angular_distance(eereco, i, j) end end -# Array-based angular distance helper (operates on direction-cosine arrays) +""" + angular_distance_arrays(nx, ny, nz, i, j) -> Float64 + +Compute the angular distance (1 - cos θ) between entries `i` and `j` using +direction-cosine arrays `nx`, `ny`, `nz`. + +# Returns +- `Float64`: angular distance. +""" Base.@propagate_inbounds @inline function angular_distance_arrays(nx, ny, nz, i, j) @muladd 1.0 - nx[i] * nx[j] - ny[i] * ny[j] - nz[i] * nz[j] end @@ -67,7 +75,13 @@ end throw(ArgumentError("Algorithm $algorithm not supported for dij_dist")) end -# Val-specialized nearest neighbour search (removes runtime branches in hot loops) +""" + get_angular_nearest_neighbours!(eereco, ::Val{JetAlgorithm.Durham}, dij_factor, p, γ=1.0, R=4.0) + +Compute initial nearest-neighbour distances for the Durham algorithm and fill +`eereco.nndist`, `eereco.nni` and `eereco.dijdist` accordingly. This variant +is specialised for the Durham metric and operates on the StructArray layout. +""" @inline function get_angular_nearest_neighbours!(eereco, ::Val{JetAlgorithm.Durham}, dij_factor, p, γ = 1.0, R = 4.0) N = length(eereco) @@ -96,6 +110,13 @@ end end end +""" + get_angular_nearest_neighbours!(eereco, ::Val{JetAlgorithm.EEKt}, dij_factor, p, γ=1.0, R=4.0) + +Compute initial nearest-neighbour distances for the EEKt algorithm and fill +`eereco.nndist`, `eereco.nni` and `eereco.dijdist`. Performs beam-distance +checks appropriate for EEKt. +""" @inline function get_angular_nearest_neighbours!(eereco, ::Val{JetAlgorithm.EEKt}, dij_factor, p, γ = 1.0, R = 4.0) N = length(eereco) @@ -121,14 +142,26 @@ end end end -# Forwarder to Val-specialized version +""" + get_angular_nearest_neighbours!(eereco, algorithm::JetAlgorithm.Algorithm, dij_factor, p, γ=1.0, R=4.0) + +Forwarding wrapper that dispatches to the `Val{...}` specialised +`get_angular_nearest_neighbours!` implementation to avoid branches in tight +loops. +""" @inline function get_angular_nearest_neighbours!(eereco, algorithm::JetAlgorithm.Algorithm, dij_factor, p, γ = 1.0, R = 4.0) return get_angular_nearest_neighbours!(eereco, Val(algorithm), dij_factor, p, γ, R) end -# Update the nearest neighbour for jet i, w.r.t. all other active jets +""" + update_nn_no_cross!(eereco, i, N, algorithm::JetAlgorithm.Algorithm, dij_factor, β=1.0, γ=1.0, R=4.0) + +Update nearest-neighbour information for slot `i` (no-cross variant) by +forwarding to algorithm-specific `Val{...}` specialisations. This wrapper +avoids runtime branching in the hot inner loop. +""" @inline function update_nn_no_cross!(eereco, i, N, algorithm::JetAlgorithm.Algorithm, dij_factor, β = 1.0, γ = 1.0, R = 4.0) # Forward to Val-specialized implementations to avoid runtime branches @@ -136,6 +169,13 @@ end end # Val-specialized no-cross update +""" + update_nn_no_cross!(eereco, i, N, ::Val{JetAlgorithm.Durham}, dij_factor, _β=1.0, _γ=1.0, R=4.0) + +Durham-specialised update of the nearest neighbour for slot `i` (no-cross +variant). Updates `nndist`, `nni` and the inline `dijdist` for slot `i` using +direction-cosine arrays for performance. +""" @inline function update_nn_no_cross!(eereco, i, N, ::Val{JetAlgorithm.Durham}, dij_factor, _β = 1.0, _γ = 1.0, R = 4.0) nndist = eereco.nndist; nni = eereco.nni @@ -162,6 +202,13 @@ end eereco.dijdist[i] = minE2p * dij_factor * nndist[i] end +""" + update_nn_no_cross!(eereco, i, N, ::Val{JetAlgorithm.EEKt}, dij_factor, _β=1.0, _γ=1.0, R=4.0) + +EEKt-specialised update of the nearest neighbour for slot `i` (no-cross +variant). Also applies the EEKt beam-distance check and updates `nni` when a +beam is closer than the dij distance. +""" @inline function update_nn_no_cross!(eereco, i, N, ::Val{JetAlgorithm.EEKt}, dij_factor, _β = 1.0, _γ = 1.0, R = 4.0) nndist = eereco.nndist; nni = eereco.nni @@ -191,13 +238,25 @@ end eereco.nni[i] = beam_close ? 0 : eereco.nni[i] end +""" + update_nn_cross!(eereco, i, N, algorithm::JetAlgorithm.Algorithm, dij_factor, β=1.0, γ=1.0, R=4.0) + +Forwarding wrapper for the cross-update variant. Dispatches to the +`Val{...}`-specialised `update_nn_cross!` to remove runtime branches. +""" @inline function update_nn_cross!(eereco, i, N, algorithm::JetAlgorithm.Algorithm, dij_factor, β = 1.0, γ = 1.0, R = 4.0) # Forward to Val-specialized implementations to avoid runtime branches return update_nn_cross!(eereco, i, N, Val(algorithm), dij_factor, β, γ, R) end -# Val-specialized cross update +""" + update_nn_cross!(eereco, i, N, ::Val{JetAlgorithm.Durham}, dij_factor, _β=1.0, _γ=1.0, R=4.0) + +Durham-specialised cross-update: updates nearest-neighbour information for +slot `i` and propagates any changes to other slots when `i` becomes their new +nearest neighbour. Computes inline dij updates to avoid function-call overhead. +""" @inline function update_nn_cross!(eereco, i, N, ::Val{JetAlgorithm.Durham}, dij_factor, _β = 1.0, _γ = 1.0, R = 4.0) nndist = eereco.nndist; nni = eereco.nni @@ -228,6 +287,13 @@ end eereco.dijdist[i] = minE2p_i * dij_factor * nndist[i] end +""" + update_nn_cross!(eereco, i, N, ::Val{JetAlgorithm.EEKt}, dij_factor, _β=1.0, _γ=1.0, R=4.0) + +EEKt-specialised cross-update. Updates `nndist`, `nni` and `dijdist` for +both the updated slot and any affected neighbours, performing beam checks +where required. +""" @inline function update_nn_cross!(eereco, i, N, ::Val{JetAlgorithm.EEKt}, dij_factor, _β = 1.0, _γ = 1.0, R = 4.0) nndist = eereco.nndist; nni = eereco.nni @@ -266,6 +332,13 @@ end nni[i] = beam_close ? 0 : nni[i] end +""" + ee_check_consistency(clusterseq, eereco, N) + +Run an internal consistency check over the reconstruction state. Emits +`@error` logs for invalid nearest-neighbour indices or malformed history +entries. Intended for debugging; inexpensive checks only. +""" function ee_check_consistency(clusterseq, eereco, N) # Check the consistency of the reconstruction state for i in 1:N diff --git a/src/EEAlgorithmValencia.jl b/src/EEAlgorithmValencia.jl index 0e6c2982..f94573ef 100644 --- a/src/EEAlgorithmValencia.jl +++ b/src/EEAlgorithmValencia.jl @@ -2,7 +2,12 @@ # Valencia-specialised helpers and implementation ################################################################################ -# Valencia distance helpers +""" + valencia_distance_inv(eereco, i, j, invR2) -> Float64 + +Calculate the Valencia dij metric (scaled by `invR2`) between slots `i` and +`j` in a StructArray `eereco`. Uses E2p and direction cosines from `eereco`. +""" Base.@propagate_inbounds @inline function valencia_distance_inv(eereco, i, j, invR2) if hasproperty(eereco, :nx) nx = eereco.nx; ny = eereco.ny; nz = eereco.nz; E2p = eereco.E2p @@ -19,30 +24,55 @@ end """ dij_dist(eereco, i, j, dij_factor, ::Val{JetAlgorithm.Valencia}, R) -Valencia dij distance uses the full Valencia metric, including the 2*(1-cosθ)/R² factor. +Valencia-specialised `dij_dist` which computes the Valencia dij metric for +slots `i` and `j`. Returns a large sentinel distance for beam index `j==0`. """ @inline function dij_dist(eereco, i, j, dij_factor, ::Val{JetAlgorithm.Valencia}, R) j == 0 && return large_dij @inbounds valencia_distance(eereco, i, j, R) end +""" + valencia_distance(eereco, i, j, R) -> Float64 + +Compute the Valencia dij metric between `i` and `j` using explicit `R` and +delegating to `valencia_distance_inv` with the appropriate scaling. +""" Base.@propagate_inbounds @inline function valencia_distance(eereco, i, j, R) return valencia_distance_inv(eereco, i, j, inv(R * R)) end # Array-based helpers +""" + valencia_distance_inv_arrays(E2p, nx, ny, nz, i, j, invR2) -> Float64 + +Array-based variant of `valencia_distance_inv` that works directly on raw +vectors (useful for the precomputed helpers/fast paths). +""" Base.@propagate_inbounds @inline function valencia_distance_inv_arrays(E2p, nx, ny, nz, i, j, invR2) angular_dist = angular_distance_arrays(nx, ny, nz, i, j) min(E2p[i], E2p[j]) * 2 * angular_dist * invR2 end # Scaled variant +""" + valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, j) -> Float64 + +Compute the Valencia distance using a pre-scaled E2p vector (`E2p_scaled`), +avoiding repeated multiplication by the R-dependent factor. Intended for +performance-sensitive precomputed loops. +""" Base.@propagate_inbounds @inline function valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, j) angular_dist = angular_distance_arrays(nx, ny, nz, i, j) min(E2p_scaled[i], E2p_scaled[j]) * angular_dist end -# Beam distance helpers +""" + valencia_beam_distance(eereco, i, γ, β) -> Float64 + +Compute the Valencia beam-distance term for slot `i` given angular exponent +`γ` and (unused) `β`. Uses direction cosine `nz` and E2p value. +""" Base.@propagate_inbounds @inline function valencia_beam_distance(eereco, i, γ, β) if hasproperty(eereco, :nz) nzv = eereco.nz; E2pv = eereco.E2p @@ -63,6 +93,12 @@ Base.@propagate_inbounds @inline function valencia_beam_distance(eereco, i, γ, end end +""" + valencia_beam_distance_arrays(E2p, nz, i, γ, β) -> Float64 + +Array-based variant of `valencia_beam_distance` that operates on raw `E2p` +and `nz` vectors. +""" Base.@propagate_inbounds @inline function valencia_beam_distance_arrays(E2p, nz, i, γ, β) nz_i = nz[i] sin2 = 1 - nz_i * nz_i @@ -76,7 +112,13 @@ Base.@propagate_inbounds @inline function valencia_beam_distance_arrays(E2p, nz, end end -## Precomputed Valencia nearest-neighbour initializer using precomputed arrays +""" + get_angular_nearest_neighbours_valencia_precomputed!(eereco, E2p_scaled, beam_term, p, γ=1.0, R=4.0) + +Initialize nearest-neighbour arrays for the Valencia algorithm using +precomputed scaled energy vector `E2p_scaled` and `beam_term`. This avoids +repeated per-pair scaling inside tight loops. +""" Base.@propagate_inbounds @inline function get_angular_nearest_neighbours_valencia_precomputed!(eereco, E2p_scaled::AbstractVector, beam_term::AbstractVector, @@ -115,7 +157,12 @@ Base.@propagate_inbounds @inline function get_angular_nearest_neighbours_valenci end end -## Precomputed Valencia no-cross update using E2p_scaled and beam_term +""" + update_nn_no_cross_arrays_precomputed!(nndist, nni, nx, ny, nz, E2p_scaled, beam_term, dijdist, i, N, dij_factor, β=1.0, γ=1.0, R=4.0) + +Precomputed Valencia no-cross nearest-neighbour update. Uses `E2p_scaled` +and `beam_term` arrays to compute distances without per-pair scaling. +""" @inline function update_nn_no_cross_arrays_precomputed!(nndist::AbstractVector, nni::AbstractVector, nx::AbstractVector, ny::AbstractVector, nz::AbstractVector, E2p_scaled::AbstractVector, beam_term::AbstractVector, @@ -143,7 +190,12 @@ end dijdist[i] = dijdist_i end -## Precomputed Valencia cross-update using E2p_scaled and beam_term +""" + update_nn_cross_arrays_precomputed!(nndist, nni, nx, ny, nz, E2p_scaled, beam_term, dijdist, i, N, dij_factor, β=1.0, γ=1.0, R=4.0) + +Precomputed Valencia cross-update variant: updates neighbour data for slot +`i` and any affected neighbours using `E2p_scaled` and `beam_term`. +""" @inline function update_nn_cross_arrays_precomputed!(nndist::AbstractVector, nni::AbstractVector, nx::AbstractVector, ny::AbstractVector, nz::AbstractVector, E2p_scaled::AbstractVector, beam_term::AbstractVector, @@ -195,7 +247,13 @@ end dijdist[i] = dijdist_i end -# Valencia-specialised implementation of the main algorithm +""" + _ee_genkt_algorithm_valencia(; particles::AbstractVector{EEJet}, algorithm::JetAlgorithm.Algorithm, p::Real, R=4.0, recombine=addjets, γ::Real=1.0, beta::Union{Real, Nothing}=nothing) + +Valencia-specialised implementation of the e+e- gen-kT clustering algorithm. +This implementation precomputes scaled energy and beam-term arrays to speed up +nearest-neighbour computations for the Valencia metric. +""" function _ee_genkt_algorithm_valencia(; particles::AbstractVector{EEJet}, algorithm::JetAlgorithm.Algorithm, p::Real, R = 4.0, recombine = addjets, γ::Real = 1.0, From 8300e797766dbcf991b35dad3908031b08b0e3df Mon Sep 17 00:00:00 2001 From: Matt LeBlanc Date: Sun, 17 Aug 2025 17:46:56 -0400 Subject: [PATCH 18/32] formatting --- src/EEAlgorithm.jl | 221 ++++++++++++++++++++++--------------- src/EEAlgorithmValencia.jl | 81 +++++++++----- src/EEJet.jl | 3 +- test/test-valencia.jl | 121 +++++++++++--------- 4 files changed, 253 insertions(+), 173 deletions(-) diff --git a/src/EEAlgorithm.jl b/src/EEAlgorithm.jl index 5699ba7d..910af4b5 100644 --- a/src/EEAlgorithm.jl +++ b/src/EEAlgorithm.jl @@ -19,11 +19,14 @@ Calculate the angular distance between two jets `i` and `j` using the formula """ Base.@propagate_inbounds @inline function angular_distance(eereco, i, j) if hasproperty(eereco, :nx) - nx = eereco.nx; ny = eereco.ny; nz = eereco.nz + nx = eereco.nx + ny = eereco.ny + nz = eereco.nz @muladd 1.0 - nx[i] * nx[j] - ny[i] * ny[j] - nz[i] * nz[j] else # Fallback for Array-of-structs (AoS) - @muladd 1.0 - eereco[i].nx * eereco[j].nx - eereco[i].ny * eereco[j].ny - eereco[i].nz * eereco[j].nz + @muladd 1.0 - eereco[i].nx * eereco[j].nx - eereco[i].ny * eereco[j].ny - + eereco[i].nz * eereco[j].nz end end @@ -85,8 +88,11 @@ is specialised for the Durham metric and operates on the StructArray layout. @inline function get_angular_nearest_neighbours!(eereco, ::Val{JetAlgorithm.Durham}, dij_factor, p, γ = 1.0, R = 4.0) N = length(eereco) - nx = eereco.nx; ny = eereco.ny; nz = eereco.nz - nndist = eereco.nndist; nni = eereco.nni + nx = eereco.nx + ny = eereco.ny + nz = eereco.nz + nndist = eereco.nndist + nni = eereco.nni @inbounds for i in 1:N local_nndist_i = large_distance local_nni_i = i @@ -178,17 +184,20 @@ direction-cosine arrays for performance. """ @inline function update_nn_no_cross!(eereco, i, N, ::Val{JetAlgorithm.Durham}, dij_factor, _β = 1.0, _γ = 1.0, R = 4.0) - nndist = eereco.nndist; nni = eereco.nni - nx = eereco.nx; ny = eereco.ny; nz = eereco.nz + nndist = eereco.nndist + nni = eereco.nni + nx = eereco.nx + ny = eereco.ny + nz = eereco.nz nndist[i] = large_distance nni[i] = i - @inbounds for j in 1:(i-1) + @inbounds for j in 1:(i - 1) this_metric = angular_distance_arrays(nx, ny, nz, i, j) better_nndist_i = this_metric < nndist[i] nndist[i] = better_nndist_i ? this_metric : nndist[i] nni[i] = better_nndist_i ? j : nni[i] end - @inbounds for j in (i+1):N + @inbounds for j in (i + 1):N this_metric = angular_distance_arrays(nx, ny, nz, i, j) better_nndist_i = this_metric < nndist[i] nndist[i] = better_nndist_i ? this_metric : nndist[i] @@ -211,19 +220,22 @@ beam is closer than the dij distance. """ @inline function update_nn_no_cross!(eereco, i, N, ::Val{JetAlgorithm.EEKt}, dij_factor, _β = 1.0, _γ = 1.0, R = 4.0) - nndist = eereco.nndist; nni = eereco.nni - nx = eereco.nx; ny = eereco.ny; nz = eereco.nz + nndist = eereco.nndist + nni = eereco.nni + nx = eereco.nx + ny = eereco.ny + nz = eereco.nz E2p = eereco.E2p E2p_i = E2p[i] nndist[i] = large_distance nni[i] = i - @inbounds for j in 1:(i-1) + @inbounds for j in 1:(i - 1) this_metric = angular_distance_arrays(nx, ny, nz, i, j) better_nndist_i = this_metric < nndist[i] nndist[i] = better_nndist_i ? this_metric : nndist[i] nni[i] = better_nndist_i ? j : nni[i] end - @inbounds for j in (i+1):N + @inbounds for j in (i + 1):N this_metric = angular_distance_arrays(nx, ny, nz, i, j) better_nndist_i = this_metric < nndist[i] nndist[i] = better_nndist_i ? this_metric : nndist[i] @@ -259,8 +271,11 @@ nearest neighbour. Computes inline dij updates to avoid function-call overhead. """ @inline function update_nn_cross!(eereco, i, N, ::Val{JetAlgorithm.Durham}, dij_factor, _β = 1.0, _γ = 1.0, R = 4.0) - nndist = eereco.nndist; nni = eereco.nni - nx = eereco.nx; ny = eereco.ny; nz = eereco.nz + nndist = eereco.nndist + nni = eereco.nni + nx = eereco.nx + ny = eereco.ny + nz = eereco.nz nndist[i] = large_distance nni[i] = i E2p = eereco.E2p @@ -296,8 +311,11 @@ where required. """ @inline function update_nn_cross!(eereco, i, N, ::Val{JetAlgorithm.EEKt}, dij_factor, _β = 1.0, _γ = 1.0, R = 4.0) - nndist = eereco.nndist; nni = eereco.nni - nx = eereco.nx; ny = eereco.ny; nz = eereco.nz + nndist = eereco.nndist + nni = eereco.nni + nx = eereco.nx + ny = eereco.ny + nz = eereco.nz E2p = eereco.E2p E2p_i = E2p[i] nndist[i] = large_distance @@ -361,18 +379,23 @@ end ################################################################################ @inline function update_nn_no_cross_arrays!(nndist::AbstractVector, nni::AbstractVector, - nx::AbstractVector, ny::AbstractVector, nz::AbstractVector, - E2p::AbstractVector, dijdist::AbstractVector, - i::Integer, N::Integer, algorithm::JetAlgorithm.Algorithm, - dij_factor, β = 1.0, γ = 1.0, R = 4.0) - return update_nn_no_cross_arrays!(nndist, nni, nx, ny, nz, E2p, dijdist, i, N, Val(algorithm), dij_factor, β, γ, R) + nx::AbstractVector, ny::AbstractVector, + nz::AbstractVector, + E2p::AbstractVector, dijdist::AbstractVector, + i::Integer, N::Integer, + algorithm::JetAlgorithm.Algorithm, + dij_factor, β = 1.0, γ = 1.0, R = 4.0) + return update_nn_no_cross_arrays!(nndist, nni, nx, ny, nz, E2p, dijdist, i, N, + Val(algorithm), dij_factor, β, γ, R) end @inline function update_nn_no_cross_arrays!(nndist::AbstractVector, nni::AbstractVector, - nx::AbstractVector, ny::AbstractVector, nz::AbstractVector, - E2p::AbstractVector, dijdist::AbstractVector, - i::Integer, N::Integer, ::Val{JetAlgorithm.Durham}, - dij_factor, _β = 1.0, _γ = 1.0, R = 4.0) + nx::AbstractVector, ny::AbstractVector, + nz::AbstractVector, + E2p::AbstractVector, dijdist::AbstractVector, + i::Integer, N::Integer, + ::Val{JetAlgorithm.Durham}, + dij_factor, _β = 1.0, _γ = 1.0, R = 4.0) nndist[i] = large_distance nni[i] = i @inbounds for j in 1:N @@ -391,10 +414,12 @@ end end @inline function update_nn_no_cross_arrays!(nndist::AbstractVector, nni::AbstractVector, - nx::AbstractVector, ny::AbstractVector, nz::AbstractVector, - E2p::AbstractVector, dijdist::AbstractVector, - i::Integer, N::Integer, ::Val{JetAlgorithm.EEKt}, - dij_factor, _β = 1.0, _γ = 1.0, R = 4.0) + nx::AbstractVector, ny::AbstractVector, + nz::AbstractVector, + E2p::AbstractVector, dijdist::AbstractVector, + i::Integer, N::Integer, + ::Val{JetAlgorithm.EEKt}, + dij_factor, _β = 1.0, _γ = 1.0, R = 4.0) nndist[i] = large_distance nni[i] = i E2p_i = E2p[i] @@ -415,22 +440,26 @@ end end @inline function update_nn_cross_arrays!(nndist::AbstractVector, nni::AbstractVector, - nx::AbstractVector, ny::AbstractVector, nz::AbstractVector, - E2p::AbstractVector, dijdist::AbstractVector, - i::Integer, N::Integer, algorithm::JetAlgorithm.Algorithm, - dij_factor, β = 1.0, γ = 1.0, R = 4.0) - return update_nn_cross_arrays!(nndist, nni, nx, ny, nz, E2p, dijdist, i, N, Val(algorithm), dij_factor, β, γ, R) + nx::AbstractVector, ny::AbstractVector, + nz::AbstractVector, + E2p::AbstractVector, dijdist::AbstractVector, + i::Integer, N::Integer, + algorithm::JetAlgorithm.Algorithm, + dij_factor, β = 1.0, γ = 1.0, R = 4.0) + return update_nn_cross_arrays!(nndist, nni, nx, ny, nz, E2p, dijdist, i, N, + Val(algorithm), dij_factor, β, γ, R) end @inline function update_nn_cross_arrays!(nndist::AbstractVector, nni::AbstractVector, - nx::AbstractVector, ny::AbstractVector, nz::AbstractVector, - E2p::AbstractVector, dijdist::AbstractVector, - i::Integer, N::Integer, ::Val{JetAlgorithm.Durham}, - dij_factor, _β = 1.0, _γ = 1.0, R = 4.0) + nx::AbstractVector, ny::AbstractVector, + nz::AbstractVector, + E2p::AbstractVector, dijdist::AbstractVector, + i::Integer, N::Integer, ::Val{JetAlgorithm.Durham}, + dij_factor, _β = 1.0, _γ = 1.0, R = 4.0) nndist[i] = large_distance nni[i] = i E2p_i = E2p[i] - @inbounds for j in 1:(i-1) + @inbounds for j in 1:(i - 1) this_metric = angular_distance_arrays(nx, ny, nz, i, j) better_nndist_i = this_metric < nndist[i] nndist[i] = better_nndist_i ? this_metric : nndist[i] @@ -442,7 +471,7 @@ end dijdist[j] = (E2p_i < E2p_j ? E2p_i : E2p_j) * dij_factor * this_metric end end - @inbounds for j in (i+1):N + @inbounds for j in (i + 1):N this_metric = angular_distance_arrays(nx, ny, nz, i, j) better_nndist_i = this_metric < nndist[i] nndist[i] = better_nndist_i ? this_metric : nndist[i] @@ -461,32 +490,33 @@ end end @inline function update_nn_cross_arrays!(nndist::AbstractVector, nni::AbstractVector, - nx::AbstractVector, ny::AbstractVector, nz::AbstractVector, - E2p::AbstractVector, dijdist::AbstractVector, - i::Integer, N::Integer, ::Val{JetAlgorithm.EEKt}, - dij_factor, _β = 1.0, _γ = 1.0, R = 4.0) + nx::AbstractVector, ny::AbstractVector, + nz::AbstractVector, + E2p::AbstractVector, dijdist::AbstractVector, + i::Integer, N::Integer, ::Val{JetAlgorithm.EEKt}, + dij_factor, _β = 1.0, _γ = 1.0, R = 4.0) E2p_i = E2p[i] nndist[i] = large_distance nni[i] = i - @inbounds for j in 1:(i-1) + @inbounds for j in 1:(i - 1) this_metric = angular_distance_arrays(nx, ny, nz, i, j) better_nndist_i = this_metric < nndist[i] nndist[i] = better_nndist_i ? this_metric : nndist[i] nni[i] = better_nndist_i ? j : nni[i] - if this_metric < nndist[j] - nndist[j] = this_metric - nni[j] = i - E2p_j = E2p[j] - new_dij = (E2p_i < E2p_j ? E2p_i : E2p_j) * dij_factor * this_metric - if E2p_j < new_dij - dijdist[j] = E2p_j - nni[j] = 0 - else - dijdist[j] = new_dij - end + if this_metric < nndist[j] + nndist[j] = this_metric + nni[j] = i + E2p_j = E2p[j] + new_dij = (E2p_i < E2p_j ? E2p_i : E2p_j) * dij_factor * this_metric + if E2p_j < new_dij + dijdist[j] = E2p_j + nni[j] = 0 + else + dijdist[j] = new_dij end + end end - @inbounds for j in (i+1):N + @inbounds for j in (i + 1):N this_metric = angular_distance_arrays(nx, ny, nz, i, j) better_nndist_i = this_metric < nndist[i] nndist[i] = better_nndist_i ? this_metric : nndist[i] @@ -521,7 +551,7 @@ Base.@propagate_inbounds @inline function fill_reco_array!(eereco, particles, R2 eereco.ny[i] = ny(particles[i]) eereco.nz[i] = nz(particles[i]) eereco.E2p[i] = energy(particles[i])^(2p) - # No precomputed beam factor; compute on demand to preserve previous behaviour + # No precomputed beam factor; compute on demand to preserve previous behaviour end end @@ -609,31 +639,37 @@ function ee_genkt_algorithm(particles::AbstractVector{T}; algorithm::JetAlgorith local_p = β === nothing ? p : β local_p = get_algorithm_power(p = local_p, algorithm = algorithm) return _ee_genkt_algorithm_valencia(particles = begin - if isnothing(preprocess) - if T == EEJet - recombination_particles = copy(particles) - sizehint!(recombination_particles, length(particles) * 2) - recombination_particles - else - recombination_particles = EEJet[] - sizehint!(recombination_particles, length(particles) * 2) - for (i, particle) in enumerate(particles) - push!(recombination_particles, EEJet(particle; cluster_hist_index = i)) - end - recombination_particles - end - else - recombination_particles = EEJet[] - sizehint!(recombination_particles, length(particles) * 2) - for (i, particle) in enumerate(particles) - push!(recombination_particles, - preprocess(particle, EEJet; cluster_hist_index = i)) - end - recombination_particles - end - end, - algorithm = algorithm, p = local_p, R = R, - recombine = recombine, γ = γ, beta = β) + if isnothing(preprocess) + if T == EEJet + recombination_particles = copy(particles) + sizehint!(recombination_particles, + length(particles) * 2) + recombination_particles + else + recombination_particles = EEJet[] + sizehint!(recombination_particles, + length(particles) * 2) + for (i, particle) in enumerate(particles) + push!(recombination_particles, + EEJet(particle; + cluster_hist_index = i)) + end + recombination_particles + end + else + recombination_particles = EEJet[] + sizehint!(recombination_particles, + length(particles) * 2) + for (i, particle) in enumerate(particles) + push!(recombination_particles, + preprocess(particle, EEJet; + cluster_hist_index = i)) + end + recombination_particles + end + end, + algorithm = algorithm, p = local_p, R = R, + recombine = recombine, γ = γ, beta = β) end # Check for consistency algorithm power (non-Valencia path) @@ -680,7 +716,8 @@ function ee_genkt_algorithm(particles::AbstractVector{T}; algorithm::JetAlgorith # internal implementations. This avoids per-iteration branching/Val() in # the hot inner loops. if algorithm == JetAlgorithm.Valencia - return _ee_genkt_algorithm_valencia(particles = recombination_particles, p = p, R = R, + return _ee_genkt_algorithm_valencia(particles = recombination_particles, p = p, + R = R, algorithm = algorithm, recombine = recombine, γ = γ) elseif algorithm == JetAlgorithm.Durham @@ -722,8 +759,13 @@ function _ee_genkt_algorithm_durham(; particles::AbstractVector{EEJet}, get_angular_nearest_neighbours!(eereco, Val(JetAlgorithm.Durham), dij_factor, p, γ, R) # Alias StructArray fields to local vectors to avoid repeated getindex - index = eereco.index; nni = eereco.nni; nndist = eereco.nndist - dijdist = eereco.dijdist; nx = eereco.nx; ny = eereco.ny; nz = eereco.nz + index = eereco.index + nni = eereco.nni + nndist = eereco.nndist + dijdist = eereco.dijdist + nx = eereco.nx + ny = eereco.ny + nz = eereco.nz E2p = eereco.E2p # Main loop @@ -731,8 +773,8 @@ function _ee_genkt_algorithm_durham(; particles::AbstractVector{EEJet}, @inbounds while N != 0 iter += 1 - dij_min, ijetA = fast_findmin(dijdist, N) - ijetB = nni[ijetA] + dij_min, ijetA = fast_findmin(dijdist, N) + ijetB = nni[ijetA] if ijetB == 0 ijetB = ijetA @@ -774,7 +816,8 @@ function _ee_genkt_algorithm_durham(; particles::AbstractVector{EEJet}, else if (nni[i] == ijetA) || (nni[i] == ijetB) || (nni[i] > N) update_nn_no_cross_arrays!(nndist, nni, nx, ny, nz, E2p, dijdist, - i, N, Val(JetAlgorithm.Durham), dij_factor, p, γ, R) + i, N, Val(JetAlgorithm.Durham), dij_factor, + p, γ, R) end end end diff --git a/src/EEAlgorithmValencia.jl b/src/EEAlgorithmValencia.jl index f94573ef..606fcd8f 100644 --- a/src/EEAlgorithmValencia.jl +++ b/src/EEAlgorithmValencia.jl @@ -10,13 +10,17 @@ Calculate the Valencia dij metric (scaled by `invR2`) between slots `i` and """ Base.@propagate_inbounds @inline function valencia_distance_inv(eereco, i, j, invR2) if hasproperty(eereco, :nx) - nx = eereco.nx; ny = eereco.ny; nz = eereco.nz; E2p = eereco.E2p + nx = eereco.nx + ny = eereco.ny + nz = eereco.nz + E2p = eereco.E2p angular_dist = angular_distance_arrays(nx, ny, nz, i, j) # Valencia dij : min(E_i^{2\u03b2}, E_j^{2\u03b2}) * 2 * (1 - cos \u03b8) * invR2 min(E2p[i], E2p[j]) * 2 * angular_dist * invR2 else # Fallback for Array-of-structs - angular_dist = 1.0 - eereco[i].nx * eereco[j].nx - eereco[i].ny * eereco[j].ny - eereco[i].nz * eereco[j].nz + angular_dist = 1.0 - eereco[i].nx * eereco[j].nx - eereco[i].ny * eereco[j].ny - + eereco[i].nz * eereco[j].nz min(eereco[i].E2p, eereco[j].E2p) * 2 * angular_dist * invR2 end end @@ -49,7 +53,8 @@ end Array-based variant of `valencia_distance_inv` that works directly on raw vectors (useful for the precomputed helpers/fast paths). """ -Base.@propagate_inbounds @inline function valencia_distance_inv_arrays(E2p, nx, ny, nz, i, j, invR2) +Base.@propagate_inbounds @inline function valencia_distance_inv_arrays(E2p, nx, ny, nz, i, + j, invR2) angular_dist = angular_distance_arrays(nx, ny, nz, i, j) min(E2p[i], E2p[j]) * 2 * angular_dist * invR2 end @@ -62,7 +67,9 @@ Compute the Valencia distance using a pre-scaled E2p vector (`E2p_scaled`), avoiding repeated multiplication by the R-dependent factor. Intended for performance-sensitive precomputed loops. """ -Base.@propagate_inbounds @inline function valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, j) +Base.@propagate_inbounds @inline function valencia_distance_inv_scaled_arrays(E2p_scaled, + nx, ny, nz, i, + j) angular_dist = angular_distance_arrays(nx, ny, nz, i, j) min(E2p_scaled[i], E2p_scaled[j]) * angular_dist end @@ -75,7 +82,8 @@ Compute the Valencia beam-distance term for slot `i` given angular exponent """ Base.@propagate_inbounds @inline function valencia_beam_distance(eereco, i, γ, β) if hasproperty(eereco, :nz) - nzv = eereco.nz; E2pv = eereco.E2p + nzv = eereco.nz + E2pv = eereco.E2p nz_i = nzv[i] sin2 = 1 - nz_i * nz_i E2p = E2pv[i] @@ -120,12 +128,17 @@ precomputed scaled energy vector `E2p_scaled` and `beam_term`. This avoids repeated per-pair scaling inside tight loops. """ Base.@propagate_inbounds @inline function get_angular_nearest_neighbours_valencia_precomputed!(eereco, - E2p_scaled::AbstractVector, - beam_term::AbstractVector, - p, γ = 1.0, R = 4.0) + E2p_scaled::AbstractVector, + beam_term::AbstractVector, + p, + γ = 1.0, + R = 4.0) N = length(eereco) - nx = eereco.nx; ny = eereco.ny; nz = eereco.nz - nndist = eereco.nndist; nni = eereco.nni + nx = eereco.nx + ny = eereco.ny + nz = eereco.nz + nndist = eereco.nndist + nni = eereco.nni @inbounds for i in 1:N nndist[i] = Inf nni[i] = i @@ -148,7 +161,8 @@ Base.@propagate_inbounds @inline function get_angular_nearest_neighbours_valenci nni[i] = local_nni_i end @inbounds for i in 1:N - eereco.dijdist[i] = valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, nni[i]) + eereco.dijdist[i] = valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, + nni[i]) end @inbounds for i in 1:N beam_closer = beam_term[i] < eereco.dijdist[i] @@ -163,12 +177,17 @@ end Precomputed Valencia no-cross nearest-neighbour update. Uses `E2p_scaled` and `beam_term` arrays to compute distances without per-pair scaling. """ -@inline function update_nn_no_cross_arrays_precomputed!(nndist::AbstractVector, nni::AbstractVector, - nx::AbstractVector, ny::AbstractVector, nz::AbstractVector, - E2p_scaled::AbstractVector, beam_term::AbstractVector, - dijdist::AbstractVector, - i::Integer, N::Integer, - dij_factor, β = 1.0, γ = 1.0, R = 4.0) +@inline function update_nn_no_cross_arrays_precomputed!(nndist::AbstractVector, + nni::AbstractVector, + nx::AbstractVector, + ny::AbstractVector, + nz::AbstractVector, + E2p_scaled::AbstractVector, + beam_term::AbstractVector, + dijdist::AbstractVector, + i::Integer, N::Integer, + dij_factor, β = 1.0, γ = 1.0, + R = 4.0) nndist_i = Inf nni_i = i @inbounds for j in 1:N @@ -196,15 +215,18 @@ end Precomputed Valencia cross-update variant: updates neighbour data for slot `i` and any affected neighbours using `E2p_scaled` and `beam_term`. """ -@inline function update_nn_cross_arrays_precomputed!(nndist::AbstractVector, nni::AbstractVector, - nx::AbstractVector, ny::AbstractVector, nz::AbstractVector, - E2p_scaled::AbstractVector, beam_term::AbstractVector, - dijdist::AbstractVector, - i::Integer, N::Integer, - dij_factor, β = 1.0, γ = 1.0, R = 4.0) +@inline function update_nn_cross_arrays_precomputed!(nndist::AbstractVector, + nni::AbstractVector, + nx::AbstractVector, ny::AbstractVector, + nz::AbstractVector, + E2p_scaled::AbstractVector, + beam_term::AbstractVector, + dijdist::AbstractVector, + i::Integer, N::Integer, + dij_factor, β = 1.0, γ = 1.0, R = 4.0) nndist_i = Inf nni_i = i - @inbounds for j in 1:(i-1) + @inbounds for j in 1:(i - 1) this_metric = valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, j) if this_metric < nndist_i nndist_i = this_metric @@ -220,7 +242,7 @@ Precomputed Valencia cross-update variant: updates neighbour data for slot end end end - @inbounds for j in (i+1):N + @inbounds for j in (i + 1):N this_metric = valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, j) if this_metric < nndist_i nndist_i = this_metric @@ -293,7 +315,8 @@ function _ee_genkt_algorithm_valencia(; particles::AbstractVector{EEJet}, beam_term[k] = E2pv[k] * sin2^γ end end - get_angular_nearest_neighbours_valencia_precomputed!(eereco, E2p_scaled, beam_term, p, γ, R) + get_angular_nearest_neighbours_valencia_precomputed!(eereco, E2p_scaled, beam_term, p, + γ, R) iter = 0 while N != 0 iter += 1 @@ -320,9 +343,9 @@ function _ee_genkt_algorithm_valencia(; particles::AbstractVector{EEJet}, push!(clusterseq.jets, merged_jet) newjet_k = length(clusterseq.jets) add_step_to_history!(clusterseq, - minmax(cluster_hist_index(jetA), - cluster_hist_index(jetB))..., - newjet_k, dij_min) + minmax(cluster_hist_index(jetA), + cluster_hist_index(jetB))..., + newjet_k, dij_min) indexv[ijetA] = newjet_k nni_v[ijetA] = 0 nndist_v[ijetA] = R2 diff --git a/src/EEJet.jl b/src/EEJet.jl index 50a19903..e62296ed 100644 --- a/src/EEJet.jl +++ b/src/EEJet.jl @@ -22,7 +22,7 @@ struct EEJet <: FourMomentum _cluster_hist_index::Int end - # Compatibility constructor was moved below the EERecoJet type definition +# Compatibility constructor was moved below the EERecoJet type definition """ EEJet(px::Real, py::Real, pz::Real, E::Real, cluster_hist_index::Int) @@ -111,4 +111,3 @@ mutable struct EERecoJet nz::Float64 E2p::Float64 end - diff --git a/test/test-valencia.jl b/test/test-valencia.jl index 10bbe09e..3503f3ac 100644 --- a/test/test-valencia.jl +++ b/test/test-valencia.jl @@ -103,57 +103,72 @@ end @test cs isa JetReconstruction.ClusterSequence end - - @testset "Valencia precomputed helpers (unit)" begin - # Construct a tiny set of EEJet with controlled directions and energies - # Ensure all jets have non-zero momentum to avoid undefined direction cosines - p1 = EEJet(0.0, 0.0, 1.0, 10.0; cluster_hist_index = 1) - p2 = EEJet(0.0, 0.1, 0.5, 5.0; cluster_hist_index = 2) - p3 = EEJet(0.1, 0.0, -1.0, 2.0; cluster_hist_index = 3) - parts = [p1, p2, p3] - N = length(parts) - R = 1.0 - pwr = 1.2 - γ = 1.0 - - # Prepare SoA like Valencia entrypoint - eereco = StructArray{EERecoJet}(undef, N) - fill_reco_array!(eereco, parts, R^2, pwr) - - nx = eereco.nx; ny = eereco.ny; nz = eereco.nz; E2p = eereco.E2p - invR2 = inv(R * R) - factor = 2 * invR2 - E2p_scaled = similar(E2p) - beam_term = similar(E2p) - for k in 1:N - E2p_scaled[k] = E2p[k] * factor - sin2 = 1.0 - nz[k] * nz[k] - beam_term[k] = E2p[k] * sin2^γ - end - - # valencia_distance_inv_scaled_arrays: compare to manual computation - i, j = 1, 2 - ang = 1.0 - nx[i]*nx[j] - ny[i]*ny[j] - nz[i]*nz[j] - @test JetReconstruction.valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, j) ≈ min(E2p_scaled[i], E2p_scaled[j]) * ang - - # beam distance arrays - @test JetReconstruction.valencia_beam_distance_arrays(E2p, nz, 1, γ, pwr) ≈ beam_term[1] - - # initializer: should populate nni/nndist/dijdist without error - JetReconstruction.get_angular_nearest_neighbours_valencia_precomputed!(eereco, E2p_scaled, beam_term, pwr, γ, R) - @test all(isfinite, eereco.nndist) - @test all(isfinite, eereco.dijdist) - @test all(i -> isa(i, Int), eereco.nni) - - # array-based no-cross and cross update helpers should execute and produce finite outputs - nnd = copy(eereco.nndist); nni = copy(eereco.nni); dijd = copy(eereco.dijdist) - JetReconstruction.update_nn_no_cross_arrays_precomputed!(nnd, nni, nx, ny, nz, E2p_scaled, beam_term, dijd, 2, N, 1.0, pwr, γ, R) - @test isfinite(nnd[2]) - @test isfinite(dijd[2]) - - # cross update (should update other slots if appropriate) — assert runs and outputs are finite - nnd2 = copy(nnd); nni2 = copy(nni); dijd2 = copy(dijd) - JetReconstruction.update_nn_cross_arrays_precomputed!(nnd2, nni2, nx, ny, nz, E2p_scaled, beam_term, dijd2, 2, N, 1.0, pwr, γ, R) - @test all(isfinite, nnd2) - @test all(isfinite, dijd2) +@testset "Valencia precomputed helpers (unit)" begin + # Construct a tiny set of EEJet with controlled directions and energies + # Ensure all jets have non-zero momentum to avoid undefined direction cosines + p1 = EEJet(0.0, 0.0, 1.0, 10.0; cluster_hist_index = 1) + p2 = EEJet(0.0, 0.1, 0.5, 5.0; cluster_hist_index = 2) + p3 = EEJet(0.1, 0.0, -1.0, 2.0; cluster_hist_index = 3) + parts = [p1, p2, p3] + N = length(parts) + R = 1.0 + pwr = 1.2 + γ = 1.0 + + # Prepare SoA like Valencia entrypoint + eereco = StructArray{EERecoJet}(undef, N) + fill_reco_array!(eereco, parts, R^2, pwr) + + nx = eereco.nx + ny = eereco.ny + nz = eereco.nz + E2p = eereco.E2p + invR2 = inv(R * R) + factor = 2 * invR2 + E2p_scaled = similar(E2p) + beam_term = similar(E2p) + for k in 1:N + E2p_scaled[k] = E2p[k] * factor + sin2 = 1.0 - nz[k] * nz[k] + beam_term[k] = E2p[k] * sin2^γ end + + # valencia_distance_inv_scaled_arrays: compare to manual computation + i, j = 1, 2 + ang = 1.0 - nx[i] * nx[j] - ny[i] * ny[j] - nz[i] * nz[j] + @test JetReconstruction.valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, + j) ≈ + min(E2p_scaled[i], E2p_scaled[j]) * ang + + # beam distance arrays + @test JetReconstruction.valencia_beam_distance_arrays(E2p, nz, 1, γ, pwr) ≈ beam_term[1] + + # initializer: should populate nni/nndist/dijdist without error + JetReconstruction.get_angular_nearest_neighbours_valencia_precomputed!(eereco, + E2p_scaled, + beam_term, pwr, + γ, R) + @test all(isfinite, eereco.nndist) + @test all(isfinite, eereco.dijdist) + @test all(i -> isa(i, Int), eereco.nni) + + # array-based no-cross and cross update helpers should execute and produce finite outputs + nnd = copy(eereco.nndist) + nni = copy(eereco.nni) + dijd = copy(eereco.dijdist) + JetReconstruction.update_nn_no_cross_arrays_precomputed!(nnd, nni, nx, ny, nz, + E2p_scaled, beam_term, dijd, 2, + N, 1.0, pwr, γ, R) + @test isfinite(nnd[2]) + @test isfinite(dijd[2]) + + # cross update (should update other slots if appropriate) — assert runs and outputs are finite + nnd2 = copy(nnd) + nni2 = copy(nni) + dijd2 = copy(dijd) + JetReconstruction.update_nn_cross_arrays_precomputed!(nnd2, nni2, nx, ny, nz, + E2p_scaled, beam_term, dijd2, 2, + N, 1.0, pwr, γ, R) + @test all(isfinite, nnd2) + @test all(isfinite, dijd2) +end From 8db5b4e464d31e7ae7d7f632a73409c8ee52cc90 Mon Sep 17 00:00:00 2001 From: Matt LeBlanc Date: Sun, 17 Aug 2025 22:19:50 -0400 Subject: [PATCH 19/32] Simplifying EEAlgorithm again --- src/EEAlgorithm.jl | 284 +++++++++++++++++++++------------------------ 1 file changed, 134 insertions(+), 150 deletions(-) diff --git a/src/EEAlgorithm.jl b/src/EEAlgorithm.jl index 910af4b5..74b37f4a 100644 --- a/src/EEAlgorithm.jl +++ b/src/EEAlgorithm.jl @@ -17,17 +17,9 @@ Calculate the angular distance between two jets `i` and `j` using the formula - `Float64`: The angular distance between `i` and `j`, which is ``1 - cos\theta``. """ -Base.@propagate_inbounds @inline function angular_distance(eereco, i, j) - if hasproperty(eereco, :nx) - nx = eereco.nx - ny = eereco.ny - nz = eereco.nz - @muladd 1.0 - nx[i] * nx[j] - ny[i] * ny[j] - nz[i] * nz[j] - else - # Fallback for Array-of-structs (AoS) - @muladd 1.0 - eereco[i].nx * eereco[j].nx - eereco[i].ny * eereco[j].ny - - eereco[i].nz * eereco[j].nz - end +@inline function angular_distance(eereco, i, j) + @inbounds @muladd 1.0 - eereco[i].nx * eereco[j].nx - eereco[i].ny * eereco[j].ny - + eereco[i].nz * eereco[j].nz end """ @@ -87,35 +79,36 @@ is specialised for the Durham metric and operates on the StructArray layout. """ @inline function get_angular_nearest_neighbours!(eereco, ::Val{JetAlgorithm.Durham}, dij_factor, p, γ = 1.0, R = 4.0) + # Get the initial nearest neighbours for each jet N = length(eereco) - nx = eereco.nx - ny = eereco.ny - nz = eereco.nz - nndist = eereco.nndist - nni = eereco.nni + # this_dist_vector = Vector{Float64}(undef, N) + # Nearest neighbour geometric distance @inbounds for i in 1:N - local_nndist_i = large_distance - local_nni_i = i - @inbounds for j in 1:N - if j != i - this_metric = angular_distance_arrays(nx, ny, nz, i, j) - better_nndist_i = this_metric < local_nndist_i - local_nndist_i = better_nndist_i ? this_metric : local_nndist_i - local_nni_i = better_nndist_i ? j : local_nni_i - better_nndist_j = this_metric < nndist[j] - nndist[j] = better_nndist_j ? this_metric : nndist[j] - nni[j] = better_nndist_j ? i : nni[j] - end + # TODO: Replace the 'j' loop with a vectorised operation over the appropriate array elements? + # this_dist_vector .= 1.0 .- eereco.nx[i:N] .* eereco[i + 1:end].nx .- + # eereco[i].ny .* eereco[i + 1:end].ny .- eereco[i].nz .* eereco[i + 1:end].nz + # The problem here will be avoiding allocations for the array outputs, which would easily + # kill performance + @inbounds for j in (i + 1):N + this_nndist = angular_distance(eereco, i, j) + + # Using these ternary operators is faster than the if-else block + better_nndist_i = this_nndist < eereco[i].nndist + eereco.nndist[i] = better_nndist_i ? this_nndist : eereco.nndist[i] + eereco.nni[i] = better_nndist_i ? j : eereco.nni[i] + better_nndist_j = this_nndist < eereco[j].nndist + eereco.nndist[j] = better_nndist_j ? this_nndist : eereco.nndist[j] + eereco.nni[j] = better_nndist_j ? i : eereco.nni[j] end - nndist[i] = local_nndist_i - nni[i] = local_nni_i end - @inbounds for i in 1:N - eereco.dijdist[i] = dij_dist(eereco, i, nni[i], dij_factor, + # Nearest neighbour dij distance + for i in 1:N + eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, Val(JetAlgorithm.Durham), R) end end + """ get_angular_nearest_neighbours!(eereco, ::Val{JetAlgorithm.EEKt}, dij_factor, p, γ=1.0, R=4.0) @@ -125,26 +118,41 @@ checks appropriate for EEKt. """ @inline function get_angular_nearest_neighbours!(eereco, ::Val{JetAlgorithm.EEKt}, dij_factor, p, γ = 1.0, R = 4.0) + # Get the initial nearest neighbours for each jet N = length(eereco) + # this_dist_vector = Vector{Float64}(undef, N) + # Nearest neighbour geometric distance @inbounds for i in 1:N + # TODO: Replace the 'j' loop with a vectorised operation over the appropriate array elements? + # this_dist_vector .= 1.0 .- eereco.nx[i:N] .* eereco[i + 1:end].nx .- + # eereco[i].ny .* eereco[i + 1:end].ny .- eereco[i].nz .* eereco[i + 1:end].nz + # The problem here will be avoiding allocations for the array outputs, which would easily + # kill performance @inbounds for j in (i + 1):N - this_metric = angular_distance(eereco, i, j) - better_nndist_i = this_metric < eereco[i].nndist - eereco.nndist[i] = better_nndist_i ? this_metric : eereco.nndist[i] + this_nndist = angular_distance(eereco, i, j) + + # Using these ternary operators is faster than the if-else block + better_nndist_i = this_nndist < eereco[i].nndist + eereco.nndist[i] = better_nndist_i ? this_nndist : eereco.nndist[i] eereco.nni[i] = better_nndist_i ? j : eereco.nni[i] - better_nndist_j = this_metric < eereco[j].nndist - eereco.nndist[j] = better_nndist_j ? this_metric : eereco.nndist[j] + better_nndist_j = this_nndist < eereco[j].nndist + eereco.nndist[j] = better_nndist_j ? this_nndist : eereco.nndist[j] eereco.nni[j] = better_nndist_j ? i : eereco.nni[j] end end - @inbounds for i in 1:N + # Nearest neighbour dij distance + for i in 1:N eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, Val(JetAlgorithm.EEKt), R) end - @inbounds for i in 1:N - beam_closer = eereco[i].E2p < eereco[i].dijdist - eereco.dijdist[i] = beam_closer ? eereco[i].E2p : eereco.dijdist[i] - eereco.nni[i] = beam_closer ? 0 : eereco.nni[i] + # For the EEKt algorithm, we need to check the beam distance as well + # (This is structured to only check for EEKt once) + if algorithm == JetAlgorithm.EEKt + @inbounds for i in 1:N + beam_closer = eereco[i].E2p < eereco[i].dijdist + eereco.dijdist[i] = beam_closer ? eereco[i].E2p : eereco.dijdist[i] + eereco.nni[i] = beam_closer ? 0 : eereco.nni[i] + end end end @@ -152,8 +160,7 @@ end get_angular_nearest_neighbours!(eereco, algorithm::JetAlgorithm.Algorithm, dij_factor, p, γ=1.0, R=4.0) Forwarding wrapper that dispatches to the `Val{...}` specialised -`get_angular_nearest_neighbours!` implementation to avoid branches in tight -loops. +`get_angular_nearest_neighbours!` implementation to avoid branches. """ @inline function get_angular_nearest_neighbours!(eereco, algorithm::JetAlgorithm.Algorithm, @@ -184,31 +191,17 @@ direction-cosine arrays for performance. """ @inline function update_nn_no_cross!(eereco, i, N, ::Val{JetAlgorithm.Durham}, dij_factor, _β = 1.0, _γ = 1.0, R = 4.0) - nndist = eereco.nndist - nni = eereco.nni - nx = eereco.nx - ny = eereco.ny - nz = eereco.nz - nndist[i] = large_distance - nni[i] = i - @inbounds for j in 1:(i - 1) - this_metric = angular_distance_arrays(nx, ny, nz, i, j) - better_nndist_i = this_metric < nndist[i] - nndist[i] = better_nndist_i ? this_metric : nndist[i] - nni[i] = better_nndist_i ? j : nni[i] - end - @inbounds for j in (i + 1):N - this_metric = angular_distance_arrays(nx, ny, nz, i, j) - better_nndist_i = this_metric < nndist[i] - nndist[i] = better_nndist_i ? this_metric : nndist[i] - nni[i] = better_nndist_i ? j : nni[i] + eereco.nndist[i] = large_distance + eereco.nni[i] = i + @inbounds for j in 1:N + if j != i + this_nndist = angular_distance(eereco, i, j) + better_nndist_i = this_nndist < eereco[i].nndist + eereco.nndist[i] = better_nndist_i ? this_nndist : eereco.nndist[i] + eereco.nni[i] = better_nndist_i ? j : eereco.nni[i] + end end - # Inline Durham dij computation to avoid function-call overhead - E2p = eereco.E2p - E2p_i = E2p[i] - E2p_nni = E2p[nni[i]] - minE2p = E2p_i < E2p_nni ? E2p_i : E2p_nni - eereco.dijdist[i] = minE2p * dij_factor * nndist[i] + eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor) end """ @@ -220,33 +213,35 @@ beam is closer than the dij distance. """ @inline function update_nn_no_cross!(eereco, i, N, ::Val{JetAlgorithm.EEKt}, dij_factor, _β = 1.0, _γ = 1.0, R = 4.0) - nndist = eereco.nndist - nni = eereco.nni - nx = eereco.nx - ny = eereco.ny - nz = eereco.nz - E2p = eereco.E2p - E2p_i = E2p[i] - nndist[i] = large_distance - nni[i] = i - @inbounds for j in 1:(i - 1) - this_metric = angular_distance_arrays(nx, ny, nz, i, j) - better_nndist_i = this_metric < nndist[i] - nndist[i] = better_nndist_i ? this_metric : nndist[i] - nni[i] = better_nndist_i ? j : nni[i] - end - @inbounds for j in (i + 1):N - this_metric = angular_distance_arrays(nx, ny, nz, i, j) - better_nndist_i = this_metric < nndist[i] - nndist[i] = better_nndist_i ? this_metric : nndist[i] - nni[i] = better_nndist_i ? j : nni[i] + # Update the nearest neighbour for jet i, w.r.t. all other active jets + # also doing the cross check for the other jet + eereco.nndist[i] = large_distance + eereco.nni[i] = i + @inbounds for j in 1:N + if j != i + this_nndist = angular_distance(eereco, i, j) + better_nndist_i = this_nndist < eereco[i].nndist + eereco.nndist[i] = better_nndist_i ? this_nndist : eereco.nndist[i] + eereco.nni[i] = better_nndist_i ? j : eereco.nni[i] + if this_nndist < eereco[j].nndist + eereco.nndist[j] = this_nndist + eereco.nni[j] = i + # j will not be revisited, so update metric distance here + eereco.dijdist[j] = dij_dist(eereco, j, i, dij_factor) + if algorithm == JetAlgorithm.EEKt + if eereco[j].E2p < eereco[j].dijdist + eereco.dijdist[j] = eereco[j].E2p + eereco.nni[j] = 0 + end + end + end + end end - # Inline EEKt dij computation and beam check - E2p_nni = E2p[nni[i]] - minE2p = E2p_i < E2p_nni ? E2p_i : E2p_nni - eereco.dijdist[i] = minE2p * dij_factor * nndist[i] - beam_close = E2p_i < eereco.dijdist[i] - eereco.dijdist[i] = beam_close ? E2p_i : eereco.dijdist[i] + eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor) + + # Need to check beam for EEKt + beam_close = eereco[i].E2p < eereco[i].dijdist + eereco.dijdist[i] = beam_close ? eereco[i].E2p : eereco.dijdist[i] eereco.nni[i] = beam_close ? 0 : eereco.nni[i] end @@ -271,35 +266,31 @@ nearest neighbour. Computes inline dij updates to avoid function-call overhead. """ @inline function update_nn_cross!(eereco, i, N, ::Val{JetAlgorithm.Durham}, dij_factor, _β = 1.0, _γ = 1.0, R = 4.0) - nndist = eereco.nndist - nni = eereco.nni - nx = eereco.nx - ny = eereco.ny - nz = eereco.nz - nndist[i] = large_distance - nni[i] = i - E2p = eereco.E2p - E2p_i = E2p[i] + # Update the nearest neighbour for jet i, w.r.t. all other active jets + # also doing the cross check for the other jet + eereco.nndist[i] = large_distance + eereco.nni[i] = i @inbounds for j in 1:N if j != i - this_metric = angular_distance_arrays(nx, ny, nz, i, j) - better_nndist_i = this_metric < nndist[i] - nndist[i] = better_nndist_i ? this_metric : nndist[i] - nni[i] = better_nndist_i ? j : nni[i] - if this_metric < nndist[j] - nndist[j] = this_metric - nni[j] = i - # Hoist E2p_j and compute new dij once - E2p_j = E2p[j] - new_dij = (E2p_i < E2p_j ? E2p_i : E2p_j) * dij_factor * this_metric - eereco.dijdist[j] = new_dij + this_nndist = angular_distance(eereco, i, j) + better_nndist_i = this_nndist < eereco[i].nndist + eereco.nndist[i] = better_nndist_i ? this_nndist : eereco.nndist[i] + eereco.nni[i] = better_nndist_i ? j : eereco.nni[i] + if this_nndist < eereco[j].nndist + eereco.nndist[j] = this_nndist + eereco.nni[j] = i + # j will not be revisited, so update metric distance here + eereco.dijdist[j] = dij_dist(eereco, j, i, dij_factor) + if algorithm == JetAlgorithm.EEKt + if eereco[j].E2p < eereco[j].dijdist + eereco.dijdist[j] = eereco[j].E2p + eereco.nni[j] = 0 + end + end end end end - # Inline dij for i - E2p_nni = E2p[nni[i]] - minE2p_i = E2p_i < E2p_nni ? E2p_i : E2p_nni - eereco.dijdist[i] = minE2p_i * dij_factor * nndist[i] + eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor) end """ @@ -311,43 +302,36 @@ where required. """ @inline function update_nn_cross!(eereco, i, N, ::Val{JetAlgorithm.EEKt}, dij_factor, _β = 1.0, _γ = 1.0, R = 4.0) - nndist = eereco.nndist - nni = eereco.nni - nx = eereco.nx - ny = eereco.ny - nz = eereco.nz - E2p = eereco.E2p - E2p_i = E2p[i] - nndist[i] = large_distance - nni[i] = i + # Update the nearest neighbour for jet i, w.r.t. all other active jets + # also doing the cross check for the other jet + eereco.nndist[i] = large_distance + eereco.nni[i] = i @inbounds for j in 1:N if j != i - this_metric = angular_distance_arrays(nx, ny, nz, i, j) - better_nndist_i = this_metric < nndist[i] - nndist[i] = better_nndist_i ? this_metric : nndist[i] - nni[i] = better_nndist_i ? j : nni[i] - if this_metric < nndist[j] - nndist[j] = this_metric - nni[j] = i - # Hoist E2p_j and compute new dij once, then beam-check - E2p_j = E2p[j] - new_dij = (E2p_i < E2p_j ? E2p_i : E2p_j) * dij_factor * this_metric - if E2p_j < new_dij - eereco.dijdist[j] = E2p_j - nni[j] = 0 - else - eereco.dijdist[j] = new_dij + this_nndist = angular_distance(eereco, i, j) + better_nndist_i = this_nndist < eereco[i].nndist + eereco.nndist[i] = better_nndist_i ? this_nndist : eereco.nndist[i] + eereco.nni[i] = better_nndist_i ? j : eereco.nni[i] + if this_nndist < eereco[j].nndist + eereco.nndist[j] = this_nndist + eereco.nni[j] = i + # j will not be revisited, so update metric distance here + eereco.dijdist[j] = dij_dist(eereco, j, i, dij_factor) + if algorithm == JetAlgorithm.EEKt + if eereco[j].E2p < eereco[j].dijdist + eereco.dijdist[j] = eereco[j].E2p + eereco.nni[j] = 0 + end end end end end - # Inline dij for i and beam check - E2p_nni = E2p[nni[i]] - minE2p_i = E2p_i < E2p_nni ? E2p_i : E2p_nni - eereco.dijdist[i] = minE2p_i * dij_factor * nndist[i] - beam_close = E2p_i < eereco.dijdist[i] - eereco.dijdist[i] = beam_close ? E2p_i : eereco.dijdist[i] - nni[i] = beam_close ? 0 : nni[i] + eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor) + + # Check beam for EEKt + beam_close = eereco[i].E2p < eereco[i].dijdist + eereco.dijdist[i] = beam_close ? eereco[i].E2p : eereco.dijdist[i] + eereco.nni[i] = beam_close ? 0 : eereco.nni[i] end """ @@ -371,7 +355,7 @@ function ee_check_consistency(clusterseq, eereco, N) end end end - @debug "Consistency check passed" + @debug "Consistency check passed at $msg" end ################################################################################ From 9cd925fe0b9521a6fae07b4a14cbbe48504d3374 Mon Sep 17 00:00:00 2001 From: Matt LeBlanc Date: Mon, 18 Aug 2025 08:32:49 -0400 Subject: [PATCH 20/32] Cleanup --- src/EEAlgorithm.jl | 51 ++++++++++++++------------------------ src/EEAlgorithmValencia.jl | 38 ++++++++++------------------ test/test-valencia.jl | 14 ++++++++--- 3 files changed, 41 insertions(+), 62 deletions(-) diff --git a/src/EEAlgorithm.jl b/src/EEAlgorithm.jl index 74b37f4a..c9b68a13 100644 --- a/src/EEAlgorithm.jl +++ b/src/EEAlgorithm.jl @@ -145,14 +145,11 @@ checks appropriate for EEKt. eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, Val(JetAlgorithm.EEKt), R) end - # For the EEKt algorithm, we need to check the beam distance as well - # (This is structured to only check for EEKt once) - if algorithm == JetAlgorithm.EEKt - @inbounds for i in 1:N - beam_closer = eereco[i].E2p < eereco[i].dijdist - eereco.dijdist[i] = beam_closer ? eereco[i].E2p : eereco.dijdist[i] - eereco.nni[i] = beam_closer ? 0 : eereco.nni[i] - end + # Beam-distance check for EEKt (this variant is EEKt-specific) + @inbounds for i in 1:N + beam_closer = eereco[i].E2p < eereco[i].dijdist + eereco.dijdist[i] = beam_closer ? eereco[i].E2p : eereco.dijdist[i] + eereco.nni[i] = beam_closer ? 0 : eereco.nni[i] end end @@ -201,7 +198,7 @@ direction-cosine arrays for performance. eereco.nni[i] = better_nndist_i ? j : eereco.nni[i] end end - eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor) + eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, Val(JetAlgorithm.Durham), R) end """ @@ -227,17 +224,12 @@ beam is closer than the dij distance. eereco.nndist[j] = this_nndist eereco.nni[j] = i # j will not be revisited, so update metric distance here - eereco.dijdist[j] = dij_dist(eereco, j, i, dij_factor) - if algorithm == JetAlgorithm.EEKt - if eereco[j].E2p < eereco[j].dijdist - eereco.dijdist[j] = eereco[j].E2p - eereco.nni[j] = 0 - end - end + eereco.dijdist[j] = dij_dist(eereco, j, i, dij_factor, Val(JetAlgorithm.EEKt), R) + # EEKt-specific beam check is handled in the EEKt method only end end end - eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor) + eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, Val(JetAlgorithm.EEKt), R) # Need to check beam for EEKt beam_close = eereco[i].E2p < eereco[i].dijdist @@ -280,17 +272,12 @@ nearest neighbour. Computes inline dij updates to avoid function-call overhead. eereco.nndist[j] = this_nndist eereco.nni[j] = i # j will not be revisited, so update metric distance here - eereco.dijdist[j] = dij_dist(eereco, j, i, dij_factor) - if algorithm == JetAlgorithm.EEKt - if eereco[j].E2p < eereco[j].dijdist - eereco.dijdist[j] = eereco[j].E2p - eereco.nni[j] = 0 - end - end + eereco.dijdist[j] = dij_dist(eereco, j, i, dij_factor, Val(JetAlgorithm.Durham), R) + # EEKt-specific beam check is handled in the EEKt method only end end end - eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor) + eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, Val(JetAlgorithm.Durham), R) end """ @@ -316,17 +303,15 @@ where required. eereco.nndist[j] = this_nndist eereco.nni[j] = i # j will not be revisited, so update metric distance here - eereco.dijdist[j] = dij_dist(eereco, j, i, dij_factor) - if algorithm == JetAlgorithm.EEKt - if eereco[j].E2p < eereco[j].dijdist - eereco.dijdist[j] = eereco[j].E2p - eereco.nni[j] = 0 - end + eereco.dijdist[j] = dij_dist(eereco, j, i, dij_factor, Val(JetAlgorithm.EEKt), R) + if eereco[j].E2p < eereco[j].dijdist + eereco.dijdist[j] = eereco[j].E2p + eereco.nni[j] = 0 end end end end - eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor) + eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, Val(JetAlgorithm.EEKt), R) # Check beam for EEKt beam_close = eereco[i].E2p < eereco[i].dijdist @@ -355,7 +340,7 @@ function ee_check_consistency(clusterseq, eereco, N) end end end - @debug "Consistency check passed at $msg" + @debug "Consistency check passed" end ################################################################################ diff --git a/src/EEAlgorithmValencia.jl b/src/EEAlgorithmValencia.jl index 606fcd8f..a06d92d1 100644 --- a/src/EEAlgorithmValencia.jl +++ b/src/EEAlgorithmValencia.jl @@ -9,20 +9,14 @@ Calculate the Valencia dij metric (scaled by `invR2`) between slots `i` and `j` in a StructArray `eereco`. Uses E2p and direction cosines from `eereco`. """ Base.@propagate_inbounds @inline function valencia_distance_inv(eereco, i, j, invR2) - if hasproperty(eereco, :nx) - nx = eereco.nx - ny = eereco.ny - nz = eereco.nz - E2p = eereco.E2p - angular_dist = angular_distance_arrays(nx, ny, nz, i, j) - # Valencia dij : min(E_i^{2\u03b2}, E_j^{2\u03b2}) * 2 * (1 - cos \u03b8) * invR2 - min(E2p[i], E2p[j]) * 2 * angular_dist * invR2 - else - # Fallback for Array-of-structs - angular_dist = 1.0 - eereco[i].nx * eereco[j].nx - eereco[i].ny * eereco[j].ny - - eereco[i].nz * eereco[j].nz - min(eereco[i].E2p, eereco[j].E2p) * 2 * angular_dist * invR2 - end + # Assume SoA layout + nx = eereco.nx + ny = eereco.ny + nz = eereco.nz + E2p = eereco.E2p + angular_dist = angular_distance_arrays(nx, ny, nz, i, j) + # Valencia dij : min(E_i^{2β}, E_j^{2β}) * 2 * (1 - cos θ) * invR2 + min(E2p[i], E2p[j]) * 2 * angular_dist * invR2 end """ @@ -81,17 +75,11 @@ Compute the Valencia beam-distance term for slot `i` given angular exponent `γ` and (unused) `β`. Uses direction cosine `nz` and E2p value. """ Base.@propagate_inbounds @inline function valencia_beam_distance(eereco, i, γ, β) - if hasproperty(eereco, :nz) - nzv = eereco.nz - E2pv = eereco.E2p - nz_i = nzv[i] - sin2 = 1 - nz_i * nz_i - E2p = E2pv[i] - else - nz_i = eereco[i].nz - sin2 = 1 - nz_i * nz_i - E2p = eereco[i].E2p - end + nzv = eereco.nz + E2pv = eereco.E2p + nz_i = nzv[i] + sin2 = 1 - nz_i * nz_i + E2p = E2pv[i] if γ == 1.0 return E2p * sin2 elseif γ == 2.0 diff --git a/test/test-valencia.jl b/test/test-valencia.jl index 3503f3ac..699b3251 100644 --- a/test/test-valencia.jl +++ b/test/test-valencia.jl @@ -68,8 +68,11 @@ run_reco_test(valencia_exclusive_d500_b1g1) # Test dij_dist for Valencia algorithm @testset "dij_dist Valencia" begin # Minimal eereco with two reco jets; only nx,ny,nz,E2p are used by valencia_distance - eereco = EERecoJet[EERecoJet(1, 0, Inf, Inf, 1.0, 0.0, 0.0, 1.0), - EERecoJet(2, 0, Inf, Inf, 0.0, 1.0, 0.0, 2.0)] + eereco = StructArray{EERecoJet}(undef, 2) + eereco.nx .= [1.0, 0.0] + eereco.ny .= [0.0, 1.0] + eereco.nz .= [0.0, 0.0] + eereco.E2p .= [1.0, 2.0] dij = dij_dist(eereco, 1, 2, 1.0, JetAlgorithm.Valencia, 0.8) @test dij ≈ valencia_distance(eereco, 1, 2, 0.8) end @@ -77,8 +80,11 @@ end # Valencia distance wrapper coverage @testset "Valencia distance wrappers" begin # Minimal eereco with two reco jets and identical directions so angle=0 - eereco = EERecoJet[EERecoJet(1, 0, Inf, Inf, 1.0, 0.0, 0.0, 9.0), - EERecoJet(2, 0, Inf, Inf, 1.0, 0.0, 0.0, 4.0)] + eereco = StructArray{EERecoJet}(undef, 2) + eereco.nx .= [1.0, 1.0] + eereco.ny .= [0.0, 0.0] + eereco.nz .= [0.0, 0.0] + eereco.E2p .= [9.0, 4.0] R = 2.0 invR2 = inv(R * R) @test valencia_distance_inv(eereco, 1, 2, invR2) == 0.0 From a0810d2d74e969ba2802635f14fbf04791c6ac1e Mon Sep 17 00:00:00 2001 From: Matt LeBlanc Date: Mon, 18 Aug 2025 09:31:25 -0400 Subject: [PATCH 21/32] Cleanup --- src/EEAlgorithm.jl | 203 ++----------------------------------- src/EEAlgorithmValencia.jl | 19 +++- 2 files changed, 26 insertions(+), 196 deletions(-) diff --git a/src/EEAlgorithm.jl b/src/EEAlgorithm.jl index c9b68a13..79cb1c0f 100644 --- a/src/EEAlgorithm.jl +++ b/src/EEAlgorithm.jl @@ -22,19 +22,6 @@ Calculate the angular distance between two jets `i` and `j` using the formula eereco[i].nz * eereco[j].nz end -""" - angular_distance_arrays(nx, ny, nz, i, j) -> Float64 - -Compute the angular distance (1 - cos θ) between entries `i` and `j` using -direction-cosine arrays `nx`, `ny`, `nz`. - -# Returns -- `Float64`: angular distance. -""" -Base.@propagate_inbounds @inline function angular_distance_arrays(nx, ny, nz, i, j) - @muladd 1.0 - nx[i] * nx[j] - ny[i] * ny[j] - nz[i] * nz[j] -end - """ dij_dist(eereco, i, j, dij_factor, algorithm::JetAlgorithm.Algorithm, R = 4.0) @@ -56,11 +43,13 @@ min(E_i^{2p}, E_j^{2p}) * dij_factor * (angular NN metric stored in nndist). For EEKt, dij_factor encodes the R-dependent normalization. """ @inline function dij_dist(eereco, i, j, dij_factor, ::Val{JetAlgorithm.Durham}, R = 4.0) + # Calculate the dij distance for jet i from jet j j == 0 && return large_dij @inbounds min(eereco[i].E2p, eereco[j].E2p) * dij_factor * eereco[i].nndist end @inline function dij_dist(eereco, i, j, dij_factor, ::Val{JetAlgorithm.EEKt}, R = 4.0) + # Calculate the dij distance for jet i from jet j j == 0 && return large_dij @inbounds min(eereco[i].E2p, eereco[j].E2p) * dij_factor * eereco[i].nndist end @@ -169,8 +158,7 @@ end update_nn_no_cross!(eereco, i, N, algorithm::JetAlgorithm.Algorithm, dij_factor, β=1.0, γ=1.0, R=4.0) Update nearest-neighbour information for slot `i` (no-cross variant) by -forwarding to algorithm-specific `Val{...}` specialisations. This wrapper -avoids runtime branching in the hot inner loop. +forwarding to algorithm-specific `Val{...}` specialisations. """ @inline function update_nn_no_cross!(eereco, i, N, algorithm::JetAlgorithm.Algorithm, dij_factor, β = 1.0, γ = 1.0, R = 4.0) @@ -343,173 +331,6 @@ function ee_check_consistency(clusterseq, eereco, N) @debug "Consistency check passed" end -################################################################################ -# Array-based nearest-neighbour update helpers (operate on raw vectors) -################################################################################ - -@inline function update_nn_no_cross_arrays!(nndist::AbstractVector, nni::AbstractVector, - nx::AbstractVector, ny::AbstractVector, - nz::AbstractVector, - E2p::AbstractVector, dijdist::AbstractVector, - i::Integer, N::Integer, - algorithm::JetAlgorithm.Algorithm, - dij_factor, β = 1.0, γ = 1.0, R = 4.0) - return update_nn_no_cross_arrays!(nndist, nni, nx, ny, nz, E2p, dijdist, i, N, - Val(algorithm), dij_factor, β, γ, R) -end - -@inline function update_nn_no_cross_arrays!(nndist::AbstractVector, nni::AbstractVector, - nx::AbstractVector, ny::AbstractVector, - nz::AbstractVector, - E2p::AbstractVector, dijdist::AbstractVector, - i::Integer, N::Integer, - ::Val{JetAlgorithm.Durham}, - dij_factor, _β = 1.0, _γ = 1.0, R = 4.0) - nndist[i] = large_distance - nni[i] = i - @inbounds for j in 1:N - if j != i - this_metric = angular_distance_arrays(nx, ny, nz, i, j) - better_nndist_i = this_metric < nndist[i] - nndist[i] = better_nndist_i ? this_metric : nndist[i] - nni[i] = better_nndist_i ? j : nni[i] - end - end - # Inline Durham dij computation - E2p_i = E2p[i] - E2p_nni = E2p[nni[i]] - minE2p = E2p_i < E2p_nni ? E2p_i : E2p_nni - dijdist[i] = minE2p * dij_factor * nndist[i] -end - -@inline function update_nn_no_cross_arrays!(nndist::AbstractVector, nni::AbstractVector, - nx::AbstractVector, ny::AbstractVector, - nz::AbstractVector, - E2p::AbstractVector, dijdist::AbstractVector, - i::Integer, N::Integer, - ::Val{JetAlgorithm.EEKt}, - dij_factor, _β = 1.0, _γ = 1.0, R = 4.0) - nndist[i] = large_distance - nni[i] = i - E2p_i = E2p[i] - @inbounds for j in 1:N - if j != i - this_metric = angular_distance_arrays(nx, ny, nz, i, j) - better_nndist_i = this_metric < nndist[i] - nndist[i] = better_nndist_i ? this_metric : nndist[i] - nni[i] = better_nndist_i ? j : nni[i] - end - end - E2p_nni = E2p[nni[i]] - minE2p = E2p_i < E2p_nni ? E2p_i : E2p_nni - dijdist[i] = minE2p * dij_factor * nndist[i] - beam_close = E2p_i < dijdist[i] - dijdist[i] = beam_close ? E2p_i : dijdist[i] - nni[i] = beam_close ? 0 : nni[i] -end - -@inline function update_nn_cross_arrays!(nndist::AbstractVector, nni::AbstractVector, - nx::AbstractVector, ny::AbstractVector, - nz::AbstractVector, - E2p::AbstractVector, dijdist::AbstractVector, - i::Integer, N::Integer, - algorithm::JetAlgorithm.Algorithm, - dij_factor, β = 1.0, γ = 1.0, R = 4.0) - return update_nn_cross_arrays!(nndist, nni, nx, ny, nz, E2p, dijdist, i, N, - Val(algorithm), dij_factor, β, γ, R) -end - -@inline function update_nn_cross_arrays!(nndist::AbstractVector, nni::AbstractVector, - nx::AbstractVector, ny::AbstractVector, - nz::AbstractVector, - E2p::AbstractVector, dijdist::AbstractVector, - i::Integer, N::Integer, ::Val{JetAlgorithm.Durham}, - dij_factor, _β = 1.0, _γ = 1.0, R = 4.0) - nndist[i] = large_distance - nni[i] = i - E2p_i = E2p[i] - @inbounds for j in 1:(i - 1) - this_metric = angular_distance_arrays(nx, ny, nz, i, j) - better_nndist_i = this_metric < nndist[i] - nndist[i] = better_nndist_i ? this_metric : nndist[i] - nni[i] = better_nndist_i ? j : nni[i] - if this_metric < nndist[j] - nndist[j] = this_metric - nni[j] = i - E2p_j = E2p[j] - dijdist[j] = (E2p_i < E2p_j ? E2p_i : E2p_j) * dij_factor * this_metric - end - end - @inbounds for j in (i + 1):N - this_metric = angular_distance_arrays(nx, ny, nz, i, j) - better_nndist_i = this_metric < nndist[i] - nndist[i] = better_nndist_i ? this_metric : nndist[i] - nni[i] = better_nndist_i ? j : nni[i] - if this_metric < nndist[j] - nndist[j] = this_metric - nni[j] = i - E2p_j = E2p[j] - minE2p = E2p_i < E2p_j ? E2p_i : E2p_j - dijdist[j] = minE2p * dij_factor * this_metric - end - end - E2p_nni = E2p[nni[i]] - minE2p_i = E2p_i < E2p_nni ? E2p_i : E2p_nni - dijdist[i] = minE2p_i * dij_factor * nndist[i] -end - -@inline function update_nn_cross_arrays!(nndist::AbstractVector, nni::AbstractVector, - nx::AbstractVector, ny::AbstractVector, - nz::AbstractVector, - E2p::AbstractVector, dijdist::AbstractVector, - i::Integer, N::Integer, ::Val{JetAlgorithm.EEKt}, - dij_factor, _β = 1.0, _γ = 1.0, R = 4.0) - E2p_i = E2p[i] - nndist[i] = large_distance - nni[i] = i - @inbounds for j in 1:(i - 1) - this_metric = angular_distance_arrays(nx, ny, nz, i, j) - better_nndist_i = this_metric < nndist[i] - nndist[i] = better_nndist_i ? this_metric : nndist[i] - nni[i] = better_nndist_i ? j : nni[i] - if this_metric < nndist[j] - nndist[j] = this_metric - nni[j] = i - E2p_j = E2p[j] - new_dij = (E2p_i < E2p_j ? E2p_i : E2p_j) * dij_factor * this_metric - if E2p_j < new_dij - dijdist[j] = E2p_j - nni[j] = 0 - else - dijdist[j] = new_dij - end - end - end - @inbounds for j in (i + 1):N - this_metric = angular_distance_arrays(nx, ny, nz, i, j) - better_nndist_i = this_metric < nndist[i] - nndist[i] = better_nndist_i ? this_metric : nndist[i] - nni[i] = better_nndist_i ? j : nni[i] - if this_metric < nndist[j] - nndist[j] = this_metric - nni[j] = i - E2p_j = E2p[j] - minE2p = E2p_i < E2p_j ? E2p_i : E2p_j - dijdist[j] = minE2p * dij_factor * this_metric - if E2p_j < dijdist[j] - dijdist[j] = E2p_j - nni[j] = 0 - end - end - end - E2p_nni = E2p[nni[i]] - minE2p_i = E2p_i < E2p_nni ? E2p_i : E2p_nni - dijdist[i] = minE2p_i * dij_factor * nndist[i] - beam_close = E2p_i < dijdist[i] - dijdist[i] = beam_close ? E2p_i : dijdist[i] - nni[i] = beam_close ? 0 : nni[i] -end - Base.@propagate_inbounds @inline function fill_reco_array!(eereco, particles, R2, p) @inbounds for i in eachindex(particles) eereco.index[i] = i @@ -680,10 +501,7 @@ function ee_genkt_algorithm(particles::AbstractVector{T}; algorithm::JetAlgorith end end - # Now call the actual reconstruction method, tuned for our internal EDM - # Dispatch once on the algorithm to call a small number of specialised - # internal implementations. This avoids per-iteration branching/Val() in - # the hot inner loops. + # Now call the actual reconstruction method. if algorithm == JetAlgorithm.Valencia return _ee_genkt_algorithm_valencia(particles = recombination_particles, p = p, R = R, @@ -701,7 +519,7 @@ function ee_genkt_algorithm(particles::AbstractVector{T}; algorithm::JetAlgorith end ################################################################################ -# Durham-specialised implementation (optimized inner loops using array helpers) +# Durham-specialised implementation ################################################################################ function _ee_genkt_algorithm_durham(; particles::AbstractVector{EEJet}, algorithm::JetAlgorithm.Algorithm, p::Real, R = 4.0, @@ -779,21 +597,19 @@ function _ee_genkt_algorithm_durham(; particles::AbstractVector{EEJet}, N -= 1 # Update nearest neighbours step using array-based helpers specialised for Durham - for i in 1:N + for i in 1:N if (ijetB != N + 1) && (nni[i] == N + 1) nni[i] = ijetB else if (nni[i] == ijetA) || (nni[i] == ijetB) || (nni[i] > N) - update_nn_no_cross_arrays!(nndist, nni, nx, ny, nz, E2p, dijdist, - i, N, Val(JetAlgorithm.Durham), dij_factor, - p, γ, R) + # use SoA-based updater + update_nn_no_cross!(eereco, i, N, Val(JetAlgorithm.Durham), dij_factor, p, γ, R) end end end if ijetA != ijetB - update_nn_cross_arrays!(nndist, nni, nx, ny, nz, E2p, dijdist, - ijetA, N, Val(JetAlgorithm.Durham), dij_factor, p, γ, R) + update_nn_cross!(eereco, ijetA, N, Val(JetAlgorithm.Durham), dij_factor, p, γ, R) end end @@ -801,6 +617,7 @@ function _ee_genkt_algorithm_durham(; particles::AbstractVector{EEJet}, end ################################################################################ +# EEKt-specialised implementation ################################################################################ """ diff --git a/src/EEAlgorithmValencia.jl b/src/EEAlgorithmValencia.jl index a06d92d1..4390a26a 100644 --- a/src/EEAlgorithmValencia.jl +++ b/src/EEAlgorithmValencia.jl @@ -2,6 +2,20 @@ # Valencia-specialised helpers and implementation ################################################################################ + +""" + angular_distance_arrays(nx, ny, nz, i, j) -> Float64 + +Compute the angular distance (1 - cos θ) between entries `i` and `j` using +direction-cosine arrays `nx`, `ny`, `nz`. + +# Returns +- `Float64`: angular distance. +""" +Base.@propagate_inbounds @inline function angular_distance_arrays(nx, ny, nz, i, j) + @muladd 1.0 - nx[i] * nx[j] - ny[i] * ny[j] - nz[i] * nz[j] +end + """ valencia_distance_inv(eereco, i, j, invR2) -> Float64 @@ -58,8 +72,7 @@ end valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, j) -> Float64 Compute the Valencia distance using a pre-scaled E2p vector (`E2p_scaled`), -avoiding repeated multiplication by the R-dependent factor. Intended for -performance-sensitive precomputed loops. +avoiding repeated multiplication by the R-dependent factor. """ Base.@propagate_inbounds @inline function valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, @@ -113,7 +126,7 @@ end Initialize nearest-neighbour arrays for the Valencia algorithm using precomputed scaled energy vector `E2p_scaled` and `beam_term`. This avoids -repeated per-pair scaling inside tight loops. +repeated per-pair scaling inside loops. """ Base.@propagate_inbounds @inline function get_angular_nearest_neighbours_valencia_precomputed!(eereco, E2p_scaled::AbstractVector, From 4cd983e05ff15aee7ba99bd8735595ceccda13b0 Mon Sep 17 00:00:00 2001 From: Matt LeBlanc Date: Mon, 18 Aug 2025 09:35:48 -0400 Subject: [PATCH 22/32] Formatter --- src/EEAlgorithm.jl | 34 +++++++++++++++++++++------------- src/EEAlgorithmValencia.jl | 1 - 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/EEAlgorithm.jl b/src/EEAlgorithm.jl index 79cb1c0f..2e59b488 100644 --- a/src/EEAlgorithm.jl +++ b/src/EEAlgorithm.jl @@ -97,7 +97,6 @@ is specialised for the Durham metric and operates on the StructArray layout. end end - """ get_angular_nearest_neighbours!(eereco, ::Val{JetAlgorithm.EEKt}, dij_factor, p, γ=1.0, R=4.0) @@ -186,7 +185,8 @@ direction-cosine arrays for performance. eereco.nni[i] = better_nndist_i ? j : eereco.nni[i] end end - eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, Val(JetAlgorithm.Durham), R) + eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, + Val(JetAlgorithm.Durham), R) end """ @@ -212,12 +212,14 @@ beam is closer than the dij distance. eereco.nndist[j] = this_nndist eereco.nni[j] = i # j will not be revisited, so update metric distance here - eereco.dijdist[j] = dij_dist(eereco, j, i, dij_factor, Val(JetAlgorithm.EEKt), R) + eereco.dijdist[j] = dij_dist(eereco, j, i, dij_factor, + Val(JetAlgorithm.EEKt), R) # EEKt-specific beam check is handled in the EEKt method only end end end - eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, Val(JetAlgorithm.EEKt), R) + eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, + Val(JetAlgorithm.EEKt), R) # Need to check beam for EEKt beam_close = eereco[i].E2p < eereco[i].dijdist @@ -260,12 +262,14 @@ nearest neighbour. Computes inline dij updates to avoid function-call overhead. eereco.nndist[j] = this_nndist eereco.nni[j] = i # j will not be revisited, so update metric distance here - eereco.dijdist[j] = dij_dist(eereco, j, i, dij_factor, Val(JetAlgorithm.Durham), R) + eereco.dijdist[j] = dij_dist(eereco, j, i, dij_factor, + Val(JetAlgorithm.Durham), R) # EEKt-specific beam check is handled in the EEKt method only end end end - eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, Val(JetAlgorithm.Durham), R) + eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, + Val(JetAlgorithm.Durham), R) end """ @@ -291,7 +295,8 @@ where required. eereco.nndist[j] = this_nndist eereco.nni[j] = i # j will not be revisited, so update metric distance here - eereco.dijdist[j] = dij_dist(eereco, j, i, dij_factor, Val(JetAlgorithm.EEKt), R) + eereco.dijdist[j] = dij_dist(eereco, j, i, dij_factor, + Val(JetAlgorithm.EEKt), R) if eereco[j].E2p < eereco[j].dijdist eereco.dijdist[j] = eereco[j].E2p eereco.nni[j] = 0 @@ -299,8 +304,9 @@ where required. end end end - eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, Val(JetAlgorithm.EEKt), R) - + eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, + Val(JetAlgorithm.EEKt), R) + # Check beam for EEKt beam_close = eereco[i].E2p < eereco[i].dijdist eereco.dijdist[i] = beam_close ? eereco[i].E2p : eereco.dijdist[i] @@ -597,19 +603,21 @@ function _ee_genkt_algorithm_durham(; particles::AbstractVector{EEJet}, N -= 1 # Update nearest neighbours step using array-based helpers specialised for Durham - for i in 1:N + for i in 1:N if (ijetB != N + 1) && (nni[i] == N + 1) nni[i] = ijetB else if (nni[i] == ijetA) || (nni[i] == ijetB) || (nni[i] > N) - # use SoA-based updater - update_nn_no_cross!(eereco, i, N, Val(JetAlgorithm.Durham), dij_factor, p, γ, R) + # use SoA-based updater + update_nn_no_cross!(eereco, i, N, Val(JetAlgorithm.Durham), dij_factor, + p, γ, R) end end end if ijetA != ijetB - update_nn_cross!(eereco, ijetA, N, Val(JetAlgorithm.Durham), dij_factor, p, γ, R) + update_nn_cross!(eereco, ijetA, N, Val(JetAlgorithm.Durham), dij_factor, p, γ, + R) end end diff --git a/src/EEAlgorithmValencia.jl b/src/EEAlgorithmValencia.jl index 4390a26a..83a89b14 100644 --- a/src/EEAlgorithmValencia.jl +++ b/src/EEAlgorithmValencia.jl @@ -2,7 +2,6 @@ # Valencia-specialised helpers and implementation ################################################################################ - """ angular_distance_arrays(nx, ny, nz, i, j) -> Float64 From 2b03029525fbd8ae81e0eaa9d8c0e464cb6d63c0 Mon Sep 17 00:00:00 2001 From: Matt LeBlanc Date: Mon, 18 Aug 2025 11:28:26 -0400 Subject: [PATCH 23/32] Cleanup after failed squash ... --- src/EEJet.jl | 4 +--- test/data/valencia-cluster-sequence-eeH.json.zst | Bin 13 -> 0 bytes 2 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 test/data/valencia-cluster-sequence-eeH.json.zst diff --git a/src/EEJet.jl b/src/EEJet.jl index e62296ed..42c8e821 100644 --- a/src/EEJet.jl +++ b/src/EEJet.jl @@ -22,9 +22,7 @@ struct EEJet <: FourMomentum _cluster_hist_index::Int end -# Compatibility constructor was moved below the EERecoJet type definition - -""" +g""" EEJet(px::Real, py::Real, pz::Real, E::Real, cluster_hist_index::Int) Constructs an `EEJet` object from the given momentum components, energy, and diff --git a/test/data/valencia-cluster-sequence-eeH.json.zst b/test/data/valencia-cluster-sequence-eeH.json.zst deleted file mode 100644 index e58c09d56ab1845ad8c524844d4acd0515de2df5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13 UcmdPcs{dDofsuh>=F1y_03PE6+5i9m From 20e779058ff71e359572360ba0a0529299e42dd7 Mon Sep 17 00:00:00 2001 From: Matt LeBlanc Date: Mon, 18 Aug 2025 11:29:06 -0400 Subject: [PATCH 24/32] Cleanup after failed squash ... --- src/EEJet.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EEJet.jl b/src/EEJet.jl index 42c8e821..a28aeee1 100644 --- a/src/EEJet.jl +++ b/src/EEJet.jl @@ -22,7 +22,7 @@ struct EEJet <: FourMomentum _cluster_hist_index::Int end -g""" +""" EEJet(px::Real, py::Real, pz::Real, E::Real, cluster_hist_index::Int) Constructs an `EEJet` object from the given momentum components, energy, and From c5a7f7c86d18a93b9431bd04ef5458d965a27b8a Mon Sep 17 00:00:00 2001 From: Matt LeBlanc Date: Sat, 23 Aug 2025 11:40:07 -0400 Subject: [PATCH 25/32] Remove Val-based branching --- src/EEAlgorithm.jl | 808 ++++++++++++++++++------------------- src/EEAlgorithmValencia.jl | 408 ------------------- src/JetReconstruction.jl | 1 - test/test-valencia.jl | 84 +--- 4 files changed, 400 insertions(+), 901 deletions(-) delete mode 100644 src/EEAlgorithmValencia.jl diff --git a/src/EEAlgorithm.jl b/src/EEAlgorithm.jl index 2e59b488..34d6aa4a 100644 --- a/src/EEAlgorithm.jl +++ b/src/EEAlgorithm.jl @@ -17,309 +17,317 @@ Calculate the angular distance between two jets `i` and `j` using the formula - `Float64`: The angular distance between `i` and `j`, which is ``1 - cos\theta``. """ -@inline function angular_distance(eereco, i, j) - @inbounds @muladd 1.0 - eereco[i].nx * eereco[j].nx - eereco[i].ny * eereco[j].ny - - eereco[i].nz * eereco[j].nz +Base.@propagate_inbounds @inline function angular_distance(eereco, i, j) + if hasproperty(eereco, :nx) + nx = eereco.nx + ny = eereco.ny + nz = eereco.nz + @muladd 1.0 - nx[i] * nx[j] - ny[i] * ny[j] - nz[i] * nz[j] + else + # Fallback for Array-of-structs (AoS) + @muladd 1.0 - eereco[i].nx * eereco[j].nx - eereco[i].ny * eereco[j].ny - + eereco[i].nz * eereco[j].nz + end end """ - dij_dist(eereco, i, j, dij_factor, algorithm::JetAlgorithm.Algorithm, R = 4.0) + valencia_distance(eereco, i, j, R) -> Float64 -Calculate the dij distance between two e⁺e⁻ jets. This is the public entry point. -Internally, this forwards to a Val-based method for the given algorithm, which -allows the compiler to specialize away branches when `algorithm` is a constant. -""" -@inline function dij_dist(eereco, i, j, dij_factor, algorithm::JetAlgorithm.Algorithm, - R = 4.0) - return dij_dist(eereco, i, j, dij_factor, Val(algorithm), R) -end +Calculate the Valencia distance between two jets `i` and `j` as +``min(E_i^{2β}, E_j^{2β}) * 2 * (1 - cos(θ_{ij})) / R²``. -""" - dij_dist(eereco, i, j, dij_factor, ::Val{JetAlgorithm.Durham}, R = 4.0) - dij_dist(eereco, i, j, dij_factor, ::Val{JetAlgorithm.EEKt}, R = 4.0) +# Arguments +- `eereco`: The array of `EERecoJet` objects. +- `i`: The first jet. +- `j`: The second jet. +- `R`: The jet radius parameter. -Durham/EEKt dij distance: -min(E_i^{2p}, E_j^{2p}) * dij_factor * (angular NN metric stored in nndist). -For EEKt, dij_factor encodes the R-dependent normalization. -""" -@inline function dij_dist(eereco, i, j, dij_factor, ::Val{JetAlgorithm.Durham}, R = 4.0) - # Calculate the dij distance for jet i from jet j - j == 0 && return large_dij - @inbounds min(eereco[i].E2p, eereco[j].E2p) * dij_factor * eereco[i].nndist +# Returns +- `Float64`: The Valencia distance between `i` and `j`. +""" +Base.@propagate_inbounds @inline function valencia_distance_inv(eereco, i, j, invR2) + if hasproperty(eereco, :nx) + nx = eereco.nx + ny = eereco.ny + nz = eereco.nz + E2p = eereco.E2p + angular_dist = angular_distance_arrays(nx, ny, nz, i, j) + # Valencia dij : min(E_i^{2β}, E_j^{2β}) * 2 * (1 - cos θ) * invR2 + min(E2p[i], E2p[j]) * 2 * angular_dist * invR2 + else + # Fallback for Array-of-structs + angular_dist = 1.0 - eereco[i].nx * eereco[j].nx - eereco[i].ny * eereco[j].ny - + eereco[i].nz * eereco[j].nz + min(eereco[i].E2p, eereco[j].E2p) * 2 * angular_dist * invR2 + end end -@inline function dij_dist(eereco, i, j, dij_factor, ::Val{JetAlgorithm.EEKt}, R = 4.0) - # Calculate the dij distance for jet i from jet j - j == 0 && return large_dij - @inbounds min(eereco[i].E2p, eereco[j].E2p) * dij_factor * eereco[i].nndist +Base.@propagate_inbounds @inline function valencia_distance(eereco, i, j, R) + return valencia_distance_inv(eereco, i, j, inv(R * R)) end -# Fallback if a non-Algorithm token is passed -@inline function dij_dist(eereco, i, j, dij_factor, algorithm, R = 4.0) - throw(ArgumentError("Algorithm $algorithm not supported for dij_dist")) +# Array-based helpers: operate directly on field vectors from StructArray to avoid +# repeated eereco[i] indexing which can be slower. +Base.@propagate_inbounds @inline function angular_distance_arrays(nx, ny, nz, i, j) + @muladd 1.0 - nx[i] * nx[j] - ny[i] * ny[j] - nz[i] * nz[j] end -""" - get_angular_nearest_neighbours!(eereco, ::Val{JetAlgorithm.Durham}, dij_factor, p, γ=1.0, R=4.0) +Base.@propagate_inbounds @inline function valencia_distance_inv_arrays(E2p, nx, ny, nz, i, + j, invR2) + angular_dist = angular_distance_arrays(nx, ny, nz, i, j) + min(E2p[i], E2p[j]) * 2 * angular_dist * invR2 +end -Compute initial nearest-neighbour distances for the Durham algorithm and fill -`eereco.nndist`, `eereco.nni` and `eereco.dijdist` accordingly. This variant -is specialised for the Durham metric and operates on the StructArray layout. """ -@inline function get_angular_nearest_neighbours!(eereco, ::Val{JetAlgorithm.Durham}, - dij_factor, p, γ = 1.0, R = 4.0) - # Get the initial nearest neighbours for each jet - N = length(eereco) - # this_dist_vector = Vector{Float64}(undef, N) - # Nearest neighbour geometric distance - @inbounds for i in 1:N - # TODO: Replace the 'j' loop with a vectorised operation over the appropriate array elements? - # this_dist_vector .= 1.0 .- eereco.nx[i:N] .* eereco[i + 1:end].nx .- - # eereco[i].ny .* eereco[i + 1:end].ny .- eereco[i].nz .* eereco[i + 1:end].nz - # The problem here will be avoiding allocations for the array outputs, which would easily - # kill performance - @inbounds for j in (i + 1):N - this_nndist = angular_distance(eereco, i, j) + valencia_beam_distance(eereco, i, γ, β) -> Float64 - # Using these ternary operators is faster than the if-else block - better_nndist_i = this_nndist < eereco[i].nndist - eereco.nndist[i] = better_nndist_i ? this_nndist : eereco.nndist[i] - eereco.nni[i] = better_nndist_i ? j : eereco.nni[i] - better_nndist_j = this_nndist < eereco[j].nndist - eereco.nndist[j] = better_nndist_j ? this_nndist : eereco.nndist[j] - eereco.nni[j] = better_nndist_j ? i : eereco.nni[j] - end +Calculate the Valencia beam distance for jet `i` using the FastJet ValenciaPlugin +definition: ``d_iB = E_i^{2β} * (sin θ_i)^{2γ}``, where ``cos θ_i = nz`` +for unit direction cosines. Since ``sin^2 θ = 1 - nz^2``, we implement +``d_iB = E_i^{2β} * (1 - nz^2)^γ``. + +# Arguments +- `eereco`: The array of `EERecoJet` objects. +- `i`: The jet index. +- `γ`: The angular exponent parameter used in the Valencia beam distance. +- `β`: The energy exponent (same as `p` in our implementation). + +# Returns +- `Float64`: The Valencia beam distance for jet `i`. +""" +Base.@propagate_inbounds @inline function valencia_beam_distance(eereco, i, γ, β) + if hasproperty(eereco, :nz) + nzv = eereco.nz + E2pv = eereco.E2p + nz_i = nzv[i] + sin2 = 1 - nz_i * nz_i + E2p = E2pv[i] + else + nz_i = eereco[i].nz + sin2 = 1 - nz_i * nz_i + E2p = eereco[i].E2p end - # Nearest neighbour dij distance - for i in 1:N - eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, - Val(JetAlgorithm.Durham), R) + # Fast-paths for common γ values to avoid pow in hot loop + if γ == 1.0 + return E2p * sin2 + elseif γ == 2.0 + return E2p * (sin2 * sin2) + else + return E2p * sin2^γ + end +end + +# Array-based helper for Valencia beam distance to avoid StructArray getindex in hot loops +Base.@propagate_inbounds @inline function valencia_beam_distance_arrays(E2p, nz, i, γ, β) + nz_i = nz[i] + sin2 = 1 - nz_i * nz_i + E2p_i = E2p[i] + # Fast-paths for common γ values to avoid pow in hot loop + if γ == 1.0 + return E2p_i * sin2 + elseif γ == 2.0 + return E2p_i * (sin2 * sin2) + else + return E2p_i * sin2^γ end end """ - get_angular_nearest_neighbours!(eereco, ::Val{JetAlgorithm.EEKt}, dij_factor, p, γ=1.0, R=4.0) + dij_dist(eereco, i, j, dij_factor, algorithm::JetAlgorithm.Algorithm, R=4.0) -Compute initial nearest-neighbour distances for the EEKt algorithm and fill -`eereco.nndist`, `eereco.nni` and `eereco.dijdist`. Performs beam-distance -checks appropriate for EEKt. +Compute dij distance for (Durham, EEKt, Valencia) using simple conditionals. +Beam index `j==0` returns a large sentinel. For Valencia we use the full +Valencia metric (independent of `dij_factor`). """ -@inline function get_angular_nearest_neighbours!(eereco, ::Val{JetAlgorithm.EEKt}, - dij_factor, p, γ = 1.0, R = 4.0) +@inline function dij_dist(eereco, i, j, dij_factor, algorithm::JetAlgorithm.Algorithm, R=4.0) + j == 0 && return large_dij + @inbounds begin + if algorithm == JetAlgorithm.Valencia + return valencia_distance(eereco, i, j, R) + else + # Durham & EEKt share same form here (min(E2p_i,E2p_j) * dij_factor * angular_metric) + return min(eereco[i].E2p, eereco[j].E2p) * dij_factor * eereco[i].nndist + end + end +end + +# Fallback for incorrect algorithm token (maintains previous test behaviour) +@inline function dij_dist(eereco, i, j, dij_factor, algorithm, R=4.0) + throw(ArgumentError("Algorithm $algorithm not supported for dij_dist")) +end + +function get_angular_nearest_neighbours!(eereco, algorithm, dij_factor, p, γ = 1.0, R = 4.0) # Get the initial nearest neighbours for each jet N = length(eereco) - # this_dist_vector = Vector{Float64}(undef, N) - # Nearest neighbour geometric distance + # For Valencia, nearest-neighbour must be chosen on the full dij metric (FastJet NNH behaviour) + @inbounds for i in 1:N + local_nndist_i = Inf + local_nni_i = i + eereco.nndist[i] = local_nndist_i + eereco.nni[i] = local_nni_i + end + # Nearest neighbour search @inbounds for i in 1:N - # TODO: Replace the 'j' loop with a vectorised operation over the appropriate array elements? - # this_dist_vector .= 1.0 .- eereco.nx[i:N] .* eereco[i + 1:end].nx .- - # eereco[i].ny .* eereco[i + 1:end].ny .- eereco[i].nz .* eereco[i + 1:end].nz - # The problem here will be avoiding allocations for the array outputs, which would easily - # kill performance @inbounds for j in (i + 1):N - this_nndist = angular_distance(eereco, i, j) + # Metric used to pick the nearest neighbour + if algorithm == JetAlgorithm.Valencia + # Use array helpers to avoid repeated StructArray getindex + this_metric = valencia_distance_inv_arrays(eereco.E2p, eereco.nx, eereco.ny, + eereco.nz, i, j, inv(R * R)) + else + this_metric = angular_distance_arrays(eereco.nx, eereco.ny, eereco.nz, i, j) + end # Using these ternary operators is faster than the if-else block - better_nndist_i = this_nndist < eereco[i].nndist - eereco.nndist[i] = better_nndist_i ? this_nndist : eereco.nndist[i] + better_nndist_i = this_metric < eereco[i].nndist + eereco.nndist[i] = better_nndist_i ? this_metric : eereco.nndist[i] eereco.nni[i] = better_nndist_i ? j : eereco.nni[i] - better_nndist_j = this_nndist < eereco[j].nndist - eereco.nndist[j] = better_nndist_j ? this_nndist : eereco.nndist[j] + better_nndist_j = this_metric < eereco[j].nndist + eereco.nndist[j] = better_nndist_j ? this_metric : eereco.nndist[j] eereco.nni[j] = better_nndist_j ? i : eereco.nni[j] end end # Nearest neighbour dij distance - for i in 1:N - eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, - Val(JetAlgorithm.EEKt), R) - end - # Beam-distance check for EEKt (this variant is EEKt-specific) @inbounds for i in 1:N - beam_closer = eereco[i].E2p < eereco[i].dijdist - eereco.dijdist[i] = beam_closer ? eereco[i].E2p : eereco.dijdist[i] - eereco.nni[i] = beam_closer ? 0 : eereco.nni[i] + if algorithm == JetAlgorithm.Valencia + eereco.dijdist[i] = valencia_distance(eereco, i, eereco[i].nni, R) + else + eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, algorithm, R) + end + end + # For the EEKt algorithm, we need to check the beam distance as well + # (This is structured to only check for EEKt once) + if algorithm == JetAlgorithm.EEKt + @inbounds for i in 1:N + beam_closer = eereco[i].E2p < eereco[i].dijdist + eereco.dijdist[i] = beam_closer ? eereco[i].E2p : eereco.dijdist[i] + eereco.nni[i] = beam_closer ? 0 : eereco.nni[i] + end + elseif algorithm == JetAlgorithm.Valencia + @inbounds for i in 1:N + valencia_beam_dist = valencia_beam_distance(eereco, i, γ, p) + beam_closer = valencia_beam_dist < eereco[i].dijdist + eereco.dijdist[i] = beam_closer ? valencia_beam_dist : eereco.dijdist[i] + eereco.nni[i] = beam_closer ? 0 : eereco.nni[i] + end end end -""" - get_angular_nearest_neighbours!(eereco, algorithm::JetAlgorithm.Algorithm, dij_factor, p, γ=1.0, R=4.0) - -Forwarding wrapper that dispatches to the `Val{...}` specialised -`get_angular_nearest_neighbours!` implementation to avoid branches. -""" -@inline function get_angular_nearest_neighbours!(eereco, - algorithm::JetAlgorithm.Algorithm, - dij_factor, p, γ = 1.0, R = 4.0) - return get_angular_nearest_neighbours!(eereco, Val(algorithm), dij_factor, p, γ, R) -end - -""" - update_nn_no_cross!(eereco, i, N, algorithm::JetAlgorithm.Algorithm, dij_factor, β=1.0, γ=1.0, R=4.0) +## Removed Val-specialised get_angular_nearest_neighbours! methods (now unified above) -Update nearest-neighbour information for slot `i` (no-cross variant) by -forwarding to algorithm-specific `Val{...}` specialisations. -""" @inline function update_nn_no_cross!(eereco, i, N, algorithm::JetAlgorithm.Algorithm, dij_factor, β = 1.0, γ = 1.0, R = 4.0) - # Forward to Val-specialized implementations to avoid runtime branches - return update_nn_no_cross!(eereco, i, N, Val(algorithm), dij_factor, β, γ, R) -end - -# Val-specialized no-cross update -""" - update_nn_no_cross!(eereco, i, N, ::Val{JetAlgorithm.Durham}, dij_factor, _β=1.0, _γ=1.0, R=4.0) - -Durham-specialised update of the nearest neighbour for slot `i` (no-cross -variant). Updates `nndist`, `nni` and the inline `dijdist` for slot `i` using -direction-cosine arrays for performance. -""" -@inline function update_nn_no_cross!(eereco, i, N, ::Val{JetAlgorithm.Durham}, dij_factor, - _β = 1.0, _γ = 1.0, R = 4.0) - eereco.nndist[i] = large_distance - eereco.nni[i] = i - @inbounds for j in 1:N - if j != i - this_nndist = angular_distance(eereco, i, j) - better_nndist_i = this_nndist < eereco[i].nndist - eereco.nndist[i] = better_nndist_i ? this_nndist : eereco.nndist[i] - eereco.nni[i] = better_nndist_i ? j : eereco.nni[i] + nndist = eereco.nndist; nni = eereco.nni + nx = eereco.nx; ny = eereco.ny; nz = eereco.nz; E2p = eereco.E2p + if algorithm == JetAlgorithm.Valencia + invR2 = inv(R * R) + nndist[i] = Inf; nni[i] = i + @inbounds for j in 1:(i - 1) + this_metric = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, j, invR2) + better = this_metric < nndist[i] + nndist[i] = better ? this_metric : nndist[i] + nni[i] = better ? j : nni[i] end - end - eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, - Val(JetAlgorithm.Durham), R) -end - -""" - update_nn_no_cross!(eereco, i, N, ::Val{JetAlgorithm.EEKt}, dij_factor, _β=1.0, _γ=1.0, R=4.0) - -EEKt-specialised update of the nearest neighbour for slot `i` (no-cross -variant). Also applies the EEKt beam-distance check and updates `nni` when a -beam is closer than the dij distance. -""" -@inline function update_nn_no_cross!(eereco, i, N, ::Val{JetAlgorithm.EEKt}, dij_factor, - _β = 1.0, _γ = 1.0, R = 4.0) - # Update the nearest neighbour for jet i, w.r.t. all other active jets - # also doing the cross check for the other jet - eereco.nndist[i] = large_distance - eereco.nni[i] = i - @inbounds for j in 1:N - if j != i - this_nndist = angular_distance(eereco, i, j) - better_nndist_i = this_nndist < eereco[i].nndist - eereco.nndist[i] = better_nndist_i ? this_nndist : eereco.nndist[i] - eereco.nni[i] = better_nndist_i ? j : eereco.nni[i] - if this_nndist < eereco[j].nndist - eereco.nndist[j] = this_nndist - eereco.nni[j] = i - # j will not be revisited, so update metric distance here - eereco.dijdist[j] = dij_dist(eereco, j, i, dij_factor, - Val(JetAlgorithm.EEKt), R) - # EEKt-specific beam check is handled in the EEKt method only - end + @inbounds for j in (i + 1):N + this_metric = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, j, invR2) + better = this_metric < nndist[i] + nndist[i] = better ? this_metric : nndist[i] + nni[i] = better ? j : nni[i] + end + eereco.dijdist[i] = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, nni[i], invR2) + val_beam = valencia_beam_distance(eereco, i, γ, β) + beam_close = val_beam < eereco.dijdist[i] + eecycle = eereco.dijdist + eecycle[i] = beam_close ? val_beam : eecycle[i] + eereco.nni[i] = beam_close ? 0 : eereco.nni[i] + else + # Durham / EEKt share angular metric; EEKt adds beam check + nndist[i] = large_distance; nni[i] = i + @inbounds for j in 1:(i - 1) + this_metric = angular_distance_arrays(nx, ny, nz, i, j) + better = this_metric < nndist[i] + nndist[i] = better ? this_metric : nndist[i] + nni[i] = better ? j : nni[i] + end + @inbounds for j in (i + 1):N + this_metric = angular_distance_arrays(nx, ny, nz, i, j) + better = this_metric < nndist[i] + nndist[i] = better ? this_metric : nndist[i] + nni[i] = better ? j : nni[i] + end + E2p_i = E2p[i]; E2p_nni = E2p[nni[i]]; minE2p = E2p_i < E2p_nni ? E2p_i : E2p_nni + eereco.dijdist[i] = minE2p * dij_factor * nndist[i] + if algorithm == JetAlgorithm.EEKt + beam_close = E2p_i < eereco.dijdist[i] + eereco.dijdist[i] = beam_close ? E2p_i : eereco.dijdist[i] + eereco.nni[i] = beam_close ? 0 : eereco.nni[i] end end - eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, - Val(JetAlgorithm.EEKt), R) - - # Need to check beam for EEKt - beam_close = eereco[i].E2p < eereco[i].dijdist - eereco.dijdist[i] = beam_close ? eereco[i].E2p : eereco.dijdist[i] - eereco.nni[i] = beam_close ? 0 : eereco.nni[i] end -""" - update_nn_cross!(eereco, i, N, algorithm::JetAlgorithm.Algorithm, dij_factor, β=1.0, γ=1.0, R=4.0) +# (Forwarding handled earlier; avoid duplicate definition) -Forwarding wrapper for the cross-update variant. Dispatches to the -`Val{...}`-specialised `update_nn_cross!` to remove runtime branches. -""" @inline function update_nn_cross!(eereco, i, N, algorithm::JetAlgorithm.Algorithm, dij_factor, β = 1.0, γ = 1.0, R = 4.0) - # Forward to Val-specialized implementations to avoid runtime branches - return update_nn_cross!(eereco, i, N, Val(algorithm), dij_factor, β, γ, R) -end - -""" - update_nn_cross!(eereco, i, N, ::Val{JetAlgorithm.Durham}, dij_factor, _β=1.0, _γ=1.0, R=4.0) - -Durham-specialised cross-update: updates nearest-neighbour information for -slot `i` and propagates any changes to other slots when `i` becomes their new -nearest neighbour. Computes inline dij updates to avoid function-call overhead. -""" -@inline function update_nn_cross!(eereco, i, N, ::Val{JetAlgorithm.Durham}, dij_factor, - _β = 1.0, _γ = 1.0, R = 4.0) - # Update the nearest neighbour for jet i, w.r.t. all other active jets - # also doing the cross check for the other jet - eereco.nndist[i] = large_distance - eereco.nni[i] = i - @inbounds for j in 1:N - if j != i - this_nndist = angular_distance(eereco, i, j) - better_nndist_i = this_nndist < eereco[i].nndist - eereco.nndist[i] = better_nndist_i ? this_nndist : eereco.nndist[i] - eereco.nni[i] = better_nndist_i ? j : eereco.nni[i] - if this_nndist < eereco[j].nndist - eereco.nndist[j] = this_nndist - eereco.nni[j] = i - # j will not be revisited, so update metric distance here - eereco.dijdist[j] = dij_dist(eereco, j, i, dij_factor, - Val(JetAlgorithm.Durham), R) - # EEKt-specific beam check is handled in the EEKt method only + nndist = eereco.nndist; nni = eereco.nni + nx = eereco.nx; ny = eereco.ny; nz = eereco.nz; E2p = eereco.E2p + if algorithm == JetAlgorithm.Valencia + nndist[i] = Inf; nni[i] = i; invR2 = inv(R * R) + @inbounds for j in 1:N + if j != i + this_metric = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, j, invR2) + better = this_metric < nndist[i] + nndist[i] = better ? this_metric : nndist[i] + nni[i] = better ? j : nni[i] + if this_metric < nndist[j] + nndist[j] = this_metric + nni[j] = i + eereco.dijdist[j] = this_metric + val_beam = valencia_beam_distance_arrays(E2p, nz, j, γ, β) + if val_beam < eereco.dijdist[j] + eereco.dijdist[j] = val_beam + nni[j] = 0 + end + end end end - end - eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, - Val(JetAlgorithm.Durham), R) -end - -""" - update_nn_cross!(eereco, i, N, ::Val{JetAlgorithm.EEKt}, dij_factor, _β=1.0, _γ=1.0, R=4.0) - -EEKt-specialised cross-update. Updates `nndist`, `nni` and `dijdist` for -both the updated slot and any affected neighbours, performing beam checks -where required. -""" -@inline function update_nn_cross!(eereco, i, N, ::Val{JetAlgorithm.EEKt}, dij_factor, - _β = 1.0, _γ = 1.0, R = 4.0) - # Update the nearest neighbour for jet i, w.r.t. all other active jets - # also doing the cross check for the other jet - eereco.nndist[i] = large_distance - eereco.nni[i] = i - @inbounds for j in 1:N - if j != i - this_nndist = angular_distance(eereco, i, j) - better_nndist_i = this_nndist < eereco[i].nndist - eereco.nndist[i] = better_nndist_i ? this_nndist : eereco.nndist[i] - eereco.nni[i] = better_nndist_i ? j : eereco.nni[i] - if this_nndist < eereco[j].nndist - eereco.nndist[j] = this_nndist - eereco.nni[j] = i - # j will not be revisited, so update metric distance here - eereco.dijdist[j] = dij_dist(eereco, j, i, dij_factor, - Val(JetAlgorithm.EEKt), R) - if eereco[j].E2p < eereco[j].dijdist - eereco.dijdist[j] = eereco[j].E2p - eereco.nni[j] = 0 + eereco.dijdist[i] = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, nni[i], invR2) + val_beam = valencia_beam_distance_arrays(E2p, nz, i, γ, β) + beam_close = val_beam < eereco.dijdist[i] + eereco.dijdist[i] = beam_close ? val_beam : eereco.dijdist[i] + nni[i] = beam_close ? 0 : nni[i] + else + nndist[i] = large_distance; nni[i] = i; E2p_i = E2p[i] + @inbounds for j in 1:N + if j != i + this_metric = angular_distance_arrays(nx, ny, nz, i, j) + better = this_metric < nndist[i] + nndist[i] = better ? this_metric : nndist[i] + nni[i] = better ? j : nni[i] + if this_metric < nndist[j] + nndist[j] = this_metric + nni[j] = i + E2p_j = E2p[j] + new_dij = (E2p_i < E2p_j ? E2p_i : E2p_j) * dij_factor * this_metric + if algorithm == JetAlgorithm.EEKt && E2p_j < new_dij + eereco.dijdist[j] = E2p_j + nni[j] = 0 + else + eereco.dijdist[j] = new_dij + end end end end + E2p_nni = E2p[nni[i]]; minE2p_i = E2p_i < E2p_nni ? E2p_i : E2p_nni + eereco.dijdist[i] = minE2p_i * dij_factor * nndist[i] + if algorithm == JetAlgorithm.EEKt + beam_close = E2p_i < eereco.dijdist[i] + eereco.dijdist[i] = beam_close ? E2p_i : eereco.dijdist[i] + nni[i] = beam_close ? 0 : nni[i] + end end - eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, - Val(JetAlgorithm.EEKt), R) - - # Check beam for EEKt - beam_close = eereco[i].E2p < eereco[i].dijdist - eereco.dijdist[i] = beam_close ? eereco[i].E2p : eereco.dijdist[i] - eereco.nni[i] = beam_close ? 0 : eereco.nni[i] end -""" - ee_check_consistency(clusterseq, eereco, N) - -Run an internal consistency check over the reconstruction state. Emits -`@error` logs for invalid nearest-neighbour indices or malformed history -entries. Intended for debugging; inexpensive checks only. -""" function ee_check_consistency(clusterseq, eereco, N) # Check the consistency of the reconstruction state for i in 1:N @@ -337,6 +345,131 @@ function ee_check_consistency(clusterseq, eereco, N) @debug "Consistency check passed" end +################################################################################ +# Array-based nearest-neighbour update helpers (operate on raw vectors) +################################################################################ + +@inline function update_nn_no_cross_arrays!(nndist::AbstractVector, nni::AbstractVector, + nx::AbstractVector, ny::AbstractVector, + nz::AbstractVector, + E2p::AbstractVector, dijdist::AbstractVector, + i::Integer, N::Integer, + algorithm::JetAlgorithm.Algorithm, + dij_factor, β = 1.0, γ = 1.0, R = 4.0) + if algorithm == JetAlgorithm.Valencia + nndist[i] = Inf; nni[i] = i; invR2 = inv(R * R) + @inbounds for j in 1:N + if j != i + this_metric = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, j, invR2) + better = this_metric < nndist[i] + nndist[i] = better ? this_metric : nndist[i] + nni[i] = better ? j : nni[i] + end + end + dijdist[i] = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, nni[i], invR2) + val_beam = valencia_beam_distance_arrays(E2p, nz, i, γ, β) + beam_close = val_beam < dijdist[i] + dijdist[i] = beam_close ? val_beam : dijdist[i] + nni[i] = beam_close ? 0 : nni[i] + else + nndist[i] = large_distance; nni[i] = i; E2p_i = E2p[i] + @inbounds for j in 1:N + if j != i + this_metric = angular_distance_arrays(nx, ny, nz, i, j) + better = this_metric < nndist[i] + nndist[i] = better ? this_metric : nndist[i] + nni[i] = better ? j : nni[i] + end + end + E2p_nni = E2p[nni[i]]; minE2p = E2p_i < E2p_nni ? E2p_i : E2p_nni + dijdist[i] = minE2p * dij_factor * nndist[i] + if algorithm == JetAlgorithm.EEKt + beam_close = E2p_i < dijdist[i] + dijdist[i] = beam_close ? E2p_i : dijdist[i] + nni[i] = beam_close ? 0 : nni[i] + end + end +end + +@inline function update_nn_cross_arrays!(nndist::AbstractVector, nni::AbstractVector, + nx::AbstractVector, ny::AbstractVector, + nz::AbstractVector, + E2p::AbstractVector, dijdist::AbstractVector, + i::Integer, N::Integer, + algorithm::JetAlgorithm.Algorithm, + dij_factor, β = 1.0, γ = 1.0, R = 4.0) + if algorithm == JetAlgorithm.Valencia + nndist[i] = Inf; nni[i] = i; invR2 = inv(R * R) + @inbounds for j in 1:(i - 1) + this_metric = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, j, invR2) + better = this_metric < nndist[i] + nndist[i] = better ? this_metric : nndist[i] + nni[i] = better ? j : nni[i] + if this_metric < nndist[j] + nndist[j] = this_metric; nni[j] = i; dijdist[j] = this_metric + val_beam = valencia_beam_distance_arrays(E2p, nz, j, γ, β) + if val_beam < dijdist[j]; dijdist[j] = val_beam; nni[j] = 0; end + end + end + @inbounds for j in (i + 1):N + this_metric = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, j, invR2) + better = this_metric < nndist[i] + nndist[i] = better ? this_metric : nndist[i] + nni[i] = better ? j : nni[i] + if this_metric < nndist[j] + nndist[j] = this_metric; nni[j] = i; dijdist[j] = this_metric + val_beam = valencia_beam_distance_arrays(E2p, nz, j, γ, β) + if val_beam < dijdist[j]; dijdist[j] = val_beam; nni[j] = 0; end + end + end + dijdist[i] = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, nni[i], invR2) + val_beam = valencia_beam_distance_arrays(E2p, nz, i, γ, β) + beam_close = val_beam < dijdist[i] + dijdist[i] = beam_close ? val_beam : dijdist[i] + nni[i] = beam_close ? 0 : nni[i] + else + nndist[i] = large_distance; nni[i] = i; E2p_i = E2p[i] + @inbounds for j in 1:(i - 1) + this_metric = angular_distance_arrays(nx, ny, nz, i, j) + better = this_metric < nndist[i] + nndist[i] = better ? this_metric : nndist[i] + nni[i] = better ? j : nni[i] + if this_metric < nndist[j] + nndist[j] = this_metric; nni[j] = i + E2p_j = E2p[j] + new_dij = (E2p_i < E2p_j ? E2p_i : E2p_j) * dij_factor * this_metric + if algorithm == JetAlgorithm.EEKt && E2p_j < new_dij + dijdist[j] = E2p_j; nni[j] = 0 + else + dijdist[j] = new_dij + end + end + end + @inbounds for j in (i + 1):N + this_metric = angular_distance_arrays(nx, ny, nz, i, j) + better = this_metric < nndist[i] + nndist[i] = better ? this_metric : nndist[i] + nni[i] = better ? j : nni[i] + if this_metric < nndist[j] + nndist[j] = this_metric; nni[j] = i + E2p_j = E2p[j] + minE2p = E2p_i < E2p_j ? E2p_i : E2p_j + dijdist[j] = minE2p * dij_factor * this_metric + if algorithm == JetAlgorithm.EEKt && E2p_j < dijdist[j] + dijdist[j] = E2p_j; nni[j] = 0 + end + end + end + E2p_nni = E2p[nni[i]]; minE2p_i = E2p_i < E2p_nni ? E2p_i : E2p_nni + dijdist[i] = minE2p_i * dij_factor * nndist[i] + if algorithm == JetAlgorithm.EEKt + beam_close = E2p_i < dijdist[i] + dijdist[i] = beam_close ? E2p_i : dijdist[i] + nni[i] = beam_close ? 0 : nni[i] + end + end +end + Base.@propagate_inbounds @inline function fill_reco_array!(eereco, particles, R2, p) @inbounds for i in eachindex(particles) eereco.index[i] = i @@ -429,50 +562,17 @@ function ee_genkt_algorithm(particles::AbstractVector{T}; algorithm::JetAlgorith p::Union{Real, Nothing} = nothing, R = 4.0, recombine = addjets, preprocess = nothing, γ::Real = 1.0, β::Union{Real, Nothing} = nothing) where {T} - # (β override for Valencia handled inside _ee_genkt_algorithm_valencia) - if algorithm == JetAlgorithm.Valencia - # Apply β override if provided, then obtain validated power - local_p = β === nothing ? p : β - local_p = get_algorithm_power(p = local_p, algorithm = algorithm) - return _ee_genkt_algorithm_valencia(particles = begin - if isnothing(preprocess) - if T == EEJet - recombination_particles = copy(particles) - sizehint!(recombination_particles, - length(particles) * 2) - recombination_particles - else - recombination_particles = EEJet[] - sizehint!(recombination_particles, - length(particles) * 2) - for (i, particle) in enumerate(particles) - push!(recombination_particles, - EEJet(particle; - cluster_hist_index = i)) - end - recombination_particles - end - else - recombination_particles = EEJet[] - sizehint!(recombination_particles, - length(particles) * 2) - for (i, particle) in enumerate(particles) - push!(recombination_particles, - preprocess(particle, EEJet; - cluster_hist_index = i)) - end - recombination_particles - end - end, - algorithm = algorithm, p = local_p, R = R, - recombine = recombine, γ = γ, beta = β) + + # For Valencia, if β is provided, overwrite p + if algorithm == JetAlgorithm.Valencia && β !== nothing + p = β end - # Check for consistency algorithm power (non-Valencia path) + # Check for consistency algorithm power p = get_algorithm_power(p = p, algorithm = algorithm) - # Cast p to Int where possible for algorithms that benefit (Durham/EEKt) - if algorithm == JetAlgorithm.Durham || algorithm == JetAlgorithm.EEKt + # Integer p if possible, i.e. if not running Valencia + if algorithm != JetAlgorithm.Valencia p = (round(p) == p) ? Int(p) : p end @@ -507,126 +607,12 @@ function ee_genkt_algorithm(particles::AbstractVector{T}; algorithm::JetAlgorith end end - # Now call the actual reconstruction method. - if algorithm == JetAlgorithm.Valencia - return _ee_genkt_algorithm_valencia(particles = recombination_particles, p = p, - R = R, - algorithm = algorithm, - recombine = recombine, γ = γ) - elseif algorithm == JetAlgorithm.Durham - return _ee_genkt_algorithm_durham(particles = recombination_particles, p = p, R = R, - algorithm = algorithm, - recombine = recombine, γ = γ) - else - return _ee_genkt_algorithm(particles = recombination_particles, p = p, R = R, - algorithm = algorithm, - recombine = recombine, γ = γ) - end + # Now call the unified implementation with conditional logic. + return _ee_genkt_algorithm(particles = recombination_particles, p = p, R = R, + algorithm = algorithm, recombine = recombine, γ = γ) end -################################################################################ -# Durham-specialised implementation -################################################################################ -function _ee_genkt_algorithm_durham(; particles::AbstractVector{EEJet}, - algorithm::JetAlgorithm.Algorithm, p::Real, R = 4.0, - recombine = addjets, γ::Real = 1.0, - beta::Union{Real, Nothing} = nothing) - # Bounds - N::Int = length(particles) - - R2 = R^2 - - # Durham dij factor - dij_factor = 2.0 - - # Prepare SoA - eereco = StructArray{EERecoJet}(undef, N) - fill_reco_array!(eereco, particles, R2, p) - - # Setup history - history, Qtot = initial_history(particles) - clusterseq = ClusterSequence(algorithm, p, R, RecoStrategy.N2Plain, particles, history, - Qtot) - - # Initial nearest neighbours (Durham-specialised) - get_angular_nearest_neighbours!(eereco, Val(JetAlgorithm.Durham), dij_factor, p, γ, R) - - # Alias StructArray fields to local vectors to avoid repeated getindex - index = eereco.index - nni = eereco.nni - nndist = eereco.nndist - dijdist = eereco.dijdist - nx = eereco.nx - ny = eereco.ny - nz = eereco.nz - E2p = eereco.E2p - - # Main loop - iter = 0 - @inbounds while N != 0 - iter += 1 - - dij_min, ijetA = fast_findmin(dijdist, N) - ijetB = nni[ijetA] - - if ijetB == 0 - ijetB = ijetA - add_step_to_history!(clusterseq, - clusterseq.jets[index[ijetA]]._cluster_hist_index, - BeamJet, Invalid, dij_min) - elseif N == 1 - ijetB = ijetA - add_step_to_history!(clusterseq, - clusterseq.jets[eereco[ijetA].index]._cluster_hist_index, - BeamJet, Invalid, dij_min) - else - if ijetB < ijetA - ijetA, ijetB = ijetB, ijetA - end - jetA = clusterseq.jets[index[ijetA]] - jetB = clusterseq.jets[index[ijetB]] - merged_jet = recombine(jetA, jetB; - cluster_hist_index = length(clusterseq.history) + 1) - push!(clusterseq.jets, merged_jet) - newjet_k = length(clusterseq.jets) - add_step_to_history!(clusterseq, - minmax(cluster_hist_index(jetA), - cluster_hist_index(jetB))..., - newjet_k, dij_min) - insert_new_jet!(eereco, ijetA, newjet_k, R2, merged_jet, p) - end - - if ijetB != N - copy_to_slot!(eereco, N, ijetB) - end - - N -= 1 - - # Update nearest neighbours step using array-based helpers specialised for Durham - for i in 1:N - if (ijetB != N + 1) && (nni[i] == N + 1) - nni[i] = ijetB - else - if (nni[i] == ijetA) || (nni[i] == ijetB) || (nni[i] > N) - # use SoA-based updater - update_nn_no_cross!(eereco, i, N, Val(JetAlgorithm.Durham), dij_factor, - p, γ, R) - end - end - end - - if ijetA != ijetB - update_nn_cross!(eereco, ijetA, N, Val(JetAlgorithm.Durham), dij_factor, p, γ, - R) - end - end - - clusterseq -end - -################################################################################ -# EEKt-specialised implementation -################################################################################ +## Removed Durham/Valencia specialised implementations; unified below. """ _ee_genkt_algorithm!(particles::AbstractVector{EEJet}; @@ -662,14 +648,10 @@ function _ee_genkt_algorithm(; particles::AbstractVector{EEJet}, # Bounds N::Int = length(particles) - # Forward Valencia directly to specialised implementation (tests call this internal API) - if algorithm == JetAlgorithm.Valencia - return _ee_genkt_algorithm_valencia(particles = particles, algorithm = algorithm, - p = (beta === nothing ? p : beta), R = R, - recombine = recombine, γ = γ, beta = beta) - end - R2 = R^2 + if algorithm == JetAlgorithm.Valencia && beta !== nothing + p = beta + end # Constant factor for the dij metric and the beam distance function if algorithm == JetAlgorithm.Durham @@ -680,6 +662,8 @@ function _ee_genkt_algorithm(; particles::AbstractVector{EEJet}, else dij_factor = 1 / (3 + cos(R)) end + elseif algorithm == JetAlgorithm.Valencia + dij_factor = 1.0 else throw(ArgumentError("Algorithm $algorithm not supported for e+e-")) end diff --git a/src/EEAlgorithmValencia.jl b/src/EEAlgorithmValencia.jl deleted file mode 100644 index 83a89b14..00000000 --- a/src/EEAlgorithmValencia.jl +++ /dev/null @@ -1,408 +0,0 @@ -################################################################################ -# Valencia-specialised helpers and implementation -################################################################################ - -""" - angular_distance_arrays(nx, ny, nz, i, j) -> Float64 - -Compute the angular distance (1 - cos θ) between entries `i` and `j` using -direction-cosine arrays `nx`, `ny`, `nz`. - -# Returns -- `Float64`: angular distance. -""" -Base.@propagate_inbounds @inline function angular_distance_arrays(nx, ny, nz, i, j) - @muladd 1.0 - nx[i] * nx[j] - ny[i] * ny[j] - nz[i] * nz[j] -end - -""" - valencia_distance_inv(eereco, i, j, invR2) -> Float64 - -Calculate the Valencia dij metric (scaled by `invR2`) between slots `i` and -`j` in a StructArray `eereco`. Uses E2p and direction cosines from `eereco`. -""" -Base.@propagate_inbounds @inline function valencia_distance_inv(eereco, i, j, invR2) - # Assume SoA layout - nx = eereco.nx - ny = eereco.ny - nz = eereco.nz - E2p = eereco.E2p - angular_dist = angular_distance_arrays(nx, ny, nz, i, j) - # Valencia dij : min(E_i^{2β}, E_j^{2β}) * 2 * (1 - cos θ) * invR2 - min(E2p[i], E2p[j]) * 2 * angular_dist * invR2 -end - -""" - dij_dist(eereco, i, j, dij_factor, ::Val{JetAlgorithm.Valencia}, R) - -Valencia-specialised `dij_dist` which computes the Valencia dij metric for -slots `i` and `j`. Returns a large sentinel distance for beam index `j==0`. -""" -@inline function dij_dist(eereco, i, j, dij_factor, ::Val{JetAlgorithm.Valencia}, R) - j == 0 && return large_dij - @inbounds valencia_distance(eereco, i, j, R) -end - -""" - valencia_distance(eereco, i, j, R) -> Float64 - -Compute the Valencia dij metric between `i` and `j` using explicit `R` and -delegating to `valencia_distance_inv` with the appropriate scaling. -""" -Base.@propagate_inbounds @inline function valencia_distance(eereco, i, j, R) - return valencia_distance_inv(eereco, i, j, inv(R * R)) -end - -# Array-based helpers -""" - valencia_distance_inv_arrays(E2p, nx, ny, nz, i, j, invR2) -> Float64 - -Array-based variant of `valencia_distance_inv` that works directly on raw -vectors (useful for the precomputed helpers/fast paths). -""" -Base.@propagate_inbounds @inline function valencia_distance_inv_arrays(E2p, nx, ny, nz, i, - j, invR2) - angular_dist = angular_distance_arrays(nx, ny, nz, i, j) - min(E2p[i], E2p[j]) * 2 * angular_dist * invR2 -end - -# Scaled variant -""" - valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, j) -> Float64 - -Compute the Valencia distance using a pre-scaled E2p vector (`E2p_scaled`), -avoiding repeated multiplication by the R-dependent factor. -""" -Base.@propagate_inbounds @inline function valencia_distance_inv_scaled_arrays(E2p_scaled, - nx, ny, nz, i, - j) - angular_dist = angular_distance_arrays(nx, ny, nz, i, j) - min(E2p_scaled[i], E2p_scaled[j]) * angular_dist -end - -""" - valencia_beam_distance(eereco, i, γ, β) -> Float64 - -Compute the Valencia beam-distance term for slot `i` given angular exponent -`γ` and (unused) `β`. Uses direction cosine `nz` and E2p value. -""" -Base.@propagate_inbounds @inline function valencia_beam_distance(eereco, i, γ, β) - nzv = eereco.nz - E2pv = eereco.E2p - nz_i = nzv[i] - sin2 = 1 - nz_i * nz_i - E2p = E2pv[i] - if γ == 1.0 - return E2p * sin2 - elseif γ == 2.0 - return E2p * (sin2 * sin2) - else - return E2p * sin2^γ - end -end - -""" - valencia_beam_distance_arrays(E2p, nz, i, γ, β) -> Float64 - -Array-based variant of `valencia_beam_distance` that operates on raw `E2p` -and `nz` vectors. -""" -Base.@propagate_inbounds @inline function valencia_beam_distance_arrays(E2p, nz, i, γ, β) - nz_i = nz[i] - sin2 = 1 - nz_i * nz_i - E2p_i = E2p[i] - if γ == 1.0 - return E2p_i * sin2 - elseif γ == 2.0 - return E2p_i * (sin2 * sin2) - else - return E2p_i * sin2^γ - end -end - -""" - get_angular_nearest_neighbours_valencia_precomputed!(eereco, E2p_scaled, beam_term, p, γ=1.0, R=4.0) - -Initialize nearest-neighbour arrays for the Valencia algorithm using -precomputed scaled energy vector `E2p_scaled` and `beam_term`. This avoids -repeated per-pair scaling inside loops. -""" -Base.@propagate_inbounds @inline function get_angular_nearest_neighbours_valencia_precomputed!(eereco, - E2p_scaled::AbstractVector, - beam_term::AbstractVector, - p, - γ = 1.0, - R = 4.0) - N = length(eereco) - nx = eereco.nx - ny = eereco.ny - nz = eereco.nz - nndist = eereco.nndist - nni = eereco.nni - @inbounds for i in 1:N - nndist[i] = Inf - nni[i] = i - end - @inbounds for i in 1:N - local_nndist_i = nndist[i] - local_nni_i = nni[i] - @inbounds for j in (i + 1):N - this_metric = valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, j) - if this_metric < local_nndist_i - local_nndist_i = this_metric - local_nni_i = j - end - if this_metric < nndist[j] - nndist[j] = this_metric - nni[j] = i - end - end - nndist[i] = local_nndist_i - nni[i] = local_nni_i - end - @inbounds for i in 1:N - eereco.dijdist[i] = valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, - nni[i]) - end - @inbounds for i in 1:N - beam_closer = beam_term[i] < eereco.dijdist[i] - eereco.dijdist[i] = beam_closer ? beam_term[i] : eereco.dijdist[i] - eereco.nni[i] = beam_closer ? 0 : eereco.nni[i] - end -end - -""" - update_nn_no_cross_arrays_precomputed!(nndist, nni, nx, ny, nz, E2p_scaled, beam_term, dijdist, i, N, dij_factor, β=1.0, γ=1.0, R=4.0) - -Precomputed Valencia no-cross nearest-neighbour update. Uses `E2p_scaled` -and `beam_term` arrays to compute distances without per-pair scaling. -""" -@inline function update_nn_no_cross_arrays_precomputed!(nndist::AbstractVector, - nni::AbstractVector, - nx::AbstractVector, - ny::AbstractVector, - nz::AbstractVector, - E2p_scaled::AbstractVector, - beam_term::AbstractVector, - dijdist::AbstractVector, - i::Integer, N::Integer, - dij_factor, β = 1.0, γ = 1.0, - R = 4.0) - nndist_i = Inf - nni_i = i - @inbounds for j in 1:N - if j != i - this_metric = valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, j) - if this_metric < nndist_i - nndist_i = this_metric - nni_i = j - end - end - end - dijdist_i = valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, nni_i) - if beam_term[i] < dijdist_i - dijdist_i = beam_term[i] - nni_i = 0 - end - nndist[i] = nndist_i - nni[i] = nni_i - dijdist[i] = dijdist_i -end - -""" - update_nn_cross_arrays_precomputed!(nndist, nni, nx, ny, nz, E2p_scaled, beam_term, dijdist, i, N, dij_factor, β=1.0, γ=1.0, R=4.0) - -Precomputed Valencia cross-update variant: updates neighbour data for slot -`i` and any affected neighbours using `E2p_scaled` and `beam_term`. -""" -@inline function update_nn_cross_arrays_precomputed!(nndist::AbstractVector, - nni::AbstractVector, - nx::AbstractVector, ny::AbstractVector, - nz::AbstractVector, - E2p_scaled::AbstractVector, - beam_term::AbstractVector, - dijdist::AbstractVector, - i::Integer, N::Integer, - dij_factor, β = 1.0, γ = 1.0, R = 4.0) - nndist_i = Inf - nni_i = i - @inbounds for j in 1:(i - 1) - this_metric = valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, j) - if this_metric < nndist_i - nndist_i = this_metric - nni_i = j - end - if this_metric < nndist[j] - nndist[j] = this_metric - nni[j] = i - dijdist[j] = this_metric - if beam_term[j] < dijdist[j] - dijdist[j] = beam_term[j] - nni[j] = 0 - end - end - end - @inbounds for j in (i + 1):N - this_metric = valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, j) - if this_metric < nndist_i - nndist_i = this_metric - nni_i = j - end - if this_metric < nndist[j] - nndist[j] = this_metric - nni[j] = i - dijdist[j] = this_metric - if beam_term[j] < dijdist[j] - dijdist[j] = beam_term[j] - nni[j] = 0 - end - end - end - # Finalize slot i - dijdist_i = valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, nni_i) - if beam_term[i] < dijdist_i - dijdist_i = beam_term[i] - nni_i = 0 - end - nndist[i] = nndist_i - nni[i] = nni_i - dijdist[i] = dijdist_i -end - -""" - _ee_genkt_algorithm_valencia(; particles::AbstractVector{EEJet}, algorithm::JetAlgorithm.Algorithm, p::Real, R=4.0, recombine=addjets, γ::Real=1.0, beta::Union{Real, Nothing}=nothing) - -Valencia-specialised implementation of the e+e- gen-kT clustering algorithm. -This implementation precomputes scaled energy and beam-term arrays to speed up -nearest-neighbour computations for the Valencia metric. -""" -function _ee_genkt_algorithm_valencia(; particles::AbstractVector{EEJet}, - algorithm::JetAlgorithm.Algorithm, p::Real, R = 4.0, - recombine = addjets, γ::Real = 1.0, - beta::Union{Real, Nothing} = nothing) - N::Int = length(particles) - R2 = R^2 - if algorithm == JetAlgorithm.Valencia && beta !== nothing - p = beta - end - dij_factor = 1.0 - eereco = StructArray{EERecoJet}(undef, N) - fill_reco_array!(eereco, particles, R2, p) - history, Qtot = initial_history(particles) - clusterseq = ClusterSequence(algorithm, p, R, RecoStrategy.N2Plain, particles, history, - Qtot) - indexv = eereco.index - nni_v = eereco.nni - nndist_v = eereco.nndist - dijdist_v = eereco.dijdist - nxv = eereco.nx - nyv = eereco.ny - nzv = eereco.nz - E2pv = eereco.E2p - invR2 = inv(R * R) - factor = 2 * invR2 - E2p_scaled = similar(E2pv) - beam_term = similar(E2pv) - @inbounds for k in 1:N - E2p_scaled[k] = E2pv[k] * factor - nz_k = nzv[k] - sin2 = 1.0 - nz_k * nz_k - if γ == 1.0 - beam_term[k] = E2pv[k] * sin2 - elseif γ == 2.0 - beam_term[k] = E2pv[k] * (sin2 * sin2) - else - beam_term[k] = E2pv[k] * sin2^γ - end - end - get_angular_nearest_neighbours_valencia_precomputed!(eereco, E2p_scaled, beam_term, p, - γ, R) - iter = 0 - while N != 0 - iter += 1 - dij_min, ijetA = fast_findmin(dijdist_v, N) - ijetB = nni_v[ijetA] - if ijetB == 0 - ijetB = ijetA - add_step_to_history!(clusterseq, - clusterseq.jets[indexv[ijetA]]._cluster_hist_index, - BeamJet, Invalid, dij_min) - elseif N == 1 - ijetB = ijetA - add_step_to_history!(clusterseq, - clusterseq.jets[indexv[ijetA]]._cluster_hist_index, - BeamJet, Invalid, dij_min) - else - if ijetB < ijetA - ijetA, ijetB = ijetB, ijetA - end - jetA = clusterseq.jets[indexv[ijetA]] - jetB = clusterseq.jets[indexv[ijetB]] - merged_jet = recombine(jetA, jetB; - cluster_hist_index = length(clusterseq.history) + 1) - push!(clusterseq.jets, merged_jet) - newjet_k = length(clusterseq.jets) - add_step_to_history!(clusterseq, - minmax(cluster_hist_index(jetA), - cluster_hist_index(jetB))..., - newjet_k, dij_min) - indexv[ijetA] = newjet_k - nni_v[ijetA] = 0 - nndist_v[ijetA] = R2 - nxv[ijetA] = nx(merged_jet) - nyv[ijetA] = ny(merged_jet) - nzv[ijetA] = nz(merged_jet) - E = energy(merged_jet) - if p isa Int - if p == 1 - E2pv[ijetA] = E * E - else - E2 = E * E - E2pv[ijetA] = E2^p - end - else - E2pv[ijetA] = E^(2p) - end - E2p_scaled[ijetA] = E2pv[ijetA] * factor - nz_k = nzv[ijetA] - sin2_k = 1.0 - nz_k * nz_k - if γ == 1.0 - beam_term[ijetA] = E2pv[ijetA] * sin2_k - elseif γ == 2.0 - beam_term[ijetA] = E2pv[ijetA] * (sin2_k * sin2_k) - else - beam_term[ijetA] = E2pv[ijetA] * sin2_k^γ - end - end - if ijetB != N - indexv[ijetB] = indexv[N] - nni_v[ijetB] = nni_v[N] - nndist_v[ijetB] = nndist_v[N] - dijdist_v[ijetB] = dijdist_v[N] - nxv[ijetB] = nxv[N] - nyv[ijetB] = nyv[N] - nzv[ijetB] = nzv[N] - E2pv[ijetB] = E2pv[N] - E2p_scaled[ijetB] = E2p_scaled[N] - beam_term[ijetB] = beam_term[N] - end - N -= 1 - @inbounds for i in 1:N - if (ijetB != N + 1) && (nni_v[i] == N + 1) - nni_v[i] = ijetB - else - if (nni_v[i] == ijetA) || (nni_v[i] == ijetB) || (nni_v[i] > N) - update_nn_no_cross_arrays_precomputed!(nndist_v, nni_v, nxv, nyv, nzv, - E2p_scaled, beam_term, dijdist_v, - i, N, dij_factor, p, γ, R) - end - end - end - if ijetA != ijetB - update_nn_cross_arrays_precomputed!(nndist_v, nni_v, nxv, nyv, nzv, - E2p_scaled, beam_term, dijdist_v, - ijetA, N, dij_factor, p, γ, R) - end - end - clusterseq -end diff --git a/src/JetReconstruction.jl b/src/JetReconstruction.jl index b83de2b8..dd171900 100644 --- a/src/JetReconstruction.jl +++ b/src/JetReconstruction.jl @@ -79,7 +79,6 @@ export tiled_jet_reconstruct ## E+E- algorithms include("EEAlgorithm.jl") export ee_genkt_algorithm -include("EEAlgorithmValencia.jl") ## SoftKiller include("SoftKiller.jl") diff --git a/test/test-valencia.jl b/test/test-valencia.jl index 699b3251..8431cd11 100644 --- a/test/test-valencia.jl +++ b/test/test-valencia.jl @@ -68,11 +68,8 @@ run_reco_test(valencia_exclusive_d500_b1g1) # Test dij_dist for Valencia algorithm @testset "dij_dist Valencia" begin # Minimal eereco with two reco jets; only nx,ny,nz,E2p are used by valencia_distance - eereco = StructArray{EERecoJet}(undef, 2) - eereco.nx .= [1.0, 0.0] - eereco.ny .= [0.0, 1.0] - eereco.nz .= [0.0, 0.0] - eereco.E2p .= [1.0, 2.0] + eereco = EERecoJet[EERecoJet(1, 0, Inf, Inf, 1.0, 0.0, 0.0, 1.0), + EERecoJet(2, 0, Inf, Inf, 0.0, 1.0, 0.0, 2.0)] dij = dij_dist(eereco, 1, 2, 1.0, JetAlgorithm.Valencia, 0.8) @test dij ≈ valencia_distance(eereco, 1, 2, 0.8) end @@ -80,11 +77,8 @@ end # Valencia distance wrapper coverage @testset "Valencia distance wrappers" begin # Minimal eereco with two reco jets and identical directions so angle=0 - eereco = StructArray{EERecoJet}(undef, 2) - eereco.nx .= [1.0, 1.0] - eereco.ny .= [0.0, 0.0] - eereco.nz .= [0.0, 0.0] - eereco.E2p .= [9.0, 4.0] + eereco = EERecoJet[EERecoJet(1, 0, Inf, Inf, 1.0, 0.0, 0.0, 9.0), + EERecoJet(2, 0, Inf, Inf, 1.0, 0.0, 0.0, 4.0)] R = 2.0 invR2 = inv(R * R) @test valencia_distance_inv(eereco, 1, 2, invR2) == 0.0 @@ -108,73 +102,3 @@ end R = 0.8, γ = 1.2) @test cs isa JetReconstruction.ClusterSequence end - -@testset "Valencia precomputed helpers (unit)" begin - # Construct a tiny set of EEJet with controlled directions and energies - # Ensure all jets have non-zero momentum to avoid undefined direction cosines - p1 = EEJet(0.0, 0.0, 1.0, 10.0; cluster_hist_index = 1) - p2 = EEJet(0.0, 0.1, 0.5, 5.0; cluster_hist_index = 2) - p3 = EEJet(0.1, 0.0, -1.0, 2.0; cluster_hist_index = 3) - parts = [p1, p2, p3] - N = length(parts) - R = 1.0 - pwr = 1.2 - γ = 1.0 - - # Prepare SoA like Valencia entrypoint - eereco = StructArray{EERecoJet}(undef, N) - fill_reco_array!(eereco, parts, R^2, pwr) - - nx = eereco.nx - ny = eereco.ny - nz = eereco.nz - E2p = eereco.E2p - invR2 = inv(R * R) - factor = 2 * invR2 - E2p_scaled = similar(E2p) - beam_term = similar(E2p) - for k in 1:N - E2p_scaled[k] = E2p[k] * factor - sin2 = 1.0 - nz[k] * nz[k] - beam_term[k] = E2p[k] * sin2^γ - end - - # valencia_distance_inv_scaled_arrays: compare to manual computation - i, j = 1, 2 - ang = 1.0 - nx[i] * nx[j] - ny[i] * ny[j] - nz[i] * nz[j] - @test JetReconstruction.valencia_distance_inv_scaled_arrays(E2p_scaled, nx, ny, nz, i, - j) ≈ - min(E2p_scaled[i], E2p_scaled[j]) * ang - - # beam distance arrays - @test JetReconstruction.valencia_beam_distance_arrays(E2p, nz, 1, γ, pwr) ≈ beam_term[1] - - # initializer: should populate nni/nndist/dijdist without error - JetReconstruction.get_angular_nearest_neighbours_valencia_precomputed!(eereco, - E2p_scaled, - beam_term, pwr, - γ, R) - @test all(isfinite, eereco.nndist) - @test all(isfinite, eereco.dijdist) - @test all(i -> isa(i, Int), eereco.nni) - - # array-based no-cross and cross update helpers should execute and produce finite outputs - nnd = copy(eereco.nndist) - nni = copy(eereco.nni) - dijd = copy(eereco.dijdist) - JetReconstruction.update_nn_no_cross_arrays_precomputed!(nnd, nni, nx, ny, nz, - E2p_scaled, beam_term, dijd, 2, - N, 1.0, pwr, γ, R) - @test isfinite(nnd[2]) - @test isfinite(dijd[2]) - - # cross update (should update other slots if appropriate) — assert runs and outputs are finite - nnd2 = copy(nnd) - nni2 = copy(nni) - dijd2 = copy(dijd) - JetReconstruction.update_nn_cross_arrays_precomputed!(nnd2, nni2, nx, ny, nz, - E2p_scaled, beam_term, dijd2, 2, - N, 1.0, pwr, γ, R) - @test all(isfinite, nnd2) - @test all(isfinite, dijd2) -end From 60fef1e15bc3dec0bd013ca6a25d8153f8646747 Mon Sep 17 00:00:00 2001 From: Matt LeBlanc Date: Sun, 24 Aug 2025 18:31:26 -0400 Subject: [PATCH 26/32] Simplify, back to conditionals --- src/EEAlgorithm.jl | 432 ++++++++++++--------------------------------- 1 file changed, 109 insertions(+), 323 deletions(-) diff --git a/src/EEAlgorithm.jl b/src/EEAlgorithm.jl index 34d6aa4a..0a0c46cd 100644 --- a/src/EEAlgorithm.jl +++ b/src/EEAlgorithm.jl @@ -17,16 +17,30 @@ Calculate the angular distance between two jets `i` and `j` using the formula - `Float64`: The angular distance between `i` and `j`, which is ``1 - cos\theta``. """ -Base.@propagate_inbounds @inline function angular_distance(eereco, i, j) - if hasproperty(eereco, :nx) - nx = eereco.nx - ny = eereco.ny - nz = eereco.nz - @muladd 1.0 - nx[i] * nx[j] - ny[i] * ny[j] - nz[i] * nz[j] - else - # Fallback for Array-of-structs (AoS) - @muladd 1.0 - eereco[i].nx * eereco[j].nx - eereco[i].ny * eereco[j].ny - - eereco[i].nz * eereco[j].nz +@inline function angular_distance(eereco, i, j) + @inbounds @muladd 1.0 - eereco[i].nx * eereco[j].nx - eereco[i].ny * eereco[j].ny - + eereco[i].nz * eereco[j].nz +end + +""" + dij_dist(eereco, i, j, dij_factor, algorithm::JetAlgorithm.Algorithm, R=4.0) + +Compute dij distance for (Durham, EEKt, Valencia) using simple conditionals. +Beam index `j==0` returns a large sentinel. For Valencia we use the full +Valencia metric (independent of `dij_factor`). +""" +@inline function dij_dist(eereco, i, j, dij_factor, algorithm, R=4.0) + if !(algorithm isa JetAlgorithm.Algorithm) + throw(ArgumentError("algorithm must be a JetAlgorithm.Algorithm")) + end + j == 0 && return large_dij + @inbounds begin + if algorithm == JetAlgorithm.Valencia + return valencia_distance(eereco, i, j, R) + else + # Durham & EEKt share same form here (min(E2p_i,E2p_j) * dij_factor * angular_metric) + return min(eereco[i].E2p, eereco[j].E2p) * dij_factor * eereco[i].nndist + end end end @@ -45,38 +59,13 @@ Calculate the Valencia distance between two jets `i` and `j` as # Returns - `Float64`: The Valencia distance between `i` and `j`. """ -Base.@propagate_inbounds @inline function valencia_distance_inv(eereco, i, j, invR2) - if hasproperty(eereco, :nx) - nx = eereco.nx - ny = eereco.ny - nz = eereco.nz - E2p = eereco.E2p - angular_dist = angular_distance_arrays(nx, ny, nz, i, j) - # Valencia dij : min(E_i^{2β}, E_j^{2β}) * 2 * (1 - cos θ) * invR2 - min(E2p[i], E2p[j]) * 2 * angular_dist * invR2 - else - # Fallback for Array-of-structs - angular_dist = 1.0 - eereco[i].nx * eereco[j].nx - eereco[i].ny * eereco[j].ny - - eereco[i].nz * eereco[j].nz - min(eereco[i].E2p, eereco[j].E2p) * 2 * angular_dist * invR2 - end -end - Base.@propagate_inbounds @inline function valencia_distance(eereco, i, j, R) - return valencia_distance_inv(eereco, i, j, inv(R * R)) -end - -# Array-based helpers: operate directly on field vectors from StructArray to avoid -# repeated eereco[i] indexing which can be slower. -Base.@propagate_inbounds @inline function angular_distance_arrays(nx, ny, nz, i, j) - @muladd 1.0 - nx[i] * nx[j] - ny[i] * ny[j] - nz[i] * nz[j] + invR2 = inv(R * R) + @muladd angular_dist = 1.0 - eereco[i].nx * eereco[j].nx - eereco[i].ny * eereco[j].ny - + eereco[i].nz * eereco[j].nz + return min(eereco[i].E2p, eereco[j].E2p) * 2 * angular_dist * invR2 end -Base.@propagate_inbounds @inline function valencia_distance_inv_arrays(E2p, nx, ny, nz, i, - j, invR2) - angular_dist = angular_distance_arrays(nx, ny, nz, i, j) - min(E2p[i], E2p[j]) * 2 * angular_dist * invR2 -end """ valencia_beam_distance(eereco, i, γ, β) -> Float64 @@ -96,18 +85,9 @@ for unit direction cosines. Since ``sin^2 θ = 1 - nz^2``, we implement - `Float64`: The Valencia beam distance for jet `i`. """ Base.@propagate_inbounds @inline function valencia_beam_distance(eereco, i, γ, β) - if hasproperty(eereco, :nz) - nzv = eereco.nz - E2pv = eereco.E2p - nz_i = nzv[i] - sin2 = 1 - nz_i * nz_i - E2p = E2pv[i] - else - nz_i = eereco[i].nz - sin2 = 1 - nz_i * nz_i - E2p = eereco[i].E2p - end - # Fast-paths for common γ values to avoid pow in hot loop + nz_i = eereco[i].nz + sin2 = 1 - nz_i * nz_i + E2p = eereco[i].E2p if γ == 1.0 return E2p * sin2 elseif γ == 2.0 @@ -117,65 +97,27 @@ Base.@propagate_inbounds @inline function valencia_beam_distance(eereco, i, γ, end end -# Array-based helper for Valencia beam distance to avoid StructArray getindex in hot loops -Base.@propagate_inbounds @inline function valencia_beam_distance_arrays(E2p, nz, i, γ, β) - nz_i = nz[i] - sin2 = 1 - nz_i * nz_i - E2p_i = E2p[i] - # Fast-paths for common γ values to avoid pow in hot loop - if γ == 1.0 - return E2p_i * sin2 - elseif γ == 2.0 - return E2p_i * (sin2 * sin2) - else - return E2p_i * sin2^γ - end -end - -""" - dij_dist(eereco, i, j, dij_factor, algorithm::JetAlgorithm.Algorithm, R=4.0) - -Compute dij distance for (Durham, EEKt, Valencia) using simple conditionals. -Beam index `j==0` returns a large sentinel. For Valencia we use the full -Valencia metric (independent of `dij_factor`). -""" -@inline function dij_dist(eereco, i, j, dij_factor, algorithm::JetAlgorithm.Algorithm, R=4.0) - j == 0 && return large_dij - @inbounds begin - if algorithm == JetAlgorithm.Valencia - return valencia_distance(eereco, i, j, R) - else - # Durham & EEKt share same form here (min(E2p_i,E2p_j) * dij_factor * angular_metric) - return min(eereco[i].E2p, eereco[j].E2p) * dij_factor * eereco[i].nndist - end - end -end - -# Fallback for incorrect algorithm token (maintains previous test behaviour) -@inline function dij_dist(eereco, i, j, dij_factor, algorithm, R=4.0) - throw(ArgumentError("Algorithm $algorithm not supported for dij_dist")) -end - function get_angular_nearest_neighbours!(eereco, algorithm, dij_factor, p, γ = 1.0, R = 4.0) # Get the initial nearest neighbours for each jet N = length(eereco) - # For Valencia, nearest-neighbour must be chosen on the full dij metric (FastJet NNH behaviour) + # Initialise sentinels so the first comparison always wins @inbounds for i in 1:N - local_nndist_i = Inf - local_nni_i = i - eereco.nndist[i] = local_nndist_i - eereco.nni[i] = local_nni_i + if algorithm == JetAlgorithm.Valencia + eereco.nndist[i] = Inf + else + eereco.nndist[i] = large_distance + end + eereco.nni[i] = i end # Nearest neighbour search @inbounds for i in 1:N @inbounds for j in (i + 1):N # Metric used to pick the nearest neighbour if algorithm == JetAlgorithm.Valencia - # Use array helpers to avoid repeated StructArray getindex - this_metric = valencia_distance_inv_arrays(eereco.E2p, eereco.nx, eereco.ny, - eereco.nz, i, j, inv(R * R)) + # Use canonical Valencia distance (StructArray-aware) + this_metric = valencia_distance(eereco, i, j, R) else - this_metric = angular_distance_arrays(eereco.nx, eereco.ny, eereco.nz, i, j) + this_metric = angular_distance(eereco, i, j) end # Using these ternary operators is faster than the if-else block @@ -195,8 +137,8 @@ function get_angular_nearest_neighbours!(eereco, algorithm, dij_factor, p, γ = eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, algorithm, R) end end - # For the EEKt algorithm, we need to check the beam distance as well - # (This is structured to only check for EEKt once) + # For the EEKt and Valencia algorithms, we need to check the beam distance as well + # (This is structured to check each algorithm's beam distance once) if algorithm == JetAlgorithm.EEKt @inbounds for i in 1:N beam_closer = eereco[i].E2p < eereco[i].dijdist @@ -213,118 +155,89 @@ function get_angular_nearest_neighbours!(eereco, algorithm, dij_factor, p, γ = end end -## Removed Val-specialised get_angular_nearest_neighbours! methods (now unified above) - @inline function update_nn_no_cross!(eereco, i, N, algorithm::JetAlgorithm.Algorithm, dij_factor, β = 1.0, γ = 1.0, R = 4.0) - nndist = eereco.nndist; nni = eereco.nni - nx = eereco.nx; ny = eereco.ny; nz = eereco.nz; E2p = eereco.E2p + # Valencia metric is unbounded, others use a large finite value if algorithm == JetAlgorithm.Valencia - invR2 = inv(R * R) - nndist[i] = Inf; nni[i] = i - @inbounds for j in 1:(i - 1) - this_metric = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, j, invR2) - better = this_metric < nndist[i] - nndist[i] = better ? this_metric : nndist[i] - nni[i] = better ? j : nni[i] - end - @inbounds for j in (i + 1):N - this_metric = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, j, invR2) - better = this_metric < nndist[i] - nndist[i] = better ? this_metric : nndist[i] - nni[i] = better ? j : nni[i] + eereco.nndist[i] = Inf + else + eereco.nndist[i] = large_distance + end + eereco.nni[i] = i + # Scan all other jets, update i, and cross-update j + @inbounds for j in 1:N + if j != i + this_metric = algorithm == JetAlgorithm.Valencia ? + valencia_distance(eereco, i, j, R) : + angular_distance(eereco, i, j) + better_i = this_metric < eereco[i].nndist + eereco.nndist[i] = better_i ? this_metric : eereco.nndist[i] + eereco.nni[i] = better_i ? j : eereco.nni[i] end - eereco.dijdist[i] = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, nni[i], invR2) + end + # Set dij for i using unified dispatcher and apply beam checks + eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, algorithm, R) + if algorithm == JetAlgorithm.EEKt + beam_close = eereco[i].E2p < eereco[i].dijdist + eereco.dijdist[i] = beam_close ? eereco[i].E2p : eereco.dijdist[i] + eereco.nni[i] = beam_close ? 0 : eereco.nni[i] + elseif algorithm == JetAlgorithm.Valencia val_beam = valencia_beam_distance(eereco, i, γ, β) - beam_close = val_beam < eereco.dijdist[i] - eecycle = eereco.dijdist - eecycle[i] = beam_close ? val_beam : eecycle[i] + beam_close = val_beam < eereco[i].dijdist + eereco.dijdist[i] = beam_close ? val_beam : eereco.dijdist[i] eereco.nni[i] = beam_close ? 0 : eereco.nni[i] - else - # Durham / EEKt share angular metric; EEKt adds beam check - nndist[i] = large_distance; nni[i] = i - @inbounds for j in 1:(i - 1) - this_metric = angular_distance_arrays(nx, ny, nz, i, j) - better = this_metric < nndist[i] - nndist[i] = better ? this_metric : nndist[i] - nni[i] = better ? j : nni[i] - end - @inbounds for j in (i + 1):N - this_metric = angular_distance_arrays(nx, ny, nz, i, j) - better = this_metric < nndist[i] - nndist[i] = better ? this_metric : nndist[i] - nni[i] = better ? j : nni[i] - end - E2p_i = E2p[i]; E2p_nni = E2p[nni[i]]; minE2p = E2p_i < E2p_nni ? E2p_i : E2p_nni - eereco.dijdist[i] = minE2p * dij_factor * nndist[i] - if algorithm == JetAlgorithm.EEKt - beam_close = E2p_i < eereco.dijdist[i] - eereco.dijdist[i] = beam_close ? E2p_i : eereco.dijdist[i] - eereco.nni[i] = beam_close ? 0 : eereco.nni[i] - end end end -# (Forwarding handled earlier; avoid duplicate definition) - @inline function update_nn_cross!(eereco, i, N, algorithm::JetAlgorithm.Algorithm, dij_factor, β = 1.0, γ = 1.0, R = 4.0) - nndist = eereco.nndist; nni = eereco.nni - nx = eereco.nx; ny = eereco.ny; nz = eereco.nz; E2p = eereco.E2p + # Valencia metric is unbounded, others use a large finite value if algorithm == JetAlgorithm.Valencia - nndist[i] = Inf; nni[i] = i; invR2 = inv(R * R) - @inbounds for j in 1:N - if j != i - this_metric = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, j, invR2) - better = this_metric < nndist[i] - nndist[i] = better ? this_metric : nndist[i] - nni[i] = better ? j : nni[i] - if this_metric < nndist[j] - nndist[j] = this_metric - nni[j] = i - eereco.dijdist[j] = this_metric - val_beam = valencia_beam_distance_arrays(E2p, nz, j, γ, β) - if val_beam < eereco.dijdist[j] - eereco.dijdist[j] = val_beam - nni[j] = 0 - end - end - end - end - eereco.dijdist[i] = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, nni[i], invR2) - val_beam = valencia_beam_distance_arrays(E2p, nz, i, γ, β) - beam_close = val_beam < eereco.dijdist[i] - eereco.dijdist[i] = beam_close ? val_beam : eereco.dijdist[i] - nni[i] = beam_close ? 0 : nni[i] + eereco.nndist[i] = Inf else - nndist[i] = large_distance; nni[i] = i; E2p_i = E2p[i] - @inbounds for j in 1:N - if j != i - this_metric = angular_distance_arrays(nx, ny, nz, i, j) - better = this_metric < nndist[i] - nndist[i] = better ? this_metric : nndist[i] - nni[i] = better ? j : nni[i] - if this_metric < nndist[j] - nndist[j] = this_metric - nni[j] = i - E2p_j = E2p[j] - new_dij = (E2p_i < E2p_j ? E2p_i : E2p_j) * dij_factor * this_metric - if algorithm == JetAlgorithm.EEKt && E2p_j < new_dij - eereco.dijdist[j] = E2p_j - nni[j] = 0 - else - eereco.dijdist[j] = new_dij + eereco.nndist[i] = large_distance + end + eereco.nni[i] = i + # Scan all other jets, update i, and cross-update j + @inbounds for j in 1:N + if j != i + this_metric = algorithm == JetAlgorithm.Valencia ? + valencia_distance(eereco, i, j, R) : + angular_distance(eereco, i, j) + better_i = this_metric < eereco[i].nndist + eereco.nndist[i] = better_i ? this_metric : eereco.nndist[i] + eereco.nni[i] = better_i ? j : eereco.nni[i] + + if this_metric < eereco[j].nndist + eereco.nndist[j] = this_metric + eereco.nni[j] = i + eereco.dijdist[j] = dij_dist(eereco, j, i, dij_factor, algorithm, R) + if algorithm == JetAlgorithm.EEKt + if eereco[j].E2p < eereco[j].dijdist + eereco.dijdist[j] = eereco[j].E2p + eereco.nni[j] = 0 + end + elseif algorithm == JetAlgorithm.Valencia + val_beam_j = valencia_beam_distance(eereco, j, γ, β) + if val_beam_j < eereco[j].dijdist + eereco.dijdist[j] = val_beam_j + eereco.nni[j] = 0 end end end end - E2p_nni = E2p[nni[i]]; minE2p_i = E2p_i < E2p_nni ? E2p_i : E2p_nni - eereco.dijdist[i] = minE2p_i * dij_factor * nndist[i] - if algorithm == JetAlgorithm.EEKt - beam_close = E2p_i < eereco.dijdist[i] - eereco.dijdist[i] = beam_close ? E2p_i : eereco.dijdist[i] - nni[i] = beam_close ? 0 : nni[i] - end + end + # Set dij for i using unified dispatcher and apply beam checks + eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, algorithm, R) + if algorithm == JetAlgorithm.EEKt + beam_close = eereco[i].E2p < eereco[i].dijdist + eereco.dijdist[i] = beam_close ? eereco[i].E2p : eereco.dijdist[i] + eereco.nni[i] = beam_close ? 0 : eereco.nni[i] + elseif algorithm == JetAlgorithm.Valencia + val_beam_i = valencia_beam_distance(eereco, i, γ, β) + beam_close = val_beam_i < eereco[i].dijdist + eereco.dijdist[i] = beam_close ? val_beam_i : eereco.dijdist[i] + eereco.nni[i] = beam_close ? 0 : eereco.nni[i] end end @@ -345,131 +258,6 @@ function ee_check_consistency(clusterseq, eereco, N) @debug "Consistency check passed" end -################################################################################ -# Array-based nearest-neighbour update helpers (operate on raw vectors) -################################################################################ - -@inline function update_nn_no_cross_arrays!(nndist::AbstractVector, nni::AbstractVector, - nx::AbstractVector, ny::AbstractVector, - nz::AbstractVector, - E2p::AbstractVector, dijdist::AbstractVector, - i::Integer, N::Integer, - algorithm::JetAlgorithm.Algorithm, - dij_factor, β = 1.0, γ = 1.0, R = 4.0) - if algorithm == JetAlgorithm.Valencia - nndist[i] = Inf; nni[i] = i; invR2 = inv(R * R) - @inbounds for j in 1:N - if j != i - this_metric = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, j, invR2) - better = this_metric < nndist[i] - nndist[i] = better ? this_metric : nndist[i] - nni[i] = better ? j : nni[i] - end - end - dijdist[i] = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, nni[i], invR2) - val_beam = valencia_beam_distance_arrays(E2p, nz, i, γ, β) - beam_close = val_beam < dijdist[i] - dijdist[i] = beam_close ? val_beam : dijdist[i] - nni[i] = beam_close ? 0 : nni[i] - else - nndist[i] = large_distance; nni[i] = i; E2p_i = E2p[i] - @inbounds for j in 1:N - if j != i - this_metric = angular_distance_arrays(nx, ny, nz, i, j) - better = this_metric < nndist[i] - nndist[i] = better ? this_metric : nndist[i] - nni[i] = better ? j : nni[i] - end - end - E2p_nni = E2p[nni[i]]; minE2p = E2p_i < E2p_nni ? E2p_i : E2p_nni - dijdist[i] = minE2p * dij_factor * nndist[i] - if algorithm == JetAlgorithm.EEKt - beam_close = E2p_i < dijdist[i] - dijdist[i] = beam_close ? E2p_i : dijdist[i] - nni[i] = beam_close ? 0 : nni[i] - end - end -end - -@inline function update_nn_cross_arrays!(nndist::AbstractVector, nni::AbstractVector, - nx::AbstractVector, ny::AbstractVector, - nz::AbstractVector, - E2p::AbstractVector, dijdist::AbstractVector, - i::Integer, N::Integer, - algorithm::JetAlgorithm.Algorithm, - dij_factor, β = 1.0, γ = 1.0, R = 4.0) - if algorithm == JetAlgorithm.Valencia - nndist[i] = Inf; nni[i] = i; invR2 = inv(R * R) - @inbounds for j in 1:(i - 1) - this_metric = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, j, invR2) - better = this_metric < nndist[i] - nndist[i] = better ? this_metric : nndist[i] - nni[i] = better ? j : nni[i] - if this_metric < nndist[j] - nndist[j] = this_metric; nni[j] = i; dijdist[j] = this_metric - val_beam = valencia_beam_distance_arrays(E2p, nz, j, γ, β) - if val_beam < dijdist[j]; dijdist[j] = val_beam; nni[j] = 0; end - end - end - @inbounds for j in (i + 1):N - this_metric = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, j, invR2) - better = this_metric < nndist[i] - nndist[i] = better ? this_metric : nndist[i] - nni[i] = better ? j : nni[i] - if this_metric < nndist[j] - nndist[j] = this_metric; nni[j] = i; dijdist[j] = this_metric - val_beam = valencia_beam_distance_arrays(E2p, nz, j, γ, β) - if val_beam < dijdist[j]; dijdist[j] = val_beam; nni[j] = 0; end - end - end - dijdist[i] = valencia_distance_inv_arrays(E2p, nx, ny, nz, i, nni[i], invR2) - val_beam = valencia_beam_distance_arrays(E2p, nz, i, γ, β) - beam_close = val_beam < dijdist[i] - dijdist[i] = beam_close ? val_beam : dijdist[i] - nni[i] = beam_close ? 0 : nni[i] - else - nndist[i] = large_distance; nni[i] = i; E2p_i = E2p[i] - @inbounds for j in 1:(i - 1) - this_metric = angular_distance_arrays(nx, ny, nz, i, j) - better = this_metric < nndist[i] - nndist[i] = better ? this_metric : nndist[i] - nni[i] = better ? j : nni[i] - if this_metric < nndist[j] - nndist[j] = this_metric; nni[j] = i - E2p_j = E2p[j] - new_dij = (E2p_i < E2p_j ? E2p_i : E2p_j) * dij_factor * this_metric - if algorithm == JetAlgorithm.EEKt && E2p_j < new_dij - dijdist[j] = E2p_j; nni[j] = 0 - else - dijdist[j] = new_dij - end - end - end - @inbounds for j in (i + 1):N - this_metric = angular_distance_arrays(nx, ny, nz, i, j) - better = this_metric < nndist[i] - nndist[i] = better ? this_metric : nndist[i] - nni[i] = better ? j : nni[i] - if this_metric < nndist[j] - nndist[j] = this_metric; nni[j] = i - E2p_j = E2p[j] - minE2p = E2p_i < E2p_j ? E2p_i : E2p_j - dijdist[j] = minE2p * dij_factor * this_metric - if algorithm == JetAlgorithm.EEKt && E2p_j < dijdist[j] - dijdist[j] = E2p_j; nni[j] = 0 - end - end - end - E2p_nni = E2p[nni[i]]; minE2p_i = E2p_i < E2p_nni ? E2p_i : E2p_nni - dijdist[i] = minE2p_i * dij_factor * nndist[i] - if algorithm == JetAlgorithm.EEKt - beam_close = E2p_i < dijdist[i] - dijdist[i] = beam_close ? E2p_i : dijdist[i] - nni[i] = beam_close ? 0 : nni[i] - end - end -end - Base.@propagate_inbounds @inline function fill_reco_array!(eereco, particles, R2, p) @inbounds for i in eachindex(particles) eereco.index[i] = i @@ -612,8 +400,6 @@ function ee_genkt_algorithm(particles::AbstractVector{T}; algorithm::JetAlgorith algorithm = algorithm, recombine = recombine, γ = γ) end -## Removed Durham/Valencia specialised implementations; unified below. - """ _ee_genkt_algorithm!(particles::AbstractVector{EEJet}; algorithm::JetAlgorithm.Algorithm, p::Real, R = 4.0, From 589f5fad74940fd6af0a4f739bfd79cd9daea316 Mon Sep 17 00:00:00 2001 From: Matt LeBlanc Date: Sun, 24 Aug 2025 18:52:55 -0400 Subject: [PATCH 27/32] A couple more tweaks to docstrings, cleaning up tests --- src/EEAlgorithm.jl | 16 +++++++++++----- test/_common.jl | 2 +- test/test-valencia.jl | 3 +-- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/EEAlgorithm.jl b/src/EEAlgorithm.jl index 0a0c46cd..428eff30 100644 --- a/src/EEAlgorithm.jl +++ b/src/EEAlgorithm.jl @@ -316,7 +316,7 @@ end """ ee_genkt_algorithm(particles::AbstractVector{T}; algorithm::JetAlgorithm.Algorithm, p::Union{Real, Nothing} = nothing, R = 4.0, recombine = addjets, - preprocess = nothing, γ::Real = 1.0) where {T} + preprocess = nothing, γ::Real = 1.0, β::Union{Real, Nothing} = nothing) where {T} Run an e+e- reconstruction algorithm on a set of initial particles. @@ -332,6 +332,8 @@ Run an e+e- reconstruction algorithm on a set of initial particles. - `recombine`: The recombination scheme to use. - `preprocess`: Preprocessing function for input particles. - `γ::Real = 1.0`: The angular exponent parameter for Valencia algorithm. Ignored for other algorithms. +- `β::Union{Real, Nothing} = nothing`: Optional alias for the Valencia energy exponent; if provided for + Valencia it overrides `p`. # Returns - The result of the jet clustering as a `ClusterSequence` object. @@ -344,7 +346,7 @@ itself, and call the actual reconstruction method `_ee_genkt_algorithm!`. If the algorithm is Durham, `R` is nominally set to 4. If the algorithm is EEkt, power `p` must be specified. -If the algorithm is Valencia, both `p` (β) and `γ` should be specified. +If the algorithm is Valencia, you can provide `p` (β) and `γ`, or pass `β` explicitly to override `p`. """ function ee_genkt_algorithm(particles::AbstractVector{T}; algorithm::JetAlgorithm.Algorithm, p::Union{Real, Nothing} = nothing, R = 4.0, recombine = addjets, @@ -401,9 +403,10 @@ function ee_genkt_algorithm(particles::AbstractVector{T}; algorithm::JetAlgorith end """ - _ee_genkt_algorithm!(particles::AbstractVector{EEJet}; + _ee_genkt_algorithm(particles::AbstractVector{EEJet}; algorithm::JetAlgorithm.Algorithm, p::Real, R = 4.0, - recombine = addjets, γ::Real = 1.0) + recombine = addjets, γ::Real = 1.0, + beta::Union{Real, Nothing} = nothing) This function is the internal implementation of the e+e- jet clustering algorithm. It takes a vector of `EEJet` `particles` representing the input @@ -413,7 +416,7 @@ Users of the package should use the `ee_genkt_algorithm` function as their entry point to this jet reconstruction. # Arguments -- `particles::AbstractVector{EEJet}`: A vector of `EEJet` particles used + - `particles::AbstractVector{EEJet}`: A vector of `EEJet` particles used as input for jet reconstruction. This vector must supply the correct `cluster_hist_index` values and will be *mutated* as part of the returned `ClusterSequence`. @@ -422,6 +425,9 @@ entry point to this jet reconstruction. is raised. - `R = 4.0`: The jet radius parameter. - `recombine = addjets`: The recombination function used to merge two jets. + - `γ::Real = 1.0`: Angular exponent for the Valencia beam metric (ignored for other algorithms). + - `beta::Union{Real, Nothing} = nothing`: Optional alias for the Valencia energy exponent (β). + When provided with `algorithm == JetAlgorithm.Valencia`, it overrides `p`. # Returns - `clusterseq`: The resulting `ClusterSequence` object representing the diff --git a/test/_common.jl b/test/_common.jl index 5fb13c1e..2a783fb7 100644 --- a/test/_common.jl +++ b/test/_common.jl @@ -14,7 +14,7 @@ using StructArrays using JetReconstruction: EERecoJet, fill_reco_array!, insert_new_jet!, copy_to_slot!, - dij_dist, valencia_distance, valencia_distance_inv + dij_dist, valencia_distance logger = ConsoleLogger(stdout, Logging.Warn) global_logger(logger) diff --git a/test/test-valencia.jl b/test/test-valencia.jl index 8431cd11..35e19387 100644 --- a/test/test-valencia.jl +++ b/test/test-valencia.jl @@ -80,8 +80,7 @@ end eereco = EERecoJet[EERecoJet(1, 0, Inf, Inf, 1.0, 0.0, 0.0, 9.0), EERecoJet(2, 0, Inf, Inf, 1.0, 0.0, 0.0, 4.0)] R = 2.0 - invR2 = inv(R * R) - @test valencia_distance_inv(eereco, 1, 2, invR2) == 0.0 + @test valencia_distance(eereco, 1, 2, R) == 0.0 @test valencia_distance(eereco, 1, 2, R) == 0.0 end From cad70b9f7ccc6367e097e2df1dcf961621aed64d Mon Sep 17 00:00:00 2001 From: Matt LeBlanc Date: Sun, 24 Aug 2025 18:54:55 -0400 Subject: [PATCH 28/32] Julia formatter --- src/EEAlgorithm.jl | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/EEAlgorithm.jl b/src/EEAlgorithm.jl index 428eff30..280dfa28 100644 --- a/src/EEAlgorithm.jl +++ b/src/EEAlgorithm.jl @@ -29,7 +29,7 @@ Compute dij distance for (Durham, EEKt, Valencia) using simple conditionals. Beam index `j==0` returns a large sentinel. For Valencia we use the full Valencia metric (independent of `dij_factor`). """ -@inline function dij_dist(eereco, i, j, dij_factor, algorithm, R=4.0) +@inline function dij_dist(eereco, i, j, dij_factor, algorithm, R = 4.0) if !(algorithm isa JetAlgorithm.Algorithm) throw(ArgumentError("algorithm must be a JetAlgorithm.Algorithm")) end @@ -62,11 +62,10 @@ Calculate the Valencia distance between two jets `i` and `j` as Base.@propagate_inbounds @inline function valencia_distance(eereco, i, j, R) invR2 = inv(R * R) @muladd angular_dist = 1.0 - eereco[i].nx * eereco[j].nx - eereco[i].ny * eereco[j].ny - - eereco[i].nz * eereco[j].nz + eereco[i].nz * eereco[j].nz return min(eereco[i].E2p, eereco[j].E2p) * 2 * angular_dist * invR2 end - """ valencia_beam_distance(eereco, i, γ, β) -> Float64 @@ -202,8 +201,8 @@ end @inbounds for j in 1:N if j != i this_metric = algorithm == JetAlgorithm.Valencia ? - valencia_distance(eereco, i, j, R) : - angular_distance(eereco, i, j) + valencia_distance(eereco, i, j, R) : + angular_distance(eereco, i, j) better_i = this_metric < eereco[i].nndist eereco.nndist[i] = better_i ? this_metric : eereco.nndist[i] eereco.nni[i] = better_i ? j : eereco.nni[i] From 62d9bd5509b8d0e781c795e3a5e68f79224f0019 Mon Sep 17 00:00:00 2001 From: Matt LeBlanc Date: Wed, 27 Aug 2025 14:42:44 -0400 Subject: [PATCH 29/32] Fix enums, pass R2 as invR2 throughout --- src/AlgorithmStrategyEnums.jl | 3 +- src/EEAlgorithm.jl | 77 ++++++++++++++++++----------------- 2 files changed, 40 insertions(+), 40 deletions(-) diff --git a/src/AlgorithmStrategyEnums.jl b/src/AlgorithmStrategyEnums.jl index 1a7f26dc..757eea2e 100644 --- a/src/AlgorithmStrategyEnums.jl +++ b/src/AlgorithmStrategyEnums.jl @@ -47,8 +47,7 @@ A dictionary that maps algorithm names to their corresponding power values. const algorithm2power = Dict(JetAlgorithm.AntiKt => -1, JetAlgorithm.CA => 0, JetAlgorithm.Kt => 1, - JetAlgorithm.Durham => 1, - JetAlgorithm.Valencia => 1) + JetAlgorithm.Durham => 1) """ get_algorithm_power(; algorithm::JetAlgorithm.Algorithm, p::Union{Real, Nothing}) -> Real diff --git a/src/EEAlgorithm.jl b/src/EEAlgorithm.jl index 280dfa28..3299d60a 100644 --- a/src/EEAlgorithm.jl +++ b/src/EEAlgorithm.jl @@ -23,20 +23,17 @@ Calculate the angular distance between two jets `i` and `j` using the formula end """ - dij_dist(eereco, i, j, dij_factor, algorithm::JetAlgorithm.Algorithm, R=4.0) + dij_dist(eereco, i, j, dij_factor, algorithm::JetAlgorithm.Algorithm, invR2) Compute dij distance for (Durham, EEKt, Valencia) using simple conditionals. Beam index `j==0` returns a large sentinel. For Valencia we use the full Valencia metric (independent of `dij_factor`). """ -@inline function dij_dist(eereco, i, j, dij_factor, algorithm, R = 4.0) - if !(algorithm isa JetAlgorithm.Algorithm) - throw(ArgumentError("algorithm must be a JetAlgorithm.Algorithm")) - end +@inline function dij_dist(eereco, i, j, dij_factor, algorithm, invR2) j == 0 && return large_dij @inbounds begin if algorithm == JetAlgorithm.Valencia - return valencia_distance(eereco, i, j, R) + return valencia_distance(eereco, i, j, invR2) else # Durham & EEKt share same form here (min(E2p_i,E2p_j) * dij_factor * angular_metric) return min(eereco[i].E2p, eereco[j].E2p) * dij_factor * eereco[i].nndist @@ -45,22 +42,21 @@ Valencia metric (independent of `dij_factor`). end """ - valencia_distance(eereco, i, j, R) -> Float64 + valencia_distance(eereco, i, j, invR2) -> Float64 Calculate the Valencia distance between two jets `i` and `j` as -``min(E_i^{2β}, E_j^{2β}) * 2 * (1 - cos(θ_{ij})) / R²``. +``min(E_i^{2β}, E_j^{2β}) * 2 * (1 - cos(θ_{ij})) * invR2``. # Arguments - `eereco`: The array of `EERecoJet` objects. - `i`: The first jet. - `j`: The second jet. -- `R`: The jet radius parameter. +- `invR2`: The inverse square of the radius, i.e. ``1 / R^2``. # Returns - `Float64`: The Valencia distance between `i` and `j`. """ -Base.@propagate_inbounds @inline function valencia_distance(eereco, i, j, R) - invR2 = inv(R * R) +Base.@propagate_inbounds @inline function valencia_distance(eereco, i, j, invR2) @muladd angular_dist = 1.0 - eereco[i].nx * eereco[j].nx - eereco[i].ny * eereco[j].ny - eereco[i].nz * eereco[j].nz return min(eereco[i].E2p, eereco[j].E2p) * 2 * angular_dist * invR2 @@ -96,7 +92,7 @@ Base.@propagate_inbounds @inline function valencia_beam_distance(eereco, i, γ, end end -function get_angular_nearest_neighbours!(eereco, algorithm, dij_factor, p, γ = 1.0, R = 4.0) +function get_angular_nearest_neighbours!(eereco, algorithm, dij_factor, p, invR2, γ = 1.0) # Get the initial nearest neighbours for each jet N = length(eereco) # Initialise sentinels so the first comparison always wins @@ -114,7 +110,7 @@ function get_angular_nearest_neighbours!(eereco, algorithm, dij_factor, p, γ = # Metric used to pick the nearest neighbour if algorithm == JetAlgorithm.Valencia # Use canonical Valencia distance (StructArray-aware) - this_metric = valencia_distance(eereco, i, j, R) + this_metric = valencia_distance(eereco, i, j, invR2) else this_metric = angular_distance(eereco, i, j) end @@ -131,9 +127,9 @@ function get_angular_nearest_neighbours!(eereco, algorithm, dij_factor, p, γ = # Nearest neighbour dij distance @inbounds for i in 1:N if algorithm == JetAlgorithm.Valencia - eereco.dijdist[i] = valencia_distance(eereco, i, eereco[i].nni, R) + eereco.dijdist[i] = valencia_distance(eereco, i, eereco[i].nni, invR2) else - eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, algorithm, R) + eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, algorithm, invR2) end end # For the EEKt and Valencia algorithms, we need to check the beam distance as well @@ -155,7 +151,7 @@ function get_angular_nearest_neighbours!(eereco, algorithm, dij_factor, p, γ = end @inline function update_nn_no_cross!(eereco, i, N, algorithm::JetAlgorithm.Algorithm, - dij_factor, β = 1.0, γ = 1.0, R = 4.0) + dij_factor, invR2, β = 1.0, γ = 1.0) # Valencia metric is unbounded, others use a large finite value if algorithm == JetAlgorithm.Valencia eereco.nndist[i] = Inf @@ -167,7 +163,7 @@ end @inbounds for j in 1:N if j != i this_metric = algorithm == JetAlgorithm.Valencia ? - valencia_distance(eereco, i, j, R) : + valencia_distance(eereco, i, j, invR2) : angular_distance(eereco, i, j) better_i = this_metric < eereco[i].nndist eereco.nndist[i] = better_i ? this_metric : eereco.nndist[i] @@ -175,7 +171,7 @@ end end end # Set dij for i using unified dispatcher and apply beam checks - eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, algorithm, R) + eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, algorithm, invR2) if algorithm == JetAlgorithm.EEKt beam_close = eereco[i].E2p < eereco[i].dijdist eereco.dijdist[i] = beam_close ? eereco[i].E2p : eereco.dijdist[i] @@ -189,7 +185,7 @@ end end @inline function update_nn_cross!(eereco, i, N, algorithm::JetAlgorithm.Algorithm, - dij_factor, β = 1.0, γ = 1.0, R = 4.0) + dij_factor, invR2, β = 1.0, γ = 1.0) # Valencia metric is unbounded, others use a large finite value if algorithm == JetAlgorithm.Valencia eereco.nndist[i] = Inf @@ -201,7 +197,7 @@ end @inbounds for j in 1:N if j != i this_metric = algorithm == JetAlgorithm.Valencia ? - valencia_distance(eereco, i, j, R) : + valencia_distance(eereco, i, j, invR2) : angular_distance(eereco, i, j) better_i = this_metric < eereco[i].nndist eereco.nndist[i] = better_i ? this_metric : eereco.nndist[i] @@ -210,7 +206,7 @@ end if this_metric < eereco[j].nndist eereco.nndist[j] = this_metric eereco.nni[j] = i - eereco.dijdist[j] = dij_dist(eereco, j, i, dij_factor, algorithm, R) + eereco.dijdist[j] = dij_dist(eereco, j, i, dij_factor, algorithm, invR2) if algorithm == JetAlgorithm.EEKt if eereco[j].E2p < eereco[j].dijdist eereco.dijdist[j] = eereco[j].E2p @@ -227,7 +223,7 @@ end end end # Set dij for i using unified dispatcher and apply beam checks - eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, algorithm, R) + eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, algorithm, invR2) if algorithm == JetAlgorithm.EEKt beam_close = eereco[i].E2p < eereco[i].dijdist eereco.dijdist[i] = beam_close ? eereco[i].E2p : eereco.dijdist[i] @@ -257,11 +253,11 @@ function ee_check_consistency(clusterseq, eereco, N) @debug "Consistency check passed" end -Base.@propagate_inbounds @inline function fill_reco_array!(eereco, particles, R2, p) +Base.@propagate_inbounds @inline function fill_reco_array!(eereco, particles, invR2, p) @inbounds for i in eachindex(particles) eereco.index[i] = i eereco.nni[i] = 0 - eereco.nndist[i] = R2 + eereco.nndist[i] = inv(invR2) # R^2 as initial sentinel for angular algorithms # eereco.dijdist[i] = UNDEF # Does not need to be initialised eereco.nx[i] = nx(particles[i]) eereco.ny[i] = ny(particles[i]) @@ -271,12 +267,12 @@ Base.@propagate_inbounds @inline function fill_reco_array!(eereco, particles, R2 end end -Base.@propagate_inbounds @inline function insert_new_jet!(eereco, i, newjet_k, R2, - merged_jet, p) +Base.@propagate_inbounds @inline function insert_new_jet!(eereco, i, newjet_k, invR2, + merged_jet, p) @inbounds begin eereco.index[i] = newjet_k eereco.nni[i] = 0 - eereco.nndist[i] = R2 + eereco.nndist[i] = inv(invR2) eereco.nx[i] = nx(merged_jet) eereco.ny[i] = ny(merged_jet) eereco.nz[i] = nz(merged_jet) @@ -396,15 +392,17 @@ function ee_genkt_algorithm(particles::AbstractVector{T}; algorithm::JetAlgorith end end + # Compute invR2 once and thread it through + invR2 = inv(R * R) # Now call the unified implementation with conditional logic. return _ee_genkt_algorithm(particles = recombination_particles, p = p, R = R, - algorithm = algorithm, recombine = recombine, γ = γ) + invR2 = invR2, algorithm = algorithm, recombine = recombine, γ = γ) end """ _ee_genkt_algorithm(particles::AbstractVector{EEJet}; - algorithm::JetAlgorithm.Algorithm, p::Real, R = 4.0, - recombine = addjets, γ::Real = 1.0, + algorithm::JetAlgorithm.Algorithm, p::Real, R::Real, + invR2::Union{Real, Nothing} = nothing, recombine = addjets, γ::Real = 1.0, beta::Union{Real, Nothing} = nothing) This function is the internal implementation of the e+e- jet clustering @@ -433,13 +431,16 @@ entry point to this jet reconstruction. reconstructed jets. """ function _ee_genkt_algorithm(; particles::AbstractVector{EEJet}, - algorithm::JetAlgorithm.Algorithm, p::Real, R = 4.0, - recombine = addjets, γ::Real = 1.0, + algorithm::JetAlgorithm.Algorithm, p::Real, R::Real, + invR2::Union{Real, Nothing} = nothing, recombine = addjets, γ::Real = 1.0, beta::Union{Real, Nothing} = nothing) # Bounds N::Int = length(particles) - R2 = R^2 + # invR2 provided by caller when available; otherwise compute from R once here + if invR2 === nothing + invR2 = inv(R * R) + end if algorithm == JetAlgorithm.Valencia && beta !== nothing p = beta end @@ -464,7 +465,7 @@ function _ee_genkt_algorithm(; particles::AbstractVector{EEJet}, # We need N slots for this array eereco = StructArray{EERecoJet}(undef, N) - fill_reco_array!(eereco, particles, R2, p) + fill_reco_array!(eereco, particles, invR2, p) # Setup the initial history and get the total energy history, Qtot = initial_history(particles) @@ -473,7 +474,7 @@ function _ee_genkt_algorithm(; particles::AbstractVector{EEJet}, Qtot) # Run over initial pairs of jets to find nearest neighbours - get_angular_nearest_neighbours!(eereco, algorithm, dij_factor, p, γ, R) + get_angular_nearest_neighbours!(eereco, algorithm, dij_factor, p, invR2, γ) # Only for debugging purposes... # ee_check_consistency(clusterseq, clusterseq_index, N, nndist, nndij, nni, "Start") @@ -522,7 +523,7 @@ function _ee_genkt_algorithm(; particles::AbstractVector{EEJet}, newjet_k, dij_min) # Update the compact arrays, reusing the JetA slot - insert_new_jet!(eereco, ijetA, newjet_k, R2, merged_jet, p) + insert_new_jet!(eereco, ijetA, newjet_k, invR2, merged_jet, p) end # Squash step - copy the final jet's compact data into the jetB slot @@ -544,7 +545,7 @@ function _ee_genkt_algorithm(; particles::AbstractVector{EEJet}, # plus "belt and braces" check for an invalid NN (>N) if (eereco[i].nni == ijetA) || (eereco[i].nni == ijetB) || (eereco[i].nni > N) - update_nn_no_cross!(eereco, i, N, algorithm, dij_factor, p, γ, R) + update_nn_no_cross!(eereco, i, N, algorithm, dij_factor, invR2, p, γ) end end end @@ -552,7 +553,7 @@ function _ee_genkt_algorithm(; particles::AbstractVector{EEJet}, # Finally, we need to update the nearest neighbours for the new jet, checking both ways # (But only if there was a new jet!) if ijetA != ijetB - update_nn_cross!(eereco, ijetA, N, algorithm, dij_factor, p, γ, R) + update_nn_cross!(eereco, ijetA, N, algorithm, dij_factor, invR2, p, γ) end # Only for debugging purposes... From 7d1158c0436cc7a8368b9fa40b1e7ea98eb186e9 Mon Sep 17 00:00:00 2001 From: Matt LeBlanc Date: Wed, 27 Aug 2025 14:43:08 -0400 Subject: [PATCH 30/32] julia formatter --- src/EEAlgorithm.jl | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/EEAlgorithm.jl b/src/EEAlgorithm.jl index 3299d60a..d3c02180 100644 --- a/src/EEAlgorithm.jl +++ b/src/EEAlgorithm.jl @@ -129,7 +129,8 @@ function get_angular_nearest_neighbours!(eereco, algorithm, dij_factor, p, invR2 if algorithm == JetAlgorithm.Valencia eereco.dijdist[i] = valencia_distance(eereco, i, eereco[i].nni, invR2) else - eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, algorithm, invR2) + eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, algorithm, + invR2) end end # For the EEKt and Valencia algorithms, we need to check the beam distance as well @@ -257,7 +258,7 @@ Base.@propagate_inbounds @inline function fill_reco_array!(eereco, particles, in @inbounds for i in eachindex(particles) eereco.index[i] = i eereco.nni[i] = 0 - eereco.nndist[i] = inv(invR2) # R^2 as initial sentinel for angular algorithms + eereco.nndist[i] = inv(invR2) # R^2 as initial sentinel for angular algorithms # eereco.dijdist[i] = UNDEF # Does not need to be initialised eereco.nx[i] = nx(particles[i]) eereco.ny[i] = ny(particles[i]) @@ -268,11 +269,11 @@ Base.@propagate_inbounds @inline function fill_reco_array!(eereco, particles, in end Base.@propagate_inbounds @inline function insert_new_jet!(eereco, i, newjet_k, invR2, - merged_jet, p) + merged_jet, p) @inbounds begin eereco.index[i] = newjet_k eereco.nni[i] = 0 - eereco.nndist[i] = inv(invR2) + eereco.nndist[i] = inv(invR2) eereco.nx[i] = nx(merged_jet) eereco.ny[i] = ny(merged_jet) eereco.nz[i] = nz(merged_jet) @@ -396,7 +397,8 @@ function ee_genkt_algorithm(particles::AbstractVector{T}; algorithm::JetAlgorith invR2 = inv(R * R) # Now call the unified implementation with conditional logic. return _ee_genkt_algorithm(particles = recombination_particles, p = p, R = R, - invR2 = invR2, algorithm = algorithm, recombine = recombine, γ = γ) + invR2 = invR2, algorithm = algorithm, recombine = recombine, + γ = γ) end """ @@ -432,7 +434,8 @@ entry point to this jet reconstruction. """ function _ee_genkt_algorithm(; particles::AbstractVector{EEJet}, algorithm::JetAlgorithm.Algorithm, p::Real, R::Real, - invR2::Union{Real, Nothing} = nothing, recombine = addjets, γ::Real = 1.0, + invR2::Union{Real, Nothing} = nothing, recombine = addjets, + γ::Real = 1.0, beta::Union{Real, Nothing} = nothing) # Bounds N::Int = length(particles) From 4ce340e09b172a077054a0eba8bf9ffb57301460 Mon Sep 17 00:00:00 2001 From: Matt LeBlanc Date: Wed, 27 Aug 2025 14:46:50 -0400 Subject: [PATCH 31/32] Add back alg isa JetAlgorithm.Algorithm check for now --- src/EEAlgorithm.jl | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/EEAlgorithm.jl b/src/EEAlgorithm.jl index d3c02180..76518c4e 100644 --- a/src/EEAlgorithm.jl +++ b/src/EEAlgorithm.jl @@ -30,6 +30,9 @@ Beam index `j==0` returns a large sentinel. For Valencia we use the full Valencia metric (independent of `dij_factor`). """ @inline function dij_dist(eereco, i, j, dij_factor, algorithm, invR2) + if !(algorithm isa JetAlgorithm.Algorithm) + throw(ArgumentError("algorithm must be a JetAlgorithm.Algorithm")) + end j == 0 && return large_dij @inbounds begin if algorithm == JetAlgorithm.Valencia @@ -129,8 +132,7 @@ function get_angular_nearest_neighbours!(eereco, algorithm, dij_factor, p, invR2 if algorithm == JetAlgorithm.Valencia eereco.dijdist[i] = valencia_distance(eereco, i, eereco[i].nni, invR2) else - eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, algorithm, - invR2) + eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, algorithm, invR2) end end # For the EEKt and Valencia algorithms, we need to check the beam distance as well @@ -258,7 +260,7 @@ Base.@propagate_inbounds @inline function fill_reco_array!(eereco, particles, in @inbounds for i in eachindex(particles) eereco.index[i] = i eereco.nni[i] = 0 - eereco.nndist[i] = inv(invR2) # R^2 as initial sentinel for angular algorithms + eereco.nndist[i] = inv(invR2) # R^2 as initial sentinel for angular algorithms # eereco.dijdist[i] = UNDEF # Does not need to be initialised eereco.nx[i] = nx(particles[i]) eereco.ny[i] = ny(particles[i]) @@ -269,11 +271,11 @@ Base.@propagate_inbounds @inline function fill_reco_array!(eereco, particles, in end Base.@propagate_inbounds @inline function insert_new_jet!(eereco, i, newjet_k, invR2, - merged_jet, p) + merged_jet, p) @inbounds begin eereco.index[i] = newjet_k eereco.nni[i] = 0 - eereco.nndist[i] = inv(invR2) + eereco.nndist[i] = inv(invR2) eereco.nx[i] = nx(merged_jet) eereco.ny[i] = ny(merged_jet) eereco.nz[i] = nz(merged_jet) @@ -397,8 +399,7 @@ function ee_genkt_algorithm(particles::AbstractVector{T}; algorithm::JetAlgorith invR2 = inv(R * R) # Now call the unified implementation with conditional logic. return _ee_genkt_algorithm(particles = recombination_particles, p = p, R = R, - invR2 = invR2, algorithm = algorithm, recombine = recombine, - γ = γ) + invR2 = invR2, algorithm = algorithm, recombine = recombine, γ = γ) end """ @@ -434,8 +435,7 @@ entry point to this jet reconstruction. """ function _ee_genkt_algorithm(; particles::AbstractVector{EEJet}, algorithm::JetAlgorithm.Algorithm, p::Real, R::Real, - invR2::Union{Real, Nothing} = nothing, recombine = addjets, - γ::Real = 1.0, + invR2::Union{Real, Nothing} = nothing, recombine = addjets, γ::Real = 1.0, beta::Union{Real, Nothing} = nothing) # Bounds N::Int = length(particles) From c4ec5ccd742756ab117adf0db2228f70079ccfdc Mon Sep 17 00:00:00 2001 From: Matt LeBlanc Date: Wed, 27 Aug 2025 14:47:08 -0400 Subject: [PATCH 32/32] julia formatter --- src/EEAlgorithm.jl | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/EEAlgorithm.jl b/src/EEAlgorithm.jl index 76518c4e..abc714fd 100644 --- a/src/EEAlgorithm.jl +++ b/src/EEAlgorithm.jl @@ -132,7 +132,8 @@ function get_angular_nearest_neighbours!(eereco, algorithm, dij_factor, p, invR2 if algorithm == JetAlgorithm.Valencia eereco.dijdist[i] = valencia_distance(eereco, i, eereco[i].nni, invR2) else - eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, algorithm, invR2) + eereco.dijdist[i] = dij_dist(eereco, i, eereco[i].nni, dij_factor, algorithm, + invR2) end end # For the EEKt and Valencia algorithms, we need to check the beam distance as well @@ -260,7 +261,7 @@ Base.@propagate_inbounds @inline function fill_reco_array!(eereco, particles, in @inbounds for i in eachindex(particles) eereco.index[i] = i eereco.nni[i] = 0 - eereco.nndist[i] = inv(invR2) # R^2 as initial sentinel for angular algorithms + eereco.nndist[i] = inv(invR2) # R^2 as initial sentinel for angular algorithms # eereco.dijdist[i] = UNDEF # Does not need to be initialised eereco.nx[i] = nx(particles[i]) eereco.ny[i] = ny(particles[i]) @@ -271,11 +272,11 @@ Base.@propagate_inbounds @inline function fill_reco_array!(eereco, particles, in end Base.@propagate_inbounds @inline function insert_new_jet!(eereco, i, newjet_k, invR2, - merged_jet, p) + merged_jet, p) @inbounds begin eereco.index[i] = newjet_k eereco.nni[i] = 0 - eereco.nndist[i] = inv(invR2) + eereco.nndist[i] = inv(invR2) eereco.nx[i] = nx(merged_jet) eereco.ny[i] = ny(merged_jet) eereco.nz[i] = nz(merged_jet) @@ -399,7 +400,8 @@ function ee_genkt_algorithm(particles::AbstractVector{T}; algorithm::JetAlgorith invR2 = inv(R * R) # Now call the unified implementation with conditional logic. return _ee_genkt_algorithm(particles = recombination_particles, p = p, R = R, - invR2 = invR2, algorithm = algorithm, recombine = recombine, γ = γ) + invR2 = invR2, algorithm = algorithm, recombine = recombine, + γ = γ) end """ @@ -435,7 +437,8 @@ entry point to this jet reconstruction. """ function _ee_genkt_algorithm(; particles::AbstractVector{EEJet}, algorithm::JetAlgorithm.Algorithm, p::Real, R::Real, - invR2::Union{Real, Nothing} = nothing, recombine = addjets, γ::Real = 1.0, + invR2::Union{Real, Nothing} = nothing, recombine = addjets, + γ::Real = 1.0, beta::Union{Real, Nothing} = nothing) # Bounds N::Int = length(particles)