diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 2dd605dee2..e5bda88eb6 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -87,7 +87,10 @@ traccc_add_library( traccc_core core TYPE SHARED "include/traccc/seeding/seed_finding.hpp" "src/seeding/seed_finding.cpp" "include/traccc/seeding/spacepoint_binning.hpp" - "src/seeding/spacepoint_binning.cpp" ) + "src/seeding/spacepoint_binning.cpp" + # Ambiguity resolution + "include/traccc/ambiguity_resolution/greedy_ambiguity_resolution_algorithm.hpp" + "src/ambiguity_resolution/greedy_ambiguity_resolution_algorithm.cpp" ) target_link_libraries( traccc_core PUBLIC Eigen3::Eigen vecmem::core detray::core traccc::Thrust traccc::algebra ) diff --git a/core/include/traccc/ambiguity_resolution/greedy_ambiguity_resolution_algorithm.hpp b/core/include/traccc/ambiguity_resolution/greedy_ambiguity_resolution_algorithm.hpp new file mode 100644 index 0000000000..08de5833b8 --- /dev/null +++ b/core/include/traccc/ambiguity_resolution/greedy_ambiguity_resolution_algorithm.hpp @@ -0,0 +1,155 @@ +/** TRACCC library, part of the ACTS project (R&D line) + * + * (c) 2024 CERN for the benefit of the ACTS project + * + * Mozilla Public License Version 2.0 + */ + +#pragma once + +// System include +#include +#include +#include +#include +#include +#include +#include +#include + +// VecMem include(s). +#include + +// Project include(s). +#include "traccc/definitions/qualifiers.hpp" +#include "traccc/edm/track_candidate.hpp" +#include "traccc/edm/track_state.hpp" +#include "traccc/utils/algorithm.hpp" + +// Greedy ambiguity resolution adapted from ACTS code + +namespace traccc { + +/// Evicts tracks that seem to be duplicates or fakes. This algorithm takes a +/// greedy approach in the sense that it will remove the track which looks "most +/// duplicate/fake" first and continues the same process with the rest. That +/// process continues until the final state conditions are met. +/// +/// The implementation works as follows: +/// 1) Calculate shared hits per track. +/// 2) If the maximum shared hits criteria is met, we are done. +/// This is the configurable amount of shared hits we are ok with +/// in our experiment. +/// 3) Else, remove the track with the highest relative shared hits (i.e. +/// shared hits / hits). +/// 4) Back to square 1. +class greedy_ambiguity_resolution_algorithm + : public algorithm { + + public: + struct config_t { + + config_t(){}; + + /// Maximum amount of shared hits per track. One (1) means "no shared + /// hit allowed". + std::uint32_t maximum_shared_hits = 1; + + /// Maximum number of iterations. + std::uint32_t maximum_iterations = 1000000; + + /// Minimum number of measurement to form a track. + std::size_t n_measurements_min = 3; + + // True if obvious errors should be checked after the completion + // of the algorithm. + bool check_obvious_errs = true; + + bool verbose_info = true; + bool verbose_error = true; + bool verbose_flood = false; + }; + + struct state_t { + std::size_t number_of_tracks{}; + + /// For this whole comment section, track_index refers to the index of a + /// track in the initial input container. + /// + /// There is no (track_id) in this algorithm, only (track_index). + + /// Associates each track_index with the track's chi2 value + std::vector track_chi2; + + /// Associates each track_index to the track's (measurement_id)s list + std::vector> measurements_per_track; + + /// Associates each measurement_id to a set of (track_index)es sharing + /// it + std::unordered_map> + tracks_per_measurement; + + /// Associates each track_index to its number of shared measurements + /// (among other tracks) + std::vector shared_measurements_per_track; + + /// Keeps the selected tracks indexes that have not (yet) been removed + /// by the algorithm + std::set selected_tracks; + }; + + /// Constructor for the greedy ambiguity resolution algorithm + /// + /// @param cfg Configuration object + // greedy_ambiguity_resolution_algorithm(const config_type& cfg) : + // _config(cfg) {} + greedy_ambiguity_resolution_algorithm(const config_t cfg = {}) + : _config{cfg} {} + + /// Run the algorithm + /// + /// @param track_states the container of the fitted track parameters + /// @return the container without ambiguous tracks + track_state_container_types::host operator()( + const typename track_state_container_types::host& track_states) + const override; + + private: + /// Computes the initial state for the input data. This function accumulates + /// information that will later be used to accelerate the ambiguity + /// resolution. + /// + /// @param track_states The input track container (output of the fitting + /// algorithm). + /// @param state An empty state object which is expected to be default + /// constructed. + void compute_initial_state( + const typename track_state_container_types::host& track_states, + state_t& state) const; + + /// Updates the state iteratively by evicting one track after the other + /// until the final state conditions are met. + /// + /// @param state A state object that was previously filled by the + /// initialization. + void resolve(state_t& state) const; + + /// Check for obvious errors returned by the algorithm: + /// - Returned tracks should be independent of each other: they should share + /// a maximum of (_config.maximum_shared_hits - 1) hits per track. + /// - Each removed track should share at least (_config.maximum_shared_hits) + /// with another initial track. + /// + /// @param initial_track_states The input track container, as given to + /// compute_initial_state. + /// @param final_state The state object after the resolve method has been + /// called. + bool check_obvious_errors( + const typename track_state_container_types::host& initial_track_states, + state_t& final_state) const; + + config_t _config; +}; + +} // namespace traccc diff --git a/core/include/traccc/edm/measurement.hpp b/core/include/traccc/edm/measurement.hpp index ab602fc361..35ca496e63 100644 --- a/core/include/traccc/edm/measurement.hpp +++ b/core/include/traccc/edm/measurement.hpp @@ -39,6 +39,9 @@ struct measurement { /// Geometry ID detray::geometry::barcode surface_link; + // Unique measurement ID + std::size_t measurement_id = 0; + /// Link to Module vector index using link_type = cell_module_collection_types::view::size_type; link_type module_link = 0; diff --git a/core/src/ambiguity_resolution/greedy_ambiguity_resolution_algorithm.cpp b/core/src/ambiguity_resolution/greedy_ambiguity_resolution_algorithm.cpp new file mode 100644 index 0000000000..1078ee3fc4 --- /dev/null +++ b/core/src/ambiguity_resolution/greedy_ambiguity_resolution_algorithm.cpp @@ -0,0 +1,428 @@ +/** TRACCC library, part of the ACTS project (R&D line) + * + * (c) 2024 CERN for the benefit of the ACTS project + * + * Mozilla Public License Version 2.0 + */ + +#include "traccc/ambiguity_resolution/greedy_ambiguity_resolution_algorithm.hpp" + +// System include +#include +#include +#include +#include +#include +#include +#include +#include + +// VecMem include(s). +#include + +// Project include(s). +#include "traccc/definitions/qualifiers.hpp" +#include "traccc/edm/track_candidate.hpp" +#include "traccc/edm/track_state.hpp" +#include "traccc/utils/algorithm.hpp" + +// Greedy ambiguity resolution adapted from ACTS code + +namespace traccc { + +namespace { + +#define LOG_INFO(msg) \ + if (_config.verbose_info) { \ + logger::info() << msg << std::endl; \ + } + +#define LOG_FLOOD(msg) \ + if (_config.verbose_flood) { \ + logger::flood() << msg << std::endl; \ + } + +#define LOG_ERROR(msg) \ + if (_config.verbose_error) { \ + logger::error() << msg << std::endl; \ + } + +// This logger is specific to the greedy_ambiguity_resolution_algorithm, and to +// this translation unit. +struct logger { + static std::ostream& error() { + std::cout << "ERROR @greedy_ambiguity_resolution_algorithm: "; + return std::cout; + } + + static std::ostream& info() { + std::cout << "@greedy_ambiguity_resolution_algorithm: "; + return std::cout; + } + + static std::ostream& flood() { + std::cout << "@greedy_ambiguity_resolution_algorithm: "; + return std::cout; + } +}; + +} // namespace + +/// Run the algorithm +/// +/// @param track_states the container of the fitted track parameters +/// @return the container without ambiguous tracks +track_state_container_types::host +greedy_ambiguity_resolution_algorithm::operator()( + const typename track_state_container_types::host& track_states) const { + + state_t state; + compute_initial_state(track_states, state); + resolve(state); + + if (_config.check_obvious_errs) { + LOG_INFO("Checking result validity..."); + check_obvious_errors(track_states, state); + } + + // Copy the tracks to be retained in the return value + + track_state_container_types::host res; + res.reserve(state.selected_tracks.size()); + + LOG_INFO("state.selected_tracks.size() = " << state.selected_tracks.size()); + + for (std::size_t index : state.selected_tracks) { + // track_states is a host_container, + // track_state> + auto const [sm_headers, sm_items] = track_states.at(index); + + // Copy header + fitting_result header = sm_headers; + + // Copy states + vecmem::vector> states; + states.reserve(sm_items.size()); + for (auto const& item : sm_items) { + states.push_back(item); + } + + res.push_back(header, states); + } + return res; +} + +void greedy_ambiguity_resolution_algorithm::compute_initial_state( + const typename track_state_container_types::host& track_states, + state_t& state) const { + + // For each track of the input container + std::size_t n_track_states = track_states.size(); + for (std::size_t track_index = 0; track_index < n_track_states; + ++track_index) { + + // fit_res is a fitting_result + // states is a vecmem_vector> + auto const& [fit_res, states] = track_states.at(track_index); + + // Kick out tracks that do not fulfill our initial requirements + if (states.size() < _config.n_measurements_min) { + continue; + } + + // Create the list of measurement_id of the current track + std::vector measurements; + for (auto const& st : states) { + measurements.push_back(st.get_measurement().measurement_id); + } + + // Add this track chi2 value + state.track_chi2.push_back(fit_res.chi2); + // Add all the (measurement_id)s of this track + state.measurements_per_track.push_back(std::move(measurements)); + // Initially, every track is in the selected_track list. They will later + // be removed according to the algorithm. + state.selected_tracks.insert(state.number_of_tracks); + ++state.number_of_tracks; + } + + // Associate each measurement to the tracks sharing it + for (std::size_t track_index = 0; track_index < state.number_of_tracks; + ++track_index) { + for (auto meas_id : state.measurements_per_track[track_index]) { + state.tracks_per_measurement[meas_id].insert(track_index); + } + } + + // Finally, we can accumulate the number of shared measurements per track + state.shared_measurements_per_track = + std::vector(state.number_of_tracks, 0); + + for (std::size_t track_index = 0; track_index < state.number_of_tracks; + ++track_index) { + for (auto meas_index : state.measurements_per_track[track_index]) { + if (state.tracks_per_measurement[meas_index].size() > 1) { + ++state.shared_measurements_per_track[track_index]; + } + } + } +} + +/// Check for obvious errors returned by the algorithm: +/// - Returned tracks should be independent of each other: they should share a +/// maximum of (_config.maximum_shared_hits - 1) hits per track. +/// - Each removed track should share at least (_config.maximum_shared_hits) +/// with another initial track. +/// +/// @param initial_track_states The input track container, as given to +/// compute_initial_state. +/// @param final_state The state object after the resolve method has been +/// called. +bool greedy_ambiguity_resolution_algorithm::check_obvious_errors( + const typename track_state_container_types::host& initial_track_states, + state_t& final_state) const { + + // Associates every measurement_id to the number of tracks that shares it + // (during initial state) + std::unordered_map initial_measurement_count; + + // Initialize initial_measurement_count + for (std::size_t track_index = 0; track_index < initial_track_states.size(); + ++track_index) { + // fit_res is a fitting_result + // states is a vecmem_vector> + auto const& [fit_res, states] = initial_track_states.at(track_index); + + for (auto const& st : states) { + std::size_t meas_id = st.get_measurement().measurement_id; + + std::unordered_map::iterator meas_it = + initial_measurement_count.find(meas_id); + + if (meas_it == initial_measurement_count.end()) { + // not found: for now, this measurement only belongs to one + // track + initial_measurement_count[meas_id] = 1; + } else { + // found: this measurement is shared between at least two tracks + ++(meas_it->second); + } + } + } + + bool all_removed_tracks_alright = true; + // ========================================================================= + // Checks that every removed track had at least + // (_config.maximum_shared_hits) commun measurements with other tracks + // ========================================================================= + std::size_t n_initial_track_states = initial_track_states.size(); + for (std::size_t track_index = 0; track_index < n_initial_track_states; + ++track_index) { + auto const& [fit_res, states] = initial_track_states.at(track_index); + + // Skip this track if it has to be kept (i.e. exists in selected_tracks) + if (final_state.selected_tracks.find(track_index) != + final_state.selected_tracks.end()) { + continue; + } + + // So if the current track has been removed from selected_tracks: + + std::size_t shared_hits = 0; + for (auto const& st : states) { + auto meas_id = st.get_measurement().measurement_id; + + std::unordered_map::iterator meas_it = + initial_measurement_count.find(meas_id); + + if (meas_it == initial_measurement_count.end()) { + // Should never happen + LOG_ERROR( + "track_index(" + << track_index + << ") which is a removed track, has a measurement not " + << "present in initial_measurement_count. This should " + << "never happen and is an implementation error.\n"); + all_removed_tracks_alright = false; + } else if (meas_it->second > 1) { + ++shared_hits; + } + } + + if (shared_hits < _config.maximum_shared_hits) { + LOG_ERROR("track_index(" + << track_index + << ") which is a removed track, should at least share " + << _config.maximum_shared_hits + << " measurement(s) with other tracks, but only shares " + << shared_hits); + all_removed_tracks_alright = false; + } + } + + if (all_removed_tracks_alright) { + LOG_INFO( + "OK 1/2: every removed track had at least one commun measurement " + "with another track."); + } + + // ========================================================================= + // Checks that returned tracks are independent of each other: they should + // share a maximum of (_config.maximum_shared_hits - 1) hits per track. + // ========================================================================= + + // Used for return value and final message + bool independent_tracks = true; + + // Associates each measurement_id to a list of (track_index)es + std::unordered_map> + tracks_per_measurements; + + // Only for measurements shared between too many tracks: associates each + // measurement_id to a list of (track_index)es + std::unordered_map> + tracks_per_meas_err; + + // Initializes tracks_per_measurements + for (std::size_t track_index : final_state.selected_tracks) { + auto const& [fit_res, states] = initial_track_states.at(track_index); + for (auto const& mes : states) { + std::size_t meas_id = mes.get_measurement().measurement_id; + tracks_per_measurements[meas_id].push_back(track_index); + } + } + + // Displays common tracks per measurement if it exceeds the maximum count + for (auto const& val : tracks_per_measurements) { + auto const& tracks_per_mes = val.second; + if (tracks_per_mes.size() > _config.maximum_shared_hits) { + std::stringstream ss; + ss << "Measurement " << val.first << " is shared between " + << tracks_per_mes.size() + << " tracks, superior to _config.maximum_shared_hits(" + << _config.maximum_shared_hits + << "). It is shared between tracks:"; + + for (std::size_t track_index : tracks_per_mes) { + ss << " " << track_index; + } + + LOG_ERROR(ss.str()); + + // Displays each track's measurements: + for (std::size_t track_index : tracks_per_mes) { + std::stringstream ssm; + ssm << " Track(" << track_index << ")'s measurements:"; + auto const& [fit_res, states] = + initial_track_states.at(track_index); + + for (auto const& st : states) { + auto meas_id = st.get_measurement().measurement_id; + ssm << " " << meas_id; + } + LOG_ERROR(ssm.str()); + } + + independent_tracks = false; + } + } + + if (independent_tracks) { + LOG_INFO( + "OK 2/2: each selected_track shares at most " + "(_config.maximum_shared_hits - 1)(=" + << _config.maximum_shared_hits - 1 << ") measurement(s)"); + } + + return (all_removed_tracks_alright && independent_tracks); +} + +namespace { + +/// Removes a track from the state which has to be done for multiple properties +/// because of redundancy. +static void remove_track(greedy_ambiguity_resolution_algorithm::state_t& state, + std::size_t track_index) { + for (auto meas_index : state.measurements_per_track[track_index]) { + state.tracks_per_measurement[meas_index].erase(track_index); + + if (state.tracks_per_measurement[meas_index].size() == 1) { + auto j_track = *state.tracks_per_measurement[meas_index].begin(); + --state.shared_measurements_per_track[j_track]; + } + } + state.selected_tracks.erase(track_index); +} +} // namespace + +void greedy_ambiguity_resolution_algorithm::resolve(state_t& state) const { + /// Compares two tracks based on the number of shared measurements in order + /// to decide if we already met the final state. + auto shared_measurements_comperator = [&state](std::size_t a, + std::size_t b) { + return state.shared_measurements_per_track[a] < + state.shared_measurements_per_track[b]; + }; + + /// Compares two tracks in order to find the one which should be evicted. + /// First we compare the relative amount of shared measurements. If that is + /// indecisive we use the chi2. + auto track_comperator = [&state](std::size_t a, std::size_t b) { + /// Helper to calculate the relative amount of shared measurements. + auto relative_shared_measurements = [&state](std::size_t i) { + return 1.0 * state.shared_measurements_per_track[i] / + state.measurements_per_track[i].size(); + }; + + if (relative_shared_measurements(a) != + relative_shared_measurements(b)) { + return relative_shared_measurements(a) < + relative_shared_measurements(b); + } + return state.track_chi2[a] < state.track_chi2[b]; + }; + + std::size_t iteration_count = 0; + for (std::size_t i = 0; i < _config.maximum_iterations; ++i) { + // Lazy out if there is nothing to filter on. + if (state.selected_tracks.empty()) { + LOG_INFO("No tracks left - exit loop"); + break; + } + + // Find the maximum amount of shared measurements per track to decide if + // we are done or not. + auto maximum_shared_measurements = *std::max_element( + state.selected_tracks.begin(), state.selected_tracks.end(), + shared_measurements_comperator); + + LOG_FLOOD( + "Current maximum shared measurements " + << state + .shared_measurements_per_track[maximum_shared_measurements]); + + if (state.shared_measurements_per_track[maximum_shared_measurements] < + _config.maximum_shared_hits) { + break; + } + + // Find the "worst" track by comparing them to each other + auto bad_track = + *std::max_element(state.selected_tracks.begin(), + state.selected_tracks.end(), track_comperator); + + LOG_FLOOD("Remove track " + << bad_track << " n_meas " + << state.measurements_per_track[bad_track].size() + << " nShared " + << state.shared_measurements_per_track[bad_track] << " chi2 " + << state.track_chi2[bad_track]); + + remove_track(state, bad_track); + ++iteration_count; + } + + LOG_INFO("Iteration_count: " << iteration_count); +} + +} // namespace traccc diff --git a/examples/options/include/traccc/options/common_options.hpp b/examples/options/include/traccc/options/common_options.hpp index 08e4d5233a..c3b913342e 100644 --- a/examples/options/include/traccc/options/common_options.hpp +++ b/examples/options/include/traccc/options/common_options.hpp @@ -22,6 +22,7 @@ struct common_options { int skip; unsigned short target_cells_per_partition; bool check_performance; + bool perform_ambiguity_resolution; common_options(po::options_description& desc); void read(const po::variables_map& vm); diff --git a/examples/options/src/options/common_options.cpp b/examples/options/src/options/common_options.cpp index 18977d54e3..e65858e6db 100644 --- a/examples/options/src/options/common_options.cpp +++ b/examples/options/src/options/common_options.cpp @@ -26,6 +26,9 @@ traccc::common_options::common_options(po::options_description& desc) { desc.add_options()("check-performance", po::value()->default_value(false), "generate performance result"); + desc.add_options()("perform-ambiguity-resolution", + po::value()->default_value(true), + "perform ambiguity resolution"); } void traccc::common_options::read(const po::variables_map& vm) { @@ -41,4 +44,6 @@ void traccc::common_options::read(const po::variables_map& vm) { target_cells_per_partition = vm["target-cells-per-partition"].as(); check_performance = vm["check-performance"].as(); + perform_ambiguity_resolution = + vm["perform-ambiguity-resolution"].as(); } \ No newline at end of file diff --git a/examples/run/cpu/seeding_example.cpp b/examples/run/cpu/seeding_example.cpp index cf9ab88db1..b33769bcc2 100644 --- a/examples/run/cpu/seeding_example.cpp +++ b/examples/run/cpu/seeding_example.cpp @@ -16,6 +16,7 @@ #include "traccc/io/utils.hpp" // algorithms +#include "traccc/ambiguity_resolution/greedy_ambiguity_resolution_algorithm.hpp" #include "traccc/finding/finding_algorithm.hpp" #include "traccc/fitting/fitting_algorithm.hpp" #include "traccc/seeding/seeding_algorithm.hpp" @@ -87,6 +88,7 @@ int seq_run(const traccc::seeding_input_config& /*i_cfg*/, uint64_t n_seeds = 0; uint64_t n_found_tracks = 0; uint64_t n_fitted_tracks = 0; + uint64_t n_ambiguity_free_tracks = 0; /***************************** * Build a geometry @@ -140,6 +142,8 @@ int seq_run(const traccc::seeding_input_config& /*i_cfg*/, traccc::fitting_algorithm host_fitting(fit_cfg); + traccc::greedy_ambiguity_resolution_algorithm host_ambiguity_resolution{}; + // Loop over events for (unsigned int event = common_opts.skip; event < common_opts.events + common_opts.skip; ++event) { @@ -168,6 +172,7 @@ int seq_run(const traccc::seeding_input_config& /*i_cfg*/, // Run CKF and KF if we are using a detray geometry traccc::track_candidate_container_types::host track_candidates; traccc::track_state_container_types::host track_states; + traccc::track_state_container_types::host track_states_ar; // Read measurements traccc::io::measurement_reader_output meas_read_out(&host_mr); @@ -193,6 +198,11 @@ int seq_run(const traccc::seeding_input_config& /*i_cfg*/, track_states = host_fitting(host_det, field, track_candidates); n_fitted_tracks += track_states.size(); + if (common_opts.perform_ambiguity_resolution) { + track_states_ar = host_ambiguity_resolution(track_states); + n_ambiguity_free_tracks += track_states_ar.size(); + } + /*------------ Statistics ------------*/ @@ -242,6 +252,13 @@ int seq_run(const traccc::seeding_input_config& /*i_cfg*/, std::cout << "- created (cpu) " << n_fitted_tracks << " fitted tracks" << std::endl; + if (common_opts.perform_ambiguity_resolution) { + std::cout << "- created (cpu) " << n_ambiguity_free_tracks + << " ambiguity free tracks" << std::endl; + } else { + std::cout << "- ambiguity resolution: deactivated" << std::endl; + } + return 0; } diff --git a/io/src/csv/read_measurements.cpp b/io/src/csv/read_measurements.cpp index 06162b3986..bb332908cc 100644 --- a/io/src/csv/read_measurements.cpp +++ b/io/src/csv/read_measurements.cpp @@ -78,6 +78,8 @@ void read_measurements(measurement_reader_output& out, meas.subs.set_indices(indices); meas.surface_link = detray::geometry::barcode{iomeas.geometry_id}; meas.module_link = link; + // Keeps measurement_id for ambiguity resolution + meas.measurement_id = iomeas.measurement_id; result_measurements.push_back(meas); }