From 2fd05f028514b9612f9a43909b02536aa0193736 Mon Sep 17 00:00:00 2001 From: Joana Niermann Date: Wed, 15 Jan 2025 17:56:14 +0100 Subject: [PATCH] Cleanup python plotting code in preparation for benchmark plots --- .../common/navigation_validation_config.hpp | 11 + .../include/detray/test/cpu/detector_scan.hpp | 19 +- .../include/detray/test/cpu/material_scan.hpp | 2 +- .../detray/test/cpu/navigation_validation.hpp | 34 +- .../device/cuda/navigation_validation.hpp | 34 +- .../random_track_generator.hpp | 16 +- .../random_track_generator_config.hpp | 1 + .../uniform_track_generator.hpp | 15 +- .../uniform_track_generator_config.hpp | 4 + .../detray/options/propagation_options.hpp | 3 +- .../options/track_generator_options.hpp | 118 ++++-- tests/tools/python/file_checker.py | 8 +- tests/tools/python/impl/plot_material_scan.py | 82 ++-- .../python/impl/plot_navigation_validation.py | 55 +-- tests/tools/python/impl/plot_ray_scan.py | 58 ++- tests/tools/python/impl/plot_track_params.py | 273 +++++-------- tests/tools/python/material_validation.py | 59 +-- tests/tools/python/navigation_validation.py | 68 +--- tests/tools/python/options/common_options.py | 7 +- .../python/options/detector_io_options.py | 9 +- .../tools/python/options/plotting_options.py | 9 +- .../python/options/propagation_options.py | 22 +- .../python/options/track_generator_options.py | 72 ++-- tests/tools/python/plotting/__init__.py | 2 +- tests/tools/python/plotting/plot_helpers.py | 34 +- tests/tools/python/plotting/pyplot_factory.py | 380 +++++++++--------- tests/tools/python/utils/__init__.py | 6 + tests/tools/python/utils/io_utils.py | 155 +++++++ tests/tools/src/cpu/detector_validation.cpp | 1 + 29 files changed, 861 insertions(+), 696 deletions(-) create mode 100644 tests/tools/python/utils/__init__.py create mode 100644 tests/tools/python/utils/io_utils.py diff --git a/tests/include/detray/test/common/navigation_validation_config.hpp b/tests/include/detray/test/common/navigation_validation_config.hpp index b76f89ce0..e8d974f0e 100644 --- a/tests/include/detray/test/common/navigation_validation_config.hpp +++ b/tests/include/detray/test/common/navigation_validation_config.hpp @@ -38,6 +38,12 @@ struct navigation_validation_config std::string m_track_param_file{"truth_trk_parameters.csv"}; /// The maximal number of test tracks to run std::size_t m_n_tracks{detray::detail::invalid_value()}; + /// Configured momentum range of the test sample (only needed to generate + /// correct file names). If none was passed, it will be determined from + /// the track data (imprecise!) + darray m_p_range{ + detray::detail::invalid_value(), + detray::detail::invalid_value()}; /// B-field vector for helix vector3_type m_B{0.f * unit::T, 0.f * unit::T, 2.f * unit::T}; @@ -55,6 +61,7 @@ struct navigation_validation_config const std::string &intersection_file() const { return m_intersection_file; } const std::string &track_param_file() const { return m_track_param_file; } std::size_t n_tracks() const { return m_n_tracks; } + darray p_range() const { return m_p_range; } const vector3_type &B_vector() { return m_B; } const auto &svg_style() const { return m_style; } /// @} @@ -86,6 +93,10 @@ struct navigation_validation_config m_n_tracks = n; return *this; } + navigation_validation_config &p_range(const darray pr) { + m_p_range = pr; + return *this; + } navigation_validation_config &B_vector(const vector3_type &B) { m_B = B; return *this; diff --git a/tests/include/detray/test/cpu/detector_scan.hpp b/tests/include/detray/test/cpu/detector_scan.hpp index 869e408ff..d7d573dbd 100644 --- a/tests/include/detray/test/cpu/detector_scan.hpp +++ b/tests/include/detray/test/cpu/detector_scan.hpp @@ -170,15 +170,24 @@ class detector_scan : public test::fixture_base<> { const std::size_t n_helices{trk_state_generator.size()}; intersection_traces.reserve(n_helices); - const auto pT_range = m_cfg.track_generator().mom_range(); - std::string mometum_str{std::to_string(pT_range[0]) + "_" + - std::to_string(pT_range[1])}; + std::string momentum_str{""}; + if constexpr (!k_use_rays) { + const auto pT_range = m_cfg.track_generator().mom_range(); + // Remove floating point imprecisions + momentum_str = + std::to_string( + std::floor(10. * static_cast(pT_range[0])) / 10.) + + "_" + + std::to_string( + std::ceil(10. * static_cast(pT_range[1])) / 10.) + + "_GeV"; + } std::string track_param_file_name{m_cfg.track_param_file() + "_" + - mometum_str + "GeV.csv"}; + momentum_str + ".csv"}; std::string intersection_file_name{m_cfg.intersection_file() + "_" + - mometum_str + "GeV.csv"}; + momentum_str + ".csv"}; const bool data_files_exist{io::file_exists(intersection_file_name) && io::file_exists(track_param_file_name)}; diff --git a/tests/include/detray/test/cpu/material_scan.hpp b/tests/include/detray/test/cpu/material_scan.hpp index d08143b0d..446478d46 100644 --- a/tests/include/detray/test/cpu/material_scan.hpp +++ b/tests/include/detray/test/cpu/material_scan.hpp @@ -127,7 +127,7 @@ class material_scan : public test::fixture_base<> { } // Record track parameters - tracks.push_back({ray.pos(), 0.f, ray.dir(), -1.f}); + tracks.push_back({ray.pos(), 0.f, ray.dir(), 0.f}); // New material record material_record_t mat_record{}; diff --git a/tests/include/detray/test/cpu/navigation_validation.hpp b/tests/include/detray/test/cpu/navigation_validation.hpp index 1177cb7ea..14ed1a6c7 100644 --- a/tests/include/detray/test/cpu/navigation_validation.hpp +++ b/tests/include/detray/test/cpu/navigation_validation.hpp @@ -142,12 +142,18 @@ class navigation_validation : public test::fixture_base<> { trajectory_type test_traj = get_parametrized_trajectory(track); const scalar q = start.charge; - // If the momentum is unknown, 1 GeV is the safest option to keep - // the direction vector normalized const scalar pT{q == 0.f ? 1.f * unit::GeV : track.pT(q)}; const scalar p{q == 0.f ? 1.f * unit::GeV : track.p(q)}; - min_pT = std::min(min_pT, pT); - max_pT = std::max(max_pT, pT); + + // If the momentum is unknown, 1 GeV is the safest option to keep + // the direction vector normalized + if (detray::detail::is_invalid_value(m_cfg.p_range()[0])) { + min_pT = std::min(min_pT, pT); + max_pT = std::max(max_pT, pT); + } else { + min_pT = m_cfg.p_range()[0]; + max_pT = m_cfg.p_range()[1]; + } // Run the propagation auto [success, obj_tracer, step_trace, mat_record, mat_trace, @@ -223,20 +229,28 @@ class navigation_validation : public test::fixture_base<> { n_matching_error); // Print track positions for plotting - std::string mometum_str{std::to_string(min_pT) + "_" + - std::to_string(max_pT)}; + std::string momentum_str{""}; + if constexpr (!k_use_rays) { + momentum_str = + std::to_string(std::floor(10. * static_cast(min_pT)) / + 10.) + + "_" + + std::to_string(std::ceil(10. * static_cast(max_pT)) / + 10.) + + "_GeV"; + } const auto data_path{ std::filesystem::path{m_cfg.track_param_file()}.parent_path()}; const auto truth_trk_path{data_path / (prefix + "truth_track_params_" + - mometum_str + "GeV.csv")}; + momentum_str + ".csv")}; const auto trk_path{data_path / (prefix + "navigation_track_params_" + - mometum_str + "GeV.csv")}; + momentum_str + ".csv")}; const auto mat_path{data_path / (prefix + "accumulated_material_" + - mometum_str + "GeV.csv")}; + momentum_str + ".csv")}; const auto missed_path{ data_path / - (prefix + "missed_intersections_dists_" + mometum_str + "GeV.csv")}; + (prefix + "missed_intersections_dists_" + momentum_str + ".csv")}; // Write the distance of the missed intersection local position // to the surface boundaries to file for plotting diff --git a/tests/include/detray/test/device/cuda/navigation_validation.hpp b/tests/include/detray/test/device/cuda/navigation_validation.hpp index 6b940d19d..3ae23e57a 100644 --- a/tests/include/detray/test/device/cuda/navigation_validation.hpp +++ b/tests/include/detray/test/device/cuda/navigation_validation.hpp @@ -295,8 +295,14 @@ class navigation_validation : public test::fixture_base<> { : trck_param.pT(q)}; const scalar p{q == 0.f ? 1.f * unit::GeV : trck_param.p(q)}; - min_pT = std::min(min_pT, pT); - max_pT = std::max(max_pT, pT); + + if (detray::detail::is_invalid_value(m_cfg.p_range()[0])) { + min_pT = std::min(min_pT, pT); + max_pT = std::max(max_pT, pT); + } else { + min_pT = m_cfg.p_range()[0]; + max_pT = m_cfg.p_range()[1]; + } // Recorded only the start position, which added by default bool success{true}; @@ -354,22 +360,30 @@ class navigation_validation : public test::fixture_base<> { n_matching_error); // Print track positions for plotting - std::string mometum_str{std::to_string(min_pT) + "_" + - std::to_string(max_pT)}; + std::string momentum_str{""}; + if constexpr (!k_use_rays) { + momentum_str = + std::to_string(std::floor(10. * static_cast(min_pT)) / + 10.) + + "_" + + std::to_string(std::ceil(10. * static_cast(max_pT)) / + 10.) + + "_GeV"; + } const auto data_path{ std::filesystem::path{m_cfg.track_param_file()}.parent_path()}; const auto truth_trk_path{ data_path / - (prefix + "truth_track_params_cuda_" + mometum_str + "GeV.csv")}; - const auto trk_path{data_path / - (prefix + "navigation_track_params_cuda_" + - mometum_str + "GeV.csv")}; + (prefix + "truth_track_params_cuda_" + momentum_str + ".csv")}; + const auto trk_path{ + data_path / + (prefix + "navigation_track_params_cuda_" + momentum_str + ".csv")}; const auto mat_path{data_path / (prefix + "accumulated_material_cuda_" + - mometum_str + "GeV.csv")}; + momentum_str + ".csv")}; const auto missed_path{data_path / (prefix + "missed_intersections_dists_cuda_" + - mometum_str + "GeV.csv")}; + momentum_str + ".csv")}; // Write the distance of the missed intersection local position // to the surface boundaries to file for plotting diff --git a/tests/include/detray/test/utils/simulation/event_generator/random_track_generator.hpp b/tests/include/detray/test/utils/simulation/event_generator/random_track_generator.hpp index 8b2ed5942..8d9adf468 100644 --- a/tests/include/detray/test/utils/simulation/event_generator/random_track_generator.hpp +++ b/tests/include/detray/test/utils/simulation/event_generator/random_track_generator.hpp @@ -12,6 +12,7 @@ #include "detray/definitions/detail/math.hpp" #include "detray/definitions/detail/qualifiers.hpp" #include "detray/definitions/units.hpp" +#include "detray/navigation/detail/ray.hpp" #include "detray/utils/ranges/ranges.hpp" // Detray test include(s) @@ -123,12 +124,15 @@ class random_track_generator vector3_t mom{math::cos(phi) * sin_theta, math::sin(phi) * sin_theta, math::cos(theta)}; - sin_theta = (sin_theta == scalar_t{0.f}) - ? std::numeric_limits::epsilon() - : sin_theta; - - mom = (m_cfg.is_pT() ? 1.f / sin_theta : 1.f) * p_mag * - vector::normalize(mom); + if constexpr (std::is_same_v>) { + mom = vector::normalize(mom); + } else { + sin_theta = (sin_theta == scalar_t{0.f}) + ? std::numeric_limits::epsilon() + : sin_theta; + mom = (m_cfg.is_pT() ? 1.f / sin_theta : 1.f) * p_mag * + vector::normalize(mom); + } // Randomly flip the charge sign darray signs{1., -1.}; diff --git a/tests/include/detray/test/utils/simulation/event_generator/random_track_generator_config.hpp b/tests/include/detray/test/utils/simulation/event_generator/random_track_generator_config.hpp index 3a808b44f..0d757e501 100644 --- a/tests/include/detray/test/utils/simulation/event_generator/random_track_generator_config.hpp +++ b/tests/include/detray/test/utils/simulation/event_generator/random_track_generator_config.hpp @@ -251,6 +251,7 @@ struct random_track_generator_config { // General out << "\nRandom track generator\n" << "----------------------------\n" + << " Random seed : " << cfg.seed() << "\n" << " No. tracks : " << cfg.n_tracks() << "\n" << " Charge : " << cfg.charge() / detray::unit::e << " [e]\n" diff --git a/tests/include/detray/test/utils/simulation/event_generator/uniform_track_generator.hpp b/tests/include/detray/test/utils/simulation/event_generator/uniform_track_generator.hpp index 5d1628c8d..ba6bdf311 100644 --- a/tests/include/detray/test/utils/simulation/event_generator/uniform_track_generator.hpp +++ b/tests/include/detray/test/utils/simulation/event_generator/uniform_track_generator.hpp @@ -12,6 +12,7 @@ #include "detray/definitions/detail/math.hpp" #include "detray/definitions/detail/qualifiers.hpp" #include "detray/definitions/units.hpp" +#include "detray/navigation/detail/ray.hpp" #include "detray/utils/ranges/ranges.hpp" // Detray test include(s) @@ -154,11 +155,15 @@ class uniform_track_generator math::sin(m_phi) * sin_theta, math::cos(m_theta)}; // Magnitude of momentum - sin_theta = (sin_theta == scalar_t{0.f}) - ? std::numeric_limits::epsilon() - : sin_theta; - p = (m_cfg.is_pT() ? 1.f / sin_theta : 1.f) * m_cfg.m_p_mag * - vector::normalize(p); + if constexpr (std::is_same_v>) { + p = vector::normalize(p); + } else { + sin_theta = (sin_theta == scalar_t{0.f}) + ? std::numeric_limits::epsilon() + : sin_theta; + p = (m_cfg.is_pT() ? 1.f / sin_theta : 1.f) * m_cfg.m_p_mag * + vector::normalize(p); + } const auto& ori = m_cfg.origin(); diff --git a/tests/include/detray/test/utils/simulation/event_generator/uniform_track_generator_config.hpp b/tests/include/detray/test/utils/simulation/event_generator/uniform_track_generator_config.hpp index d120a7a83..35713985a 100644 --- a/tests/include/detray/test/utils/simulation/event_generator/uniform_track_generator_config.hpp +++ b/tests/include/detray/test/utils/simulation/event_generator/uniform_track_generator_config.hpp @@ -199,6 +199,9 @@ struct uniform_track_generator_config { DETRAY_HOST_DEVICE constexpr darray eta_range() const { return m_eta_range; } + DETRAY_HOST_DEVICE constexpr darray mom_range() const { + return {m_p_mag, m_p_mag}; + } DETRAY_HOST_DEVICE constexpr std::size_t phi_steps() const { return m_phi_steps; } @@ -230,6 +233,7 @@ struct uniform_track_generator_config { // General out << "\nUnform track generator\n" << "----------------------------\n" + << " Random seed : " << cfg.seed() << "\n" << " No. tracks : " << cfg.n_tracks() << "\n" << " -> phi steps : " << cfg.phi_steps() << "\n" << " -> theta/eta steps : " << cfg.theta_steps() << "\n" diff --git a/tests/tools/include/detray/options/propagation_options.hpp b/tests/tools/include/detray/options/propagation_options.hpp index 7e634a323..ffa193332 100644 --- a/tests/tools/include/detray/options/propagation_options.hpp +++ b/tests/tools/include/detray/options/propagation_options.hpp @@ -42,7 +42,7 @@ void add_options( "mask_tolerance_scalor", boost::program_options::value()->default_value( cfg.mask_tolerance_scalor), - "Mask tolerance scaling")( + "Mask tolerance scale factor")( "overstep_tolerance", boost::program_options::value()->default_value( cfg.overstep_tolerance / unit::um), @@ -172,6 +172,7 @@ void configure_options( cfg.path_limit = path_limit * unit::m; } + cfg.do_covariance_transport = false; if (vm.count("covariance_transport")) { cfg.do_covariance_transport = true; } diff --git a/tests/tools/include/detray/options/track_generator_options.hpp b/tests/tools/include/detray/options/track_generator_options.hpp index 346803ef0..a2cdcdd5a 100644 --- a/tests/tools/include/detray/options/track_generator_options.hpp +++ b/tests/tools/include/detray/options/track_generator_options.hpp @@ -41,6 +41,9 @@ void add_uniform_track_gen_options( boost::program_options::value()->default_value( cfg.eta_steps()), "No. eta steps for particle gun")( + "random_seed", + boost::program_options::value()->default_value(cfg.seed()), + "Seed for the random number generator")( "eta_range", boost::program_options::value>()->multitoken(), "Min, Max range of eta values for particle gun")( @@ -48,14 +51,12 @@ void add_uniform_track_gen_options( "origin", boost::program_options::value>()->multitoken(), "Coordintates for particle gun origin position [mm]")( - "p_tot", - boost::program_options::value()->default_value( - static_cast(cfg.m_p_mag) / unit::GeV), - "Total momentum of the test particle [GeV]")( - "p_T", - boost::program_options::value()->default_value( - static_cast(cfg.m_p_mag) / unit::GeV), - "Transverse momentum of the test particle [GeV]"); + "p_range", + boost::program_options::value>()->multitoken(), + "Total momentum [range] of the test particles [GeV]")( + "pT_range", + boost::program_options::value>()->multitoken(), + "Transverse momentum [range] of the test particles [GeV]"); } /// Add options for detray event generation @@ -66,6 +67,7 @@ void configure_uniform_track_gen_options( cfg.phi_steps(vm["phi_steps"].as()); cfg.eta_steps(vm["eta_steps"].as()); + cfg.seed(vm["random_seed"].as()); cfg.randomize_charge(vm.count("randomize_charge")); if (vm.count("eta_range")) { @@ -87,15 +89,49 @@ void configure_uniform_track_gen_options( "Particle gun origin needs three arguments"); } } - if (!vm["p_T"].defaulted() && !vm["p_tot"].defaulted()) { + if (vm.count("pT_range") && vm.count("p_range")) { throw std::invalid_argument( "Transverse and total momentum cannot be specified at the same " "time"); } - if (!vm["p_T"].defaulted()) { - cfg.p_T(vm["p_T"].as() * unit::GeV); + if (vm.count("pT_range")) { + const auto pt_range = vm["pT_range"].as>(); + + // Default + if (pt_range.empty()) { + cfg.p_T(static_cast(cfg.m_p_mag)); + } else if (pt_range.size() <= 2u) { + if (pt_range.size() == 2u) { + std::cout + << "WARNING: Momentum range not possible with uniform " + "track generator: Using first value." + << std::endl; + } + cfg.p_T(pt_range[0] * unit::GeV); + } else { + throw std::invalid_argument( + "Wrong number of arguments for pT range: Need one argument or " + "range"); + } } else { - cfg.p_tot(vm["p_tot"].as() * unit::GeV); + const auto p_range = vm["p_range"].as>(); + + // Default + if (p_range.empty()) { + cfg.p_tot(static_cast(cfg.m_p_mag)); + } else if (p_range.size() <= 2u) { + if (p_range.size() == 2u) { + std::cout + << "WARNING: Momentum range not possible with uniform " + "track generator: Using first value." + << std::endl; + } + cfg.p_tot(p_range[0] * unit::GeV); + } else { + throw std::invalid_argument( + "Wrong number of arguments for p_tot range: Need one argument " + "or range"); + } } } @@ -110,24 +146,25 @@ void add_rnd_track_gen_options( boost::program_options::value()->default_value( cfg.n_tracks()), "No. of tracks for particle gun")( + "random_seed", + boost::program_options::value()->default_value(cfg.seed()), + "Seed for the random number generator")( "theta_range", boost::program_options::value>()->multitoken(), - "Min, Max range of theta values for particle gun")( + "Min, Max range of theta values for particle gun. Interval in [0, pi)")( "eta_range", boost::program_options::value>()->multitoken(), "Min, Max range of eta values for particle gun")( "randomize_charge", "Randomly flip charge sign per track")( "origin", boost::program_options::value>()->multitoken(), - "Coordintates for particle gun origin position")( - "p_tot", - boost::program_options::value()->default_value( - static_cast(cfg.mom_range()[0]) / unit::GeV), - "Total momentum of the test particle [GeV]")( - "p_T", - boost::program_options::value()->default_value( - static_cast(cfg.mom_range()[0]) / unit::GeV), - "Transverse momentum of the test particle [GeV]"); + "Coordintates for particle gun origin position [mm]")( + "p_range", + boost::program_options::value>()->multitoken(), + "Total momentum [range] of the test particles [GeV]")( + "pT_range", + boost::program_options::value>()->multitoken(), + "Transverse momentum [range] of the test particles [GeV]"); } /// Add options for detray event generation @@ -137,6 +174,7 @@ void configure_rnd_track_gen_options( random_track_generator_config &cfg) { cfg.n_tracks(vm["n_tracks"].as()); + cfg.seed(vm["random_seed"].as()); cfg.randomize_charge(vm.count("randomize_charge")); if (vm.count("eta_range") && vm.count("theta_range")) { @@ -178,15 +216,43 @@ void configure_rnd_track_gen_options( "Particle gun origin needs three coordinates"); } } - if (!vm["p_T"].defaulted() && !vm["p_tot"].defaulted()) { + if (vm.count("pT_range") && vm.count("p_range")) { throw std::invalid_argument( "Transverse and total momentum cannot be specified at the same " "time"); } - if (!vm["p_T"].defaulted()) { - cfg.p_T(vm["p_T"].as() * unit::GeV); + if (vm.count("pT_range")) { + const auto pt_range = vm["pT_range"].as>(); + + // Default + if (pt_range.empty()) { + cfg.p_T(cfg.mom_range()[0]); + } else if (pt_range.size() == 1u) { + cfg.p_T(pt_range[0] * unit::GeV); + } else if (pt_range.size() == 2u) { + cfg.pT_range(pt_range[0] * unit::GeV, + pt_range[1] * unit::GeV); + } else { + throw std::invalid_argument( + "Wrong number of arguments for pT range: Need one argument or " + "range"); + } } else { - cfg.p_tot(vm["p_tot"].as() * unit::GeV); + const auto p_range = vm["p_range"].as>(); + + // Default + if (p_range.empty()) { + cfg.p_tot(cfg.mom_range()[0]); + } else if (p_range.size() == 1u) { + cfg.p_tot(p_range[0] * unit::GeV); + } else if (p_range.size() == 2u) { + cfg.mom_range(p_range[0] * unit::GeV, + p_range[1] * unit::GeV); + } else { + throw std::invalid_argument( + "Wrong number of arguments for p_tot range: Need one argument " + "or range"); + } } } diff --git a/tests/tools/python/file_checker.py b/tests/tools/python/file_checker.py index ac22a6016..ad972f69d 100644 --- a/tests/tools/python/file_checker.py +++ b/tests/tools/python/file_checker.py @@ -19,7 +19,7 @@ def __main__(): - # ----------------------------------------------------------------arg parsing + # ---------------------------------------------------------------arg parsing parser = argparse.ArgumentParser(description="Detray File Validation") @@ -79,7 +79,7 @@ def __main__(): else: filename_dict[grid_file] = surface_grid_schema - # ------------------------------------------------------------------------run + # -----------------------------------------------------------------------run for filename, schema in filename_dict.items(): with open(filename) as file: @@ -92,9 +92,9 @@ def __main__(): print(f"{filename}: OK") -# ------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ if __name__ == "__main__": __main__() -# ------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ diff --git a/tests/tools/python/impl/plot_material_scan.py b/tests/tools/python/impl/plot_material_scan.py index 11e6be06a..a9f5d7a33 100644 --- a/tests/tools/python/impl/plot_material_scan.py +++ b/tests/tools/python/impl/plot_material_scan.py @@ -1,6 +1,6 @@ # Detray library, part of the ACTS project (R&D line) # -# (c) 2023-2024 CERN for the benefit of the ACTS project +# (c) 2023-2025 CERN for the benefit of the ACTS project # # Mozilla Public License Version 2.0 @@ -120,9 +120,9 @@ def X0_vs_eta_phi(df, label, detector, plot_factory, out_format="pdf"): y=df["phi"], z=df["mat_tX0"], label=detector, - x_label=label_eta, - y_label=label_phi, - z_label=label_thickness_x0, + x_axis=plotting.axis_options(label=label_eta), + y_axis=plotting.axis_options(label=label_phi), + z_axis=plotting.axis_options(label=label_thickness_x0), x_bins=x_binning, y_bins=y_binning, figsize=(9, 7), @@ -137,9 +137,9 @@ def X0_vs_eta_phi(df, label, detector, plot_factory, out_format="pdf"): y=df["phi"], z=df["mat_sX0"], label=detector, - x_label=label_eta, - y_label=label_phi, - z_label=label_path_x0, + x_axis=plotting.axis_options(label=label_eta), + y_axis=plotting.axis_options(label=label_phi), + z_axis=plotting.axis_options(label=label_path_x0), x_bins=x_binning, y_bins=y_binning, figsize=(9, 7), @@ -163,9 +163,9 @@ def L0_vs_eta_phi(df, label, detector, plot_factory, out_format="pdf"): y=df["phi"], z=df["mat_tL0"], label=detector, - x_label=label_eta, - y_label=label_phi, - z_label=label_thickness_l0, + x_axis=plotting.axis_options(label=label_eta), + y_axis=plotting.axis_options(label=label_phi), + z_axis=plotting.axis_options(label=label_thickness_l0), x_bins=x_binning, y_bins=y_binning, figsize=(9, 7), @@ -180,9 +180,9 @@ def L0_vs_eta_phi(df, label, detector, plot_factory, out_format="pdf"): y=df["phi"], z=df["mat_sL0"], label=detector, - x_label=label_eta, - y_label=label_phi, - z_label=label_path_l0, + x_axis=plotting.axis_options(label=label_eta), + y_axis=plotting.axis_options(label=label_phi), + z_axis=plotting.axis_options(label=label_path_l0), x_bins=x_binning, y_bins=y_binning, figsize=(9, 7), @@ -197,13 +197,12 @@ def L0_vs_eta_phi(df, label, detector, plot_factory, out_format="pdf"): def X0_vs_eta(df, label, detector, plot_factory, out_format="pdf"): # Where to place the legend box - box_anchor_x = 1.02 - box_anchor_y = 1.145 + lgd_ops = plotting.legend_options( + loc=ldg_loc, horiz_anchor=0.725, vert_anchor=1.145 + ) # Histogram bin edges x_binning, y_binning = get_n_bins(df) - lgd_ops = plotting.get_legend_options() - lgd_ops._replace(loc=ldg_loc) # Same number of entries in every bin as per uniform ray scan n_phi = len(y_binning) - 1 @@ -214,17 +213,14 @@ def X0_vs_eta(df, label, detector, plot_factory, out_format="pdf"): errors=get_errors(df, n_phi, "mat_tX0"), normalize=False, label=rf"{detector}", - x_label=label_eta, - y_label=label_thickness_x0, + x_axis=plotting.axis_options(label=label_eta), + y_axis=plotting.axis_options(label=label_thickness_x0), bins=x_binning, show_stats=False, figsize=(9, 7), lgd_ops=lgd_ops, ) - # Move the legend ouside plot - hist_data.lgd.set_bbox_to_anchor((box_anchor_x, box_anchor_y)) - plot_factory.write_plot(hist_data, detector + "_" + label + "_t_X0", out_format) hist_data = plot_factory.hist1D( @@ -233,17 +229,14 @@ def X0_vs_eta(df, label, detector, plot_factory, out_format="pdf"): errors=get_errors(df, n_phi, "mat_sX0"), normalize=False, label=rf"{detector}", - x_label=label_eta, - y_label=label_path_x0, + x_axis=plotting.axis_options(label=label_eta), + y_axis=plotting.axis_options(label=label_path_x0), bins=x_binning, show_stats=False, figsize=(9, 7), lgd_ops=lgd_ops, ) - # Move the legend ouside plot - hist_data.lgd.set_bbox_to_anchor((box_anchor_x, box_anchor_y)) - plot_factory.write_plot(hist_data, detector + "_" + label + "_s_X0", out_format) @@ -252,13 +245,12 @@ def X0_vs_eta(df, label, detector, plot_factory, out_format="pdf"): def L0_vs_eta(df, label, detector, plot_factory, out_format="pdf"): # Where to place the legend box - box_anchor_x = 1.02 - box_anchor_y = 1.145 + lgd_ops = plotting.legend_options( + loc=ldg_loc, horiz_anchor=0.725, vert_anchor=1.145 + ) # Histogram bin edges x_binning, y_binning = get_n_bins(df) - lgd_ops = plotting.get_legend_options() - lgd_ops._replace(loc=ldg_loc) # Same number of entries in every bin as per uniform ray scan n_phi = len(y_binning) - 1 @@ -269,17 +261,14 @@ def L0_vs_eta(df, label, detector, plot_factory, out_format="pdf"): errors=get_errors(df, n_phi, "mat_tL0"), normalize=False, label=rf"{detector}", - x_label=label_eta, - y_label=label_thickness_l0, + x_axis=plotting.axis_options(label=label_eta), + y_axis=plotting.axis_options(label=label_thickness_l0), bins=x_binning, show_stats=False, figsize=(9, 7), lgd_ops=lgd_ops, ) - # Move the legend ouside plot - hist_data.lgd.set_bbox_to_anchor((box_anchor_x, box_anchor_y)) - plot_factory.write_plot(hist_data, detector + "_" + label + "_t_L0", out_format) hist_data = plot_factory.hist1D( @@ -288,17 +277,14 @@ def L0_vs_eta(df, label, detector, plot_factory, out_format="pdf"): errors=get_errors(df, n_phi, "mat_sL0"), normalize=False, label=rf"{detector}", - x_label=label_eta, - y_label=label_path_l0, + x_axis=plotting.axis_options(label=label_eta), + y_axis=plotting.axis_options(label=label_path_l0), bins=x_binning, show_stats=False, figsize=(9, 7), lgd_ops=lgd_ops, ) - # Move the legend ouside plot - hist_data.lgd.set_bbox_to_anchor((box_anchor_x, box_anchor_y)) - plot_factory.write_plot(hist_data, detector + "_" + label + "_s_L0", out_format) @@ -307,13 +293,10 @@ def L0_vs_eta(df, label, detector, plot_factory, out_format="pdf"): def compare_mat(df_truth, df_rec, label, detector, plot_factory, out_format="pdf"): # Where to place the legend box - box_anchor_x = 1.02 - box_anchor_y = 1.29 + lgd_ops = plotting.legend_options(loc=ldg_loc, horiz_anchor=0.5, vert_anchor=1.29) # Histogram bin edges x_binning, y_binning = get_n_bins(df_truth) - lgd_ops = plotting.get_legend_options() - lgd_ops._replace(loc=ldg_loc) # Same number of entries in every bin as per uniform ray scan n_phi = len(y_binning) - 1 @@ -324,8 +307,8 @@ def compare_mat(df_truth, df_rec, label, detector, plot_factory, out_format="pdf errors=get_errors(df_truth, n_phi, "mat_sX0"), normalize=False, label=rf"{detector}: scan", - x_label=label_eta, - y_label=label_path_x0, + x_axis=plotting.axis_options(label=label_eta), + y_axis=plotting.axis_options(label=label_path_x0), bins=x_binning, show_stats=False, figsize=(10, 10), @@ -333,11 +316,8 @@ def compare_mat(df_truth, df_rec, label, detector, plot_factory, out_format="pdf lgd_ops=lgd_ops, ) - # Move the legend ouside plot - truth_data.lgd.set_bbox_to_anchor((box_anchor_x, box_anchor_y)) - # Add recorded data for comparison - rec_data = plot_factory.add_plot( + rec_data = plot_factory.add_hist( old_hist=truth_data, x=df_rec["eta"].to_numpy(dtype=np.double), w=df_rec["mat_sX0"] / n_phi, diff --git a/tests/tools/python/impl/plot_navigation_validation.py b/tests/tools/python/impl/plot_navigation_validation.py index 165b73fb0..fb196e387 100644 --- a/tests/tools/python/impl/plot_navigation_validation.py +++ b/tests/tools/python/impl/plot_navigation_validation.py @@ -22,10 +22,21 @@ import os +""" Construct a string in the format of the output file names and compare it""" + + +def __comp_filename(filename, det_name, file_stem, p_min="", p_max=""): + check_file_stem = f"{det_name}_{file_stem}_" in filename + check_p_min = f"_{p_min}" in filename + check_p_max = f"_{p_max}" in filename + + return check_file_stem and check_p_min and check_p_max + + """ Read the detector scan data from files and prepare data frames """ -def read_scan_data(inputdir, det_name, momentum, logging): +def read_scan_data(logging, inputdir, det_name, p_min, p_max): # Input data directory data_dir = os.fsencode(inputdir) @@ -37,13 +48,17 @@ def read_scan_data(inputdir, det_name, momentum, logging): for file in os.listdir(data_dir): filename = os.fsdecode(file) - if filename.find(det_name + "_ray_scan_intersections_" + momentum) != -1: + if __comp_filename(filename, det_name, "ray_scan_intersections"): ray_scan_intersections_file = inputdir + "/" + filename - elif filename.find(det_name + "_ray_scan_track_parameters_" + momentum) != -1: + elif __comp_filename(filename, det_name, "ray_scan_track_parameters"): ray_scan_track_param_file = inputdir + "/" + filename - elif filename.find(det_name + "_helix_scan_intersections_" + momentum) != -1: + elif __comp_filename( + filename, det_name, "helix_scan_intersections", p_min, p_max + ): helix_scan_intersections_file = inputdir + "/" + filename - elif filename.find(det_name + "_helix_scan_track_parameters_" + momentum) != -1: + elif __comp_filename( + filename, det_name, "helix_scan_track_parameters", p_min, p_max + ): helix_scan_track_param_file = inputdir + "/" + filename # Read ray scan data @@ -62,7 +77,7 @@ def read_scan_data(inputdir, det_name, momentum, logging): """ Read the recorded track positions from files and prepare data frames """ -def read_navigation_data(inputdir, det_name, momentum, read_cuda, logging): +def read_navigation_data(logging, inputdir, det_name, p_min, p_max, read_cuda): # Input data directory data_dir = os.fsencode(inputdir) @@ -74,31 +89,25 @@ def read_navigation_data(inputdir, det_name, momentum, read_cuda, logging): for file in os.listdir(data_dir): filename = os.fsdecode(file) - if ( - read_cuda - and filename.find( - det_name + "_ray_navigation_track_params_cuda_" + momentum - ) - != -1 + if read_cuda and __comp_filename( + filename, det_name, "ray_navigation_track_params_cuda" ): ray_data_cuda_file = inputdir + "/" + filename - elif filename.find(det_name + "_ray_navigation_track_params_" + momentum) != -1: + elif __comp_filename(filename, det_name, "ray_navigation_track_params"): ray_data_file = inputdir + "/" + filename - elif filename.find(det_name + "_ray_truth_track_params_" + momentum) != -1: + elif __comp_filename(filename, det_name, "ray_truth_track_params"): ray_truth_file = inputdir + "/" + filename - elif ( - read_cuda - and filename.find( - det_name + "_helix_navigation_track_params_cuda_" + momentum - ) - != -1 + elif read_cuda and __comp_filename( + filename, det_name, "helix_navigation_track_params_cuda", p_min, p_max ): helix_data_cuda_file = inputdir + "/" + filename - elif ( - filename.find(det_name + "_helix_navigation_track_params_" + momentum) != -1 + elif __comp_filename( + filename, det_name, "helix_navigation_track_params", p_min, p_max ): helix_data_file = inputdir + "/" + filename - elif filename.find(det_name + "_helix_truth_track_params_" + momentum) != -1: + elif __comp_filename( + filename, det_name, "helix_truth_track_params", p_min, p_max + ): helix_truth_file = inputdir + "/" + filename ray_df = read_track_data(ray_data_file, logging) diff --git a/tests/tools/python/impl/plot_ray_scan.py b/tests/tools/python/impl/plot_ray_scan.py index 820448ed3..cc8ce91cc 100644 --- a/tests/tools/python/impl/plot_ray_scan.py +++ b/tests/tools/python/impl/plot_ray_scan.py @@ -1,6 +1,6 @@ # Detray library, part of the ACTS project (R&D line) # -# (c) 2023-2024 CERN for the benefit of the ACTS project +# (c) 2023-2025 CERN for the benefit of the ACTS project # # Mozilla Public License Version 2.0 @@ -59,13 +59,21 @@ def plot_intersection_points_xy( ) # Plot the xy coordinates of the filtered intersections points - lgd_ops = plotting.legend_options("upper center", 4, 0.4, 0.005) + lgd_ops = plotting.legend_options( + loc="upper center", + ncol=4, + colspacing=0.4, + handletextpad=0.005, + horiz_anchor=0.5, + vert_anchor=1.095, + ) + hist_data = plot_factory.scatter( figsize=(10, 10), x=senstive_x, y=senstive_y, - x_label=r"$x\,\mathrm{[mm]}$", - y_label=r"$y\,\mathrm{[mm]}$", + x_axis=plotting.axis_options(label=r"$x\,\mathrm{[mm]}$"), + y_axis=plotting.axis_options(label=r"$y\,\mathrm{[mm]}$"), label="sensitives", color="C5", show_stats=lambda x, y: f"{n_rays} {tracks}", @@ -89,20 +97,7 @@ def plot_intersection_points_xy( plot_factory.highlight_region(hist_data, passive_x, passive_y, "C2", "passives") # Set aspect ratio - hist_data.ax.set_aspect("equal") - - # Refine legend - hist_data.lgd.legend_handles[0].set_visible(False) - for handle in hist_data.lgd.legend_handles[1:]: - handle.set_sizes([40]) - - # For this plot, move the legend ouside - hist_data.lgd.set_bbox_to_anchor((0.5, 1.095)) - - # Adjust spacing in box - for vpack in hist_data.lgd._legend_handle_box.get_children()[:1]: - for hpack in vpack.get_children(): - hpack.get_children()[0].set_width(0) + hist_data.axes.set_aspect("equal") detector_name = detector.replace(" ", "_") plot_factory.write_plot( @@ -130,13 +125,21 @@ def plot_intersection_points_rz( ) # Plot the xy coordinates of the filtered intersections points - lgd_ops = plotting.legend_options("upper center", 4, 0.8, 0.005) + lgd_ops = plotting.legend_options( + loc="upper center", + ncol=4, + colspacing=0.8, + handletextpad=0.005, + horiz_anchor=0.5, + vert_anchor=1.165, + ) + hist_data = plot_factory.scatter( figsize=(12, 6), x=sensitive_z, y=np.hypot(sensitive_x, sensitive_y), - x_label=r"$z\,\mathrm{[mm]}$", - y_label=r"$r\,\mathrm{[mm]}$", + x_axis=plotting.axis_options(label=r"$z\,\mathrm{[mm]}$"), + y_axis=plotting.axis_options(label=r"$r\,\mathrm{[mm]}$"), label="sensitives", color="C5", show_stats=lambda x, y: f"{n_rays} {tracks}", @@ -163,19 +166,6 @@ def plot_intersection_points_rz( hist_data, passive_z, np.hypot(passive_x, passive_y), "C2", "passives" ) - # Refine legend - hist_data.lgd.legend_handles[0].set_visible(False) - for handle in hist_data.lgd.legend_handles[1:]: - handle.set_sizes([45]) - - # For this plot, move the legend ouside - hist_data.lgd.set_bbox_to_anchor((0.5, 1.165)) - - # Adjust spacing in box - for vpack in hist_data.lgd._legend_handle_box.get_children()[:1]: - for hpack in vpack.get_children(): - hpack.get_children()[0].set_width(0) - detector_name = detector.replace(" ", "_") plot_factory.write_plot( hist_data, f"{detector_name}_{scan_type}_scan_rz", out_format diff --git a/tests/tools/python/impl/plot_track_params.py b/tests/tools/python/impl/plot_track_params.py index a919727b5..0d9e55790 100644 --- a/tests/tools/python/impl/plot_track_params.py +++ b/tests/tools/python/impl/plot_track_params.py @@ -1,6 +1,6 @@ # Detray library, part of the ACTS project (R&D line) # -# (c) 2024 CERN for the benefit of the ACTS project +# (c) 2024-2025 CERN for the benefit of the ACTS project # # Mozilla Public License Version 2.0 @@ -10,8 +10,8 @@ # python includes import numpy as np import pandas as pd -import os import math +import sys # Common options lgd_loc = "upper right" @@ -24,11 +24,11 @@ def read_track_data(file, logging): # Preserve floating point precision df = pd.read_csv(file, float_precision="round_trip") logging.debug(df) - else: - logging.warning("Could not find navigation data file: " + file) - df = pd.DataFrame({}) - return df + return df + else: + logging.error("Could not find navigation data file: " + file) + sys.exit(1) """ Plot the distributions of track parameter data """ @@ -36,174 +36,90 @@ def read_track_data(file, logging): def plot_track_params(opts, detector, track_type, plot_factory, out_format, df): - from matplotlib.ticker import ScalarFormatter - detector_name = detector.replace(" ", "_") - # Where to place the legend box - box_anchor_x = 1.02 - box_anchor_y = 1.245 + fig_size = (8.5, 8.5) + + # Configure the plot legend + lgd_ops = plotting.legend_options( + loc=lgd_loc, + ncol=4, + colspacing=0.8, + handletextpad=0.005, + horiz_anchor=1.02, + vert_anchor=1.245, + ) + + # Configure the y axes for all hists + # y_axis_opts = plotting.axis_options(label="", label_format="{x:3.1f}") + y_axis_opts = plotting.axis_options(label="") + + # Plot the track data + def __create_plot(x, bins, suffix, x_axis, y_axis=y_axis_opts): + hist_data = plot_factory.hist1D( + x=x, + bins=bins, + x_axis=x_axis, + y_axis=y_axis, + lgd_ops=lgd_ops, + figsize=fig_size, + ) + + plot_factory.write_plot( + hist_data, f"{detector_name}_{track_type}_{suffix}", out_format + ) # Plot the charge - lgd_ops = plotting.legend_options(lgd_loc, 4, 0.8, 0.005) - q_hist_data = plot_factory.hist1D( - x=df["q"], - bins=4, - x_label=r"$q\,\mathrm{[e]}$", - lgd_ops=lgd_ops, - figsize=(8.5, 8.5), - ax_formatter=ScalarFormatter(), - ) - - # For this plot, move the legend ouside - q_hist_data.lgd.set_bbox_to_anchor((box_anchor_x, box_anchor_y)) - - plot_factory.write_plot( - q_hist_data, f"{detector_name}_{track_type}_charge_dist", out_format - ) + x_axis_opts = plotting.axis_options(label=r"$q\,\mathrm{[e]}$") + __create_plot(x=df["q"], bins=4, x_axis=x_axis_opts, suffix="charge_dist") # Plot the total momentum p = np.sqrt(np.square(df["px"]) + np.square(df["py"]) + np.square(df["pz"])) + x_axis_opts = plotting.axis_options(label=r"$p_{tot}\,\mathrm{[GeV]}$") - lgd_ops = plotting.legend_options(lgd_loc, 4, 0.8, 0.005) - p_hist_data = plot_factory.hist1D( + __create_plot( x=p, bins=100, - x_label=r"$p_{tot}\,\mathrm{[GeV]}$", - lgd_ops=lgd_ops, - set_log=True, - figsize=(8.5, 8.5), - ax_formatter=ScalarFormatter(), - ) - - p_hist_data.lgd.set_bbox_to_anchor((box_anchor_x, box_anchor_y)) - - plot_factory.write_plot( - p_hist_data, f"{detector_name}_{track_type}_p_dist", out_format + x_axis=x_axis_opts, + y_axis=y_axis_opts._replace(log_scale=True), + suffix="p_dist", ) # Plot the transverse momentum pT = np.sqrt(np.square(df["px"]) + np.square(df["py"])) + x_axis_opts = plotting.axis_options(label=r"$p_{T}\,\mathrm{[GeV]}$") - lgd_ops = plotting.legend_options(lgd_loc, 4, 0.8, 0.005) - pT_hist_data = plot_factory.hist1D( - x=pT, - bins=100, - x_label=r"$p_{T}\,\mathrm{[GeV]}$", - lgd_ops=lgd_ops, - figsize=(8.5, 8.5), - ax_formatter=ScalarFormatter(), - ) - - pT_hist_data.lgd.set_bbox_to_anchor((box_anchor_x, box_anchor_y)) - - plot_factory.write_plot( - pT_hist_data, f"{detector_name}_{track_type}_pT_dist", out_format - ) + __create_plot(x=pT, bins=100, x_axis=x_axis_opts, suffix="pT_dist") # Plot the x-origin - lgd_ops = plotting.legend_options(lgd_loc, 4, 0.8, 0.005) - x_hist_data = plot_factory.hist1D( - x=df["x"], - bins=100, - x_label=r"$x\,\mathrm{[mm]}$", - lgd_ops=lgd_ops, - figsize=(8.5, 8.5), - ax_formatter=ScalarFormatter(), - ) - - x_hist_data.lgd.set_bbox_to_anchor((box_anchor_x, box_anchor_y)) - - plot_factory.write_plot( - x_hist_data, f"{detector_name}_{track_type}_x_origin", out_format - ) + x_axis_opts = plotting.axis_options(label=r"$x\,\mathrm{[mm]}$") + __create_plot(x=df["x"], bins=100, x_axis=x_axis_opts, suffix="x_origin") # Plot the y-origin - lgd_ops = plotting.legend_options(lgd_loc, 4, 0.8, 0.005) - y_hist_data = plot_factory.hist1D( - x=df["y"], - bins=100, - x_label=r"$y\,\mathrm{[mm]}$", - lgd_ops=lgd_ops, - figsize=(8.5, 8.5), - ax_formatter=ScalarFormatter(), - ) + x_axis_opts = plotting.axis_options(label=r"$y\,\mathrm{[mm]}$") + __create_plot(x=df["y"], bins=100, x_axis=x_axis_opts, suffix="y_origin") - y_hist_data.lgd.set_bbox_to_anchor((box_anchor_x, box_anchor_y)) - - plot_factory.write_plot( - y_hist_data, f"{detector_name}_{track_type}_y_origin", out_format - ) # Plot the z-origin - lgd_ops = plotting.legend_options(lgd_loc, 4, 0.8, 0.005) - z_hist_data = plot_factory.hist1D( - x=df["z"], - bins=100, - x_label=r"$z\,\mathrm{[mm]}$", - lgd_ops=lgd_ops, - figsize=(8.5, 8.5), - ax_formatter=ScalarFormatter(), - ) - - z_hist_data.lgd.set_bbox_to_anchor((box_anchor_x, box_anchor_y)) - - plot_factory.write_plot( - z_hist_data, f"{detector_name}_{track_type}_z_origin", out_format - ) + x_axis_opts = plotting.axis_options(label=r"$z\,\mathrm{[mm]}$") + __create_plot(x=df["z"], bins=100, x_axis=x_axis_opts, suffix="z_origin") # Plot the phi angle of the track direction phi = np.arctan2(df["py"], df["px"]) - lgd_ops = plotting.legend_options(lgd_loc, 4, 0.8, 0.005) - dir_phi_hist_data = plot_factory.hist1D( - x=phi, - bins=100, - x_label=r"$\varphi\,\mathrm{[rad]}$", - lgd_ops=lgd_ops, - figsize=(8.5, 8.5), - ax_formatter=ScalarFormatter(), - ) + x_axis_opts = plotting.axis_options(label=r"$\varphi\,\mathrm{[rad]}$") - dir_phi_hist_data.lgd.set_bbox_to_anchor((box_anchor_x, box_anchor_y)) - - plot_factory.write_plot( - dir_phi_hist_data, f"{detector_name}_{track_type}_dir_phi", out_format - ) + __create_plot(x=phi, bins=100, x_axis=x_axis_opts, suffix="dir_phi") # Plot the theta value of the track direction theta = np.arctan2(pT, df["pz"]) - lgd_ops = plotting.legend_options(lgd_loc, 4, 0.8, 0.005) - dir_theta_hist_data = plot_factory.hist1D( - x=theta, - bins=100, - x_label=r"$\theta\,\mathrm{[rad]}$", - lgd_ops=lgd_ops, - figsize=(8.5, 8.5), - ax_formatter=ScalarFormatter(), - ) + x_axis_opts = plotting.axis_options(label=r"$\theta\,\mathrm{[rad]}$") - dir_theta_hist_data.lgd.set_bbox_to_anchor((box_anchor_x, box_anchor_y)) - - plot_factory.write_plot( - dir_theta_hist_data, f"{detector_name}_{track_type}_dir_theta", out_format - ) + __create_plot(x=theta, bins=100, x_axis=x_axis_opts, suffix="dir_theta") # Plot the eta value of the track direction eta = np.arctanh(df["pz"] / p) - lgd_ops = plotting.legend_options(lgd_loc, 4, 0.8, 0.005) - dir_eta_hist_data = plot_factory.hist1D( - x=eta, - bins=100, - x_label=r"$\eta$", - lgd_ops=lgd_ops, - figsize=(8.5, 8.5), - ax_formatter=ScalarFormatter(), - ) + x_axis_opts = plotting.axis_options(label=r"$\eta$") - dir_eta_hist_data.lgd.set_bbox_to_anchor((box_anchor_x, box_anchor_y)) - - plot_factory.write_plot( - dir_eta_hist_data, f"{detector_name}_{track_type}_dir_eta", out_format - ) + __create_plot(x=eta, bins=100, x_axis=x_axis_opts, suffix="dir_eta") """ Plot the track positions of two data sources - rz view """ @@ -241,13 +157,21 @@ def compare_track_pos_xy( ) # Plot the xy coordinates of the filtered track positions - lgd_ops = plotting.legend_options("upper center", 4, 0.4, 0.005) + lgd_ops = plotting.legend_options( + loc="upper center", + ncol=4, + colspacing=0.4, + handletextpad=0.005, + horiz_anchor=0.5, + vert_anchor=1.095, + ) + hist_data = plot_factory.scatter( figsize=(10, 10), x=first_x, y=first_y, - x_label=r"$x\,\mathrm{[mm]}$", - y_label=r"$y\,\mathrm{[mm]}$", + x_axis=plotting.axis_options(label=r"$x\,\mathrm{[mm]}$"), + y_axis=plotting.axis_options(label=r"$y\,\mathrm{[mm]}$"), label=label1, color=color1, alpha=1.0, @@ -258,9 +182,6 @@ def compare_track_pos_xy( # Compare agaist second data set plot_factory.highlight_region(hist_data, second_x, second_y, color2, label2) - # For this plot, move the legend ouside - hist_data.lgd.set_bbox_to_anchor((0.5, 1.095)) - detector_name = detector.replace(" ", "_") l1 = label1.replace(" ", "_").replace("(", "").replace(")", "") l2 = label2.replace(" ", "_").replace("(", "").replace(")", "") @@ -303,13 +224,21 @@ def compare_track_pos_rz( ) # Plot the xy coordinates of the filtered intersections points - lgd_ops = plotting.legend_options("upper center", 4, 0.8, 0.005) + lgd_ops = plotting.legend_options( + loc="upper center", + ncol=4, + colspacing=0.8, + handletextpad=0.005, + horiz_anchor=0.5, + vert_anchor=1.168, + ) + hist_data = plot_factory.scatter( figsize=(12, 6), x=first_z, y=np.hypot(first_x, first_y), - x_label=r"$z\,\mathrm{[mm]}$", - y_label=r"$r\,\mathrm{[mm]}$", + x_axis=plotting.axis_options(label=r"$z\,\mathrm{[mm]}$"), + y_axis=plotting.axis_options(label=r"$r\,\mathrm{[mm]}$"), label=label1, color=color1, alpha=1.0, @@ -322,9 +251,6 @@ def compare_track_pos_rz( hist_data, second_z, np.hypot(second_x, second_y), color2, label2 ) - # For this plot, move the legend ouside - hist_data.lgd.set_bbox_to_anchor((0.5, 1.168)) - detector_name = detector.replace(" ", "_") l1 = label1.replace(" ", "_").replace("(", "").replace(")", "") l2 = label2.replace(" ", "_").replace("(", "").replace(")", "") @@ -346,9 +272,6 @@ def plot_track_pos_dist( ): tracks = "rays" if scan_type == "ray" else "helices" - # Where to place the legend box - box_anchor_x = 1.02 - box_anchor_y = 1.24 dist = np.sqrt( np.square(df1["x"] - df2["x"]) @@ -369,19 +292,26 @@ def plot_track_pos_dist( track_id = (df1["track_id"].to_numpy())[i] print(f"track {track_id}: {d}") + # Where to place the legend box + lgd_ops = plotting.legend_options( + loc=lgd_loc, + ncol=4, + colspacing=0.8, + handletextpad=0.005, + horiz_anchor=1.02, + vert_anchor=1.24, + ) + # Plot the xy coordinates of the filtered intersections points - lgd_ops = plotting.legend_options(lgd_loc, 4, 0.8, 0.005) hist_data = plot_factory.hist1D( x=filtered_dist, bins=100, - x_label=r"$d\,\mathrm{[mm]}$", - set_log=True, + x_axis=plotting.axis_options(label=r"$d\,\mathrm{[mm]}$"), + y_axis=plotting.axis_options(label="", log_scale=True), figsize=(8.5, 8.5), lgd_ops=lgd_ops, ) - hist_data.lgd.set_bbox_to_anchor((box_anchor_x, box_anchor_y)) - detector_name = detector.replace(" ", "_") l1 = label1.replace(" ", "_").replace("(", "").replace(")", "") l2 = label2.replace(" ", "_").replace("(", "").replace(")", "") @@ -398,9 +328,6 @@ def plot_track_pos_res( ): tracks = "rays" if scan_type == "ray" else "helices" - # Where to place the legend box - box_anchor_x = 1.02 - box_anchor_y = 1.28 res = df1[var] - df2[var] @@ -421,14 +348,23 @@ def plot_track_pos_res( else: o_out = o_out + 1 + lgd_ops = plotting.legend_options( + loc=lgd_loc, + ncol=4, + colspacing=0.01, + handletextpad=0.0005, + horiz_anchor=1.02, + vert_anchor=1.28, + ) + # Plot the xy coordinates of the filtered intersections points - lgd_ops = plotting.legend_options(lgd_loc, 4, 0.01, 0.0005) hist_data = plot_factory.hist1D( x=filtered_res, figsize=(9, 9), bins=100, - x_label=r"$\mathrm{res}" + rf"\,{var}" + r"\,\mathrm{[mm]}$", - set_log=False, + x_axis=plotting.axis_options( + label=r"$\mathrm{res}" + rf"\,{var}" + r"\,\mathrm{[mm]}$" + ), lgd_ops=lgd_ops, u_outlier=u_out, o_outlier=o_out, @@ -438,9 +374,6 @@ def plot_track_pos_res( if mu is None or sig is None: print(rf"WARNING: fit failed (res ({tracks}): {label1} - {label2} )") - # Move the legend ouside plo - hist_data.lgd.set_bbox_to_anchor((box_anchor_x, box_anchor_y)) - detector_name = detector.replace(" ", "_") l1 = label1.replace(" ", "_").replace("(", "").replace(")", "") l2 = label2.replace(" ", "_").replace("(", "").replace(")", "") diff --git a/tests/tools/python/material_validation.py b/tests/tools/python/material_validation.py index ba7fce0ee..041c9fb0c 100644 --- a/tests/tools/python/material_validation.py +++ b/tests/tools/python/material_validation.py @@ -1,6 +1,6 @@ # Detray library, part of the ACTS project (R&D line) # -# (c) 2023-2024 CERN for the benefit of the ACTS project +# (c) 2023-2025 CERN for the benefit of the ACTS project # # Mozilla Public License Version 2.0 @@ -20,6 +20,8 @@ parse_detector_io_options, parse_plotting_options, ) +from utils import read_detector_name +from utils import add_track_generator_args, add_propagation_args, add_detector_io_args # python includes import argparse @@ -31,7 +33,7 @@ def __main__(): - # ----------------------------------------------------------------arg parsing + # ---------------------------------------------------------------arg parsing descr = "Detray Material Validation" @@ -88,42 +90,26 @@ def __main__(): logging.error(f"Material validation binaries were not found! ({args.bindir})") sys.exit(1) - # ------------------------------------------------------------------------run + # -----------------------------------------------------------------------run # Pass on the options for the validation tools args_list = [ - "--geometry_file", - args.geometry_file, - "--material_file", - args.material_file, - "--phi_steps", - str(args.phi_steps), - "--eta_steps", - str(args.eta_steps), - "--eta_range", - str(args.eta_range[0]), - str(args.eta_range[1]), "--tol", str(args.tolerance), - "--min_mask_tolerance", - str(args.min_mask_tol), - "--max_mask_tolerance", - str(args.max_mask_tol), - "--overstep_tolerance", - str(args.overstep_tol), - "--path_tolerance", - str(args.path_tol), - "--rk-tolerance", - str(args.rk_error_tol), - "--path_limit", - str(args.path_limit), - "--search_window", - str(args.search_window[0]), - str(args.search_window[1]), ] - if args.grid_file: - args_list = args_list + ["--grid_file", args.grid_file] + # Add parsed options to argument list + add_detector_io_args(args_list, args) + add_track_generator_args(args_list, args) + add_propagation_args(args_list, args) + + logging.debug(args_list) + + if "--material_file" not in args_list: + logging.error( + "Detector material is required! Please add it using the '--material_file' option" + ) + sys.exit(1) # Run the host validation and produce the truth data logging.debug("Running CPU material validation") @@ -140,14 +126,11 @@ def __main__(): if args.sycl: logging.error("SYCL material validation is not implemented") - # ------------------------------------------------------------------------plot + # ----------------------------------------------------------------------plot logging.info("Generating data plots...\n") - geo_file = open(args.geometry_file) - json_geo = json.loads(geo_file.read()) - - det_name = json_geo["header"]["common"]["detector"] + det_name = read_detector_name(args.geometry_file, logging) logging.debug("Detector: " + det_name) df_scan, df_cpu, df_cuda = read_material_data(in_dir, logging, det_name, args.cuda) @@ -204,9 +187,9 @@ def __main__(): ) -# ------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ if __name__ == "__main__": __main__() -# ------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ diff --git a/tests/tools/python/navigation_validation.py b/tests/tools/python/navigation_validation.py index d185a71bb..90ebd232d 100644 --- a/tests/tools/python/navigation_validation.py +++ b/tests/tools/python/navigation_validation.py @@ -1,6 +1,6 @@ # Detray library, part of the ACTS project (R&D line) # -# (c) 2023-2024 CERN for the benefit of the ACTS project +# (c) 2023-2025 CERN for the benefit of the ACTS project # # Mozilla Public License Version 2.0 @@ -21,6 +21,8 @@ parse_plotting_options, ) from plotting import pyplot_factory as plt_factory +from utils import read_detector_name, get_p_range +from utils import add_track_generator_args, add_propagation_args, add_detector_io_args # python imports import argparse @@ -32,7 +34,7 @@ def __main__(): - # ----------------------------------------------------------------arg parsing + # ---------------------------------------------------------------arg parsing descr = "Detray Navigation Validation" @@ -120,47 +122,17 @@ def __main__(): logging.error(f"Navigation validation binaries were not found! ({args.bindir})") sys.exit(1) - # ------------------------------------------------------------------------run + # -----------------------------------------------------------------------run # Pass on the options for the validation tools - args_list = [ - "--data_dir", - datadir, - "--geometry_file", - args.geometry_file, - "--n_tracks", - str(args.n_tracks), - "--randomize_charge", - str(args.randomize_charge), - "--p_T", - str(args.transverse_momentum), - "--eta_range", - str(args.eta_range[0]), - str(args.eta_range[1]), - "--min_mask_tolerance", - str(args.min_mask_tol), - "--max_mask_tolerance", - str(args.max_mask_tol), - "--mask_tolerance_scalor", - str(args.mask_tol_scalor), - "--overstep_tolerance", - str(args.overstep_tol), - "--path_tolerance", - str(args.path_tol), - "--rk-tolerance", - str(args.rk_error_tol), - "--path_limit", - str(args.path_limit), - "--search_window", - str(args.search_window[0]), - str(args.search_window[1]), - ] + args_list = ["--data_dir", datadir] - if args.grid_file: - args_list = args_list + ["--grid_file", args.grid_file] + # Add parsed options to argument list + add_detector_io_args(args_list, args) + add_track_generator_args(args_list, args) + add_propagation_args(args_list, args) - if args.material_file: - args_list = args_list + ["--material_file", args.material_file] + logging.debug(args_list) # Run the host validation and produce the truth data logging.debug("Running CPU validation") @@ -177,14 +149,11 @@ def __main__(): if args.sycl: logging.error("SYCL validation is not implemented") - # ------------------------------------------------------------------------plot + # ----------------------------------------------------------------------plot logging.info("Generating data plots...\n") - geo_file = open(args.geometry_file) - json_geo = json.loads(geo_file.read()) - - det_name = json_geo["header"]["common"]["detector"] + det_name = read_detector_name(args.geometry_file, logging) logging.debug("Detector: " + det_name) # Check the data path (should have been created when running the validation) @@ -195,8 +164,9 @@ def __main__(): plot_factory = plt_factory(out_dir, logging) # Read the truth data + p_min, p_max = get_p_range(args, logging) ray_scan_df, helix_scan_df = read_scan_data( - datadir, det_name, str(args.transverse_momentum), logging + logging, datadir, det_name, p_min, p_max ) plot_detector_scan_data(args, det_name, plot_factory, "ray", ray_scan_df, "png") @@ -221,9 +191,7 @@ def __main__(): helix_nav_df, helix_truth_df, helix_nav_cuda_df, - ) = read_navigation_data( - datadir, det_name, str(args.transverse_momentum), args.cuda, logging - ) + ) = read_navigation_data(logging, datadir, det_name, p_min, p_max, args.cuda) # Plot label_cpu = "navigation (CPU)" @@ -305,9 +273,9 @@ def __main__(): ) -# ------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ if __name__ == "__main__": __main__() -# ------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ diff --git a/tests/tools/python/options/common_options.py b/tests/tools/python/options/common_options.py index bd2f99874..8ab6d8cf9 100644 --- a/tests/tools/python/options/common_options.py +++ b/tests/tools/python/options/common_options.py @@ -9,9 +9,9 @@ import sys from datetime import datetime -# ------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ # Options parsing -# ------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ """ Parent parser that contains common options """ @@ -67,6 +67,3 @@ def parse_common_options(args, prog_name=sys.argv[0]): ) return logging - - -# ------------------------------------------------------------------------------- diff --git a/tests/tools/python/options/detector_io_options.py b/tests/tools/python/options/detector_io_options.py index 9bc8c3a5c..a326afbf5 100644 --- a/tests/tools/python/options/detector_io_options.py +++ b/tests/tools/python/options/detector_io_options.py @@ -8,11 +8,11 @@ import os import sys -# ------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ # Options parsing -# ------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ -""" Parent detector reader that contains common options """ +""" Parent detector reader options that contain common options """ def detector_io_options(): @@ -58,6 +58,3 @@ def parse_detector_io_options(args, logging): if args.material_file and not os.path.isfile(args.material_file): logging.error(f"Detector material file does not exist! ({args.material_file})") sys.exit(1) - - -# ------------------------------------------------------------------------------- diff --git a/tests/tools/python/options/plotting_options.py b/tests/tools/python/options/plotting_options.py index e3588f39c..7d5b61583 100644 --- a/tests/tools/python/options/plotting_options.py +++ b/tests/tools/python/options/plotting_options.py @@ -8,9 +8,9 @@ import os import sys -# ------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ # Options parsing -# ------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ """ Parent parser that contains plotting options """ @@ -37,7 +37,7 @@ def plotting_options(): "--output_format", "-of", help=("Format of the plot files (svg|png|pdf)."), - default="png", + default="pdf", type=str, ) @@ -63,6 +63,3 @@ def parse_plotting_options(args, logging): sys.exit(1) return args.inputdir, args.outdir, args.output_format - - -# ------------------------------------------------------------------------------- diff --git a/tests/tools/python/options/propagation_options.py b/tests/tools/python/options/propagation_options.py index 533510a96..45d108c00 100644 --- a/tests/tools/python/options/propagation_options.py +++ b/tests/tools/python/options/propagation_options.py @@ -6,9 +6,9 @@ import argparse -# ------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ # Options parsing -# ------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ """ Parent parser that contains propagation options """ @@ -29,7 +29,7 @@ def propagation_options(): "--max_mask_tol", "-max_mtol", help=("Max. mask tolerance [mm]"), - default=1, + default=3, type=float, ) parser.add_argument( @@ -46,7 +46,7 @@ def propagation_options(): "--overstep_tol", "-otol", help=("Overstep tolerance [um]"), - default=-100, + default=-300, type=float, ) parser.add_argument( @@ -80,13 +80,6 @@ def propagation_options(): default=0.0001, type=float, ) - parser.add_argument( - "--max_n_steps", - "-n_step", - help=("Max. Runge-Kutta step updates"), - default=10000, - type=int, - ) parser.add_argument( "--path_limit", "-plim", @@ -99,14 +92,14 @@ def propagation_options(): "-bethe", help=("Use Bethe energy loss"), action="store_true", - default=True, + default=False, ) parser.add_argument( "--covariance_transport", "-cov_trnsp", help=("Do covaraiance transport"), action="store_true", - default=True, + default=False, ) parser.add_argument( "--energy_loss_grad", @@ -124,6 +117,3 @@ def propagation_options(): ) return parser - - -# ------------------------------------------------------------------------------- diff --git a/tests/tools/python/options/track_generator_options.py b/tests/tools/python/options/track_generator_options.py index c495738a7..0ed6bbcbb 100644 --- a/tests/tools/python/options/track_generator_options.py +++ b/tests/tools/python/options/track_generator_options.py @@ -6,26 +6,34 @@ import argparse -# ------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ # Options parsing -# ------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ -""" Parent parser that contains propagation options """ +""" Adds options that are common to all track generators """ -def random_track_generator_options(): - - parser = argparse.ArgumentParser(add_help=False) +def common_track_generator_options(parser): - # Navigation options parser.add_argument( - "--n_tracks", "-n", help=("Number of test tracks"), default=100, type=int + "--random_seed", + "-seed", + help=("Seed for the random number generator"), + default=5489, + type=int, ) parser.add_argument( - "--transverse-momentum", - "-p_T", - help=("Transverse momentum of the test particle [GeV]"), - default=10, + "--pT_range", + "-pTr", + nargs=2, + help=("Transverse momentum [range] of the test particle [GeV]"), + type=float, + ) + parser.add_argument( + "--p_range", + "-pr", + nargs=2, + help=("Total momentum [range] of the test particle [GeV]"), type=float, ) parser.add_argument( @@ -41,19 +49,37 @@ def random_track_generator_options(): "-rand_chrg", help=("Randomly flip charge sign per track"), action="store_true", - default=True, + default=False, + ) + + return parser + + +""" Parent parser that contains random track generator options """ + + +def random_track_generator_options(): + + parser = argparse.ArgumentParser(add_help=False) + + common_track_generator_options(parser) + + parser.add_argument( + "--n_tracks", "-n", help=("Number of test tracks"), default=100, type=int ) return parser -""" Parent parser that contains propagation options """ +""" Parent parser that contains uniform track generator options """ def uniform_track_generator_options(): parser = argparse.ArgumentParser(add_help=False) + common_track_generator_options(parser) + # Navigation options parser.add_argument( "--phi_steps", "-n_phi", help=("Number steps in phi"), default=50, type=int @@ -61,23 +87,5 @@ def uniform_track_generator_options(): parser.add_argument( "--eta_steps", "-n_eta", help=("Number steps in eta"), default=50, type=int ) - parser.add_argument( - "--transverse-momentum", - "-p_T", - help=("Transverse momentum of the test particle [GeV]"), - default=10, - type=float, - ) - parser.add_argument( - "--eta_range", - "-eta", - nargs=2, - help=("Eta range of generated tracks"), - default=[-4, 4], - type=float, - ) return parser - - -# ------------------------------------------------------------------------------- diff --git a/tests/tools/python/plotting/__init__.py b/tests/tools/python/plotting/__init__.py index 801cf873c..a3485a092 100644 --- a/tests/tools/python/plotting/__init__.py +++ b/tests/tools/python/plotting/__init__.py @@ -1,3 +1,3 @@ +from .plot_helpers import plt_data, axis_options, legend_options from .plot_helpers import filter_data from .pyplot_factory import pyplot_factory -from .pyplot_factory import legend_options, get_legend_options diff --git a/tests/tools/python/plotting/plot_helpers.py b/tests/tools/python/plotting/plot_helpers.py index 0d9b746cc..71835d68a 100644 --- a/tests/tools/python/plotting/plot_helpers.py +++ b/tests/tools/python/plotting/plot_helpers.py @@ -1,15 +1,40 @@ # Detray library, part of the ACTS project (R&D line) # -# (c) 2023-2024 CERN for the benefit of the ACTS project +# (c) 2023-2025 CERN for the benefit of the ACTS project # # Mozilla Public License Version 2.0 from collections import namedtuple import numpy as np -# ------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ +# Common helper types to configure plots +# ------------------------------------------------------------------------------ + +""" Pass plotting data between functions """ +plt_data = namedtuple( + "plt_data", + "fig axes lgd data bins mu rms errors", + defaults=[None, None, None, None, 0, -1, -1, None], +) + +""" Configuration for plot axes """ +axis_options = namedtuple( + "axis_options", + "label min max log_scale tick_positions label_format", + defaults=["x", None, None, False, None, None], +) + +""" Configuration for plot legends """ +legend_options = namedtuple( + "legend_options", + "title loc ncol colspacing handletextpad horiz_anchor vert_anchor", + defaults=[None, "upper right", 1, 1, 1, 0.5, 0.5], +) + +# ------------------------------------------------------------------------------ # Common helpers for plotting measurement data -# ------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ """ Filter the data in a data frame by a given prescription """ @@ -29,6 +54,3 @@ def filter_data(data, filter=lambda df: [], variables=[]): data_coll.append(filtered[var].to_numpy(dtype=np.double)) return tuple(data_coll) - - -# ------------------------------------------------------------------------------- diff --git a/tests/tools/python/plotting/pyplot_factory.py b/tests/tools/python/plotting/pyplot_factory.py index c03d04494..89d147a75 100644 --- a/tests/tools/python/plotting/pyplot_factory.py +++ b/tests/tools/python/plotting/pyplot_factory.py @@ -1,6 +1,6 @@ # Detray library, part of the ACTS project (R&D line) # -# (c) 2023 CERN for the benefit of the ACTS project +# (c) 2023-2025 CERN for the benefit of the ACTS project # # Mozilla Public License Version 2.0 @@ -12,10 +12,12 @@ # python based plotting import matplotlib.pyplot as plt import matplotlib.colors as mcolors -from matplotlib.ticker import ScalarFormatter +from matplotlib import ticker +import matplotlib.style as style from mpl_toolkits.axes_grid1 import make_axes_locatable -import matplotlib.style as style +# detray imports +from .plot_helpers import plt_data, axis_options, legend_options style.use("tableau-colorblind10") # style.use('seaborn-colorblind') @@ -30,37 +32,23 @@ # See: https://stackoverflow.com/questions/42142144/displaying-first-decimal-digit-in-scientific-notation-in-matplotlib -class ScalarFormatterForceFormat(ScalarFormatter): +class ScalarFormatterForceFormat(ticker.ScalarFormatter): def _set_format(self): self.format = "%3.1f" -# ------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ # Global identifiers -# ------------------------------------------------------------------------------- - -""" Pass plotting data between functions """ -plt_data = namedtuple("plt_data", "fig ax lgd data bins mu rms errors") - -""" Wrap the configuration for a legend """ -legend_options = namedtuple("legend_options", "loc ncol colspacing handletextpad") +# ------------------------------------------------------------------------------ +""" Default color for graphs and histograms """ default_color = "tab:blue" -""" Conveniently get the legend options """ - - -def get_legend_options(): - return legend_options("upper right", 1, 1, 1) - - -# ------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ # Data Plotting -# ------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ -""" -Plotter interface that uses pyplot/matplotlib. -""" +""" Plotter interface that uses pyplot/matplotlib. """ class pyplot_factory: @@ -74,17 +62,63 @@ def __init__(self, out_dir, logger, atlas_badge=""): self.axis_formatter = ScalarFormatterForceFormat() self.axis_formatter.set_powerlimits((-2, 2)) - """ Add legend to a plot. Labbels must be defined. """ - - def add_legend(self, ax, options=get_legend_options()): + # Add legend to a plot. Labels must be defined. + def __add_legend(self, ax, options=legend_options()): return ax.legend( + title=options.title, loc=options.loc, + bbox_to_anchor=(options.horiz_anchor, options.vert_anchor), ncol=options.ncol, borderpad=0.3, columnspacing=options.colspacing, handletextpad=options.handletextpad, ) + # Adjust label spacing in legend + def __adjust_lgd_label_spacing(self, lgd): + # Refine legend + lgd.legend_handles[0].set_visible(False) + for handle in lgd.legend_handles[1:]: + handle.set_sizes([40]) + + # Adjust spacing in box + for vpack in lgd._legend_handle_box.get_children()[:1]: + for hpack in vpack.get_children(): + hpack.get_children()[0].set_width(0) + + # Update after adding new entry to existing legend + def __update_legend(self, lgd): + handles, labels = lgd.axes.get_legend_handles_labels() + lgd._legend_box = None + lgd._init_legend_box(handles, labels) + lgd._set_loc(lgd._loc) + lgd.set_title(lgd.get_title().get_text()) + + # Find the axis boundaries either from data or custom boundaries + def __get_axis_boundaries(self, data, axis_opts): + if axis_opts.min is not None and axis_opts.max is not None: + return axis_opts.min, axis_opts.max + else: + return np.min(data), np.max(data) + + # Apply boundary to input data + def __apply_boundary(self, data, min_v, max_v): + if min_v is not None and max_v is not None: + out = data[np.nonzero(data >= min_v)] + out = out[np.nonzero(out <= max_v)] + return out + else: + return data + + # Set axis tick label formatting + def __set_label_format(self, label_format, axis, offset=True): + if label_format is not None: + tick_formatter = ticker.StrMethodFormatter(label_format) + axis.set_major_formatter(tick_formatter) + axis.set_minor_formatter(tick_formatter) + else: + axis.set_major_formatter(self.axis_formatter) + """ Create a histogram from given input data. The normalization is achieved by dividing the bin count by the total number of observations. The error is @@ -94,46 +128,49 @@ def add_legend(self, ax, options=get_legend_options()): def hist1D( self, x, + bins=1, errors=None, w=None, - x_label="x", - y_label="", title="", label="", - x_min=None, - x_max=None, - bins=1, + x_axis=axis_options(label="x"), + y_axis=axis_options(label=""), color=default_color, alpha=0.75, - set_log=False, normalize=False, show_error=False, show_stats=True, u_outlier=-1, o_outlier=-1, + lgd_ops=legend_options(), figsize=(8, 8), - lgd_ops=get_legend_options(), - layout="constrained", - ax_formatter=None, + layout="compressed", ): # Create fresh plot fig = plt.figure(figsize=figsize, layout=layout) ax = fig.add_subplot(1, 1, 1) - if ax_formatter is None: - ax.xaxis.set_major_formatter(self.axis_formatter) - ax.yaxis.set_major_formatter(self.axis_formatter) + # Refine plot + ax.set_title(title) + ax.set_xlabel(x_axis.label) + ax.set_ylabel(y_axis.label) + ax.grid(True, alpha=0.25) + + # Plot log scale + if x_axis.log_scale: + ax.set_xscale("log") + if y_axis.log_scale: + ax.set_yscale("log") - ax.xaxis.set_major_formatter(ScalarFormatter(useOffset=False)) + # Leave x-axis with default formatter for 1D histograms + ax.xaxis.set_major_formatter(ticker.ScalarFormatter()) + self.__set_label_format(y_axis.label_format, ax.yaxis, True) # Do calculations on data in the range of the histogram - if x_min is not None and x_max is not None: - x = x[np.nonzero(x >= x_min)] - x = x[np.nonzero(x <= x_max)] - else: - x_min = np.min(x) - x_max = np.max(x) + x_min, x_max = self.__get_axis_boundaries(x, x_axis) + if x_axis.min is not None and x_axis.max is not None: + x = self.__apply_boundary(x, x_min, x_max) # Display number of entries in under- and overflow bins underflow = len(np.argwhere(x < x_min)) @@ -145,7 +182,7 @@ def hist1D( # Nothing left to do if len(x) == 0: self.logger.debug(rf" create hist: empty data {label}") - return plt_data(fig, ax, None, None, None, None, None, None) + return plt_data(fig=fig, axes=ax) # Histogram normalization scale = 1.0 / len(x) if normalize else 1.0 @@ -153,7 +190,7 @@ def hist1D( # Format the 'newline' newline = "\n" - # Name of the datat collection + # Name of the data collection label_str = f"{label} ({len(x)} entries)" if u_outlier >= 0 or o_outlier >= 0: label_str = ( @@ -194,23 +231,6 @@ def hist1D( mean = None stdev = None - # Refine plot - ax.set_title(title) - ax.set_xlabel(x_label) - ax.set_ylabel(y_label) - ax.grid(True, alpha=0.25) - - # Add legend - lgd = self.add_legend(ax, lgd_ops) - - # Adjust spacing in box - lgd.legend_handles[0].set_visible(False) - if show_stats: - lgd.legend_handles[1].set_visible(False) - for vpack in lgd._legend_handle_box.get_children(): - for hpack in vpack.get_children(): - hpack.get_children()[0].set_width(0) - # Calculate the bin error bin_centers = 0.5 * (bins[1:] + bins[:-1]) err = np.sqrt(scale * data) if errors is None else errors @@ -226,15 +246,31 @@ def hist1D( capsize=2.5, ) - # Plot log scale - if set_log: - ax.set_yscale("log") + # Add legend + lgd = self.__add_legend(ax, lgd_ops) + + # Adjust spacing in box + lgd.legend_handles[0].set_visible(False) + if show_stats: + lgd.legend_handles[1].set_visible(False) + for vpack in lgd._legend_handle_box.get_children(): + for hpack in vpack.get_children(): + hpack.get_children()[0].set_width(0) - return plt_data(fig, ax, lgd, data, bins, mean, stdev, err) + return plt_data( + fig=fig, + axes=ax, + lgd=lgd, + data=data, + bins=bins, + mu=mean, + rms=stdev, + errors=err, + ) - """ Add new data to an existing plot """ + """ Add new histogram to an existing plot """ - def add_plot( + def add_hist( self, old_hist, x, @@ -247,12 +283,8 @@ def add_plot( show_error=False, ): - # do calculations on data in the range of the histogram - x_min = np.min(old_hist.bins) - x_max = np.max(old_hist.bins) - - x = x[np.nonzero(x >= x_min)] - x = x[np.nonzero(x <= x_max)] + # Do calculations on data in the range of the histogram + x = self.__apply_boundary(x, np.min(old_hist.bins), np.max(old_hist.bins)) # Nothing left to do if len(x) == 0 or old_hist.data is None: @@ -261,7 +293,7 @@ def add_plot( # Add new data to old hist axis scale = 1.0 / len(x) if normalize else 1.0 - data, bins, _ = old_hist.ax.hist( + data, bins, _ = old_hist.axes.hist( x=x, bins=old_hist.bins, label=f"{label} ({len(x)} entries)", @@ -271,19 +303,11 @@ def add_plot( edgecolor=color, ) - # Update legend - lgd = old_hist.lgd - handles, labels = lgd.axes.get_legend_handles_labels() - lgd._legend_box = None - lgd._init_legend_box(handles, labels) - lgd._set_loc(lgd._loc) - lgd.set_title(lgd.get_title().get_text()) - # Calculate the bin error bin_centers = 0.5 * (bins[1:] + bins[:-1]) err = np.sqrt(scale * data) if errors is None else errors if show_error or errors is not None: - old_hist.ax.errorbar( + old_hist.axes.errorbar( bin_centers, data, yerr=err, @@ -294,8 +318,16 @@ def add_plot( capsize=2.5, ) + # Update legend + self.__update_legend(old_hist.lgd) + return plt_data( - old_hist.fig, old_hist.ax, old_hist.lgd, data, bins, None, None, err + fig=old_hist.fig, + axes=old_hist.axes, + lgd=old_hist.lgd, + data=data, + bins=bins, + errors=err, ) """ @@ -311,17 +343,17 @@ def add_ratio( nom.fig.set_figwidth(8) if nom.bins is None or denom.bins is None: - return plt_data(nom.fig, nom.ax, None, None, None, None, None, None) + return plt_data(fig=nom.fig, axes=nom.axes) if len(nom.bins) != len(denom.bins): - return plt_data(nom.fig, nom.ax, None, None, None, None, None, None) + return plt_data(fig=nom.fig, axes=nom.axes) # Remove ticks/labels that are already visible on the ratio plot - old_x_label = nom.ax.xaxis.get_label().get_text() - nom.ax.tick_params( + x_label = nom.axes.xaxis.get_label().get_text() + nom.axes.tick_params( axis="x", which="both", bottom=True, top=False, labelbottom=False ) - nom.ax.set_xlabel("") + nom.axes.set_xlabel("") # Don't print a warning when dividing by zero with np.errstate(divide="ignore"), np.errstate(invalid="ignore"): @@ -344,11 +376,15 @@ def add_ratio( posinf=0, ) - # create new axes on the bottom of the current axes + # Create new axes on the bottom of the current axes # The first argument of the new_vertical(new_horizontal) method is # the height (width) of the axes to be created in inches. - divider = make_axes_locatable(nom.ax) - ratio_plot = divider.append_axes("bottom", 1.2, pad=0.2, sharex=nom.ax) + divider = make_axes_locatable(nom.axes) + ratio_plot = divider.append_axes("bottom", 1.2, pad=0.2, sharex=nom.axes) + # Ratio should be around 1: Don't use scientific notation/offset + ratio_plot.axes.yaxis.set_major_formatter( + ticker.ScalarFormatter(useOffset=False) + ) if show_error: ratio_plot.errorbar( bin_centers, ratio, yerr=errors, label=label, color=color, fmt="." @@ -362,10 +398,12 @@ def add_ratio( marker=".", linestyle="", ) + # Refine plot - ratio_plot.set_xlabel(old_x_label) + ratio_plot.set_xlabel(x_label) ratio_plot.set_ylabel("ratio") ratio_plot.grid(True, alpha=0.25) + # Plot log scale if set_log: ratio_plot.set_yscale("log") @@ -373,13 +411,9 @@ def add_ratio( # Add a horizontal blue line at y = 1. ratio_plot.axline((nom.bins[0], 1), (nom.bins[-1], 1), linewidth=1, color="b") - # Add legend - # lgd = self.add_legend(ratio_plot) - # Move the legend - # lgd.set_bbox_to_anchor((box_anchor_x, box_anchor_y)) nom.fig.set_size_inches((9, 9)) - return plt_data(nom.fig, ratio_plot, None, None, None, None, None, errors) + return plt_data(fig=nom.fig, axes=ratio_plot, errors=errors) """ Create a 2D histogram from given input data. If z values are given they will @@ -391,17 +425,13 @@ def hist2D( x, y, z=None, - x_label="x", - y_label="y", - z_label="", - title="", - label="", - x_min=None, - x_max=None, x_bins=1, - y_min=None, - y_max=None, y_bins=1, + x_axis=axis_options(label="x"), + y_axis=axis_options(label="y"), + z_axis=axis_options(label=""), + title="", + label="", color=default_color, alpha=0.75, show_stats=True, @@ -412,25 +442,24 @@ def hist2D( fig = plt.figure(figsize=figsize, layout="constrained") ax = fig.add_subplot(1, 1, 1) + # Refine plot + ax.set_title(title) + ax.set_xlabel(x_axis.label) + ax.set_ylabel(y_axis.label) + # Do calculations on data in the range of the histogram - if x_min is not None and x_max is not None: - x = x[np.nonzero(x >= x_min)] - x = x[np.nonzero(x <= x_max)] - else: - x_min = np.min(x) - x_max = np.max(x) + x_min, x_max = self.__get_axis_boundaries(x, x_axis) + if x_axis.min is not None and x_axis.max is not None: + x = self.__apply_boundary(x, x_min, x_max) - if y_min is not None and y_max is not None: - y = y[np.nonzero(y >= y_min)] - y = y[np.nonzero(y <= y_max)] - else: - y_min = np.min(y) - y_max = np.max(y) + y_min, y_max = self.__get_axis_boundaries(y, y_axis) + if y_axis.min is not None and y_axis.max is not None: + y = self.__apply_boundary(y, y_min, y_max) # Nothing left to do if len(x) == 0 or len(y) == 0: self.logger.debug(rf" create hist: empty data {label}") - return plt_data(fig, ax, None, None, None, None, None, None) + return plt_data(fig=fig, axes=ax) # Fill data data, _, _, hist = ax.hist2d( @@ -464,15 +493,10 @@ def hist2D( rf"{newline}yRMS = {y_rms:.2e}", ) - # Refine plot - ax.set_title(title) - ax.set_xlabel(x_label) - ax.set_ylabel(y_label) - # Add the colorbar - fig.colorbar(hist, label=z_label) + fig.colorbar(hist, label=z_axis.label) - return plt_data(fig, ax, None, data, None, None, None, None) + return plt_data(fig=fig, axes=ax, data=data) """ Create a 2D scatter plot """ @@ -480,79 +504,60 @@ def scatter( self, x, y, - x_label="", - y_label="", + x_axis=axis_options(label=""), + y_axis=axis_options(label=""), title="", label="", color=default_color, alpha=1, - figsize=(8, 6), show_stats=lambda x, _: f"{len(x)} entries", - lgd_ops=get_legend_options(), + lgd_ops=legend_options(), + figsize=(8, 6), ): fig = plt.figure(figsize=figsize, layout="constrained") ax = fig.add_subplot(1, 1, 1) + # Refine plot + ax.set_title(title) + ax.set_xlabel(x_axis.label) + ax.set_ylabel(y_axis.label) + ax.grid(True, alpha=0.25) + # Create empty plot with blank marker containing the extra label ax.plot([], [], " ", label=show_stats(x, y)) scatter = ax.scatter( x, y, label=label, c=color, s=0.1, alpha=alpha, rasterized=True ) - # Refine plot - ax.set_title(title) - ax.set_xlabel(x_label) - ax.set_ylabel(y_label) - ax.grid(True, alpha=0.25) - # Add legend - lgd = self.add_legend(ax, lgd_ops) + lgd = self.__add_legend(ax, lgd_ops) # Refine legend - lgd.legend_handles[0].set_visible(False) - for handle in lgd.legend_handles[1:]: - handle.set_sizes([40]) + self.__adjust_lgd_label_spacing(lgd) - # Adjust spacing in box - for vpack in lgd._legend_handle_box.get_children()[:1]: - for hpack in vpack.get_children(): - hpack.get_children()[0].set_width(0) - - return plt_data(fig, ax, lgd, scatter, None, None, None, None) + return plt_data(fig=fig, axes=ax, lgd=lgd, data=scatter) """ Add new data in a different color to a scatter plot """ def highlight_region(self, plot_data, x, y, color, label=""): if label == "": - plot_data.ax.scatter(x, y, c=color, alpha=1, s=0.1, rasterized=True) + plot_data.axes.scatter(x, y, c=color, alpha=1, s=0.1, rasterized=True) else: - plot_data.ax.scatter( + plot_data.axes.scatter( x, y, c=color, alpha=1, s=0.1, label=label, rasterized=True ) # Update legend - lgd = plot_data.lgd - handles, labels = lgd.axes.get_legend_handles_labels() - lgd._legend_box = None - lgd._init_legend_box(handles, labels) - lgd._set_loc(lgd._loc) - lgd.set_title(lgd.get_title().get_text()) + self.__update_legend(plot_data.lgd) # Refine legend - lgd.legend_handles[0].set_visible(False) - for handle in lgd.legend_handles[1:]: - handle.set_sizes([40]) - - # Adjust spacing in box - for vpack in lgd._legend_handle_box.get_children()[:1]: - for hpack in vpack.get_children(): - hpack.get_children()[0].set_width(0) + self.__adjust_lgd_label_spacing(plot_data.lgd) """ Fit a Gaussian to a 1D distribution and plot in the same figure. """ - def fit_gaussian(self, dist): + def fit_gaussian(self, dist, color="tab:orange"): # Calculate bin centers from bin edges bins = dist.bins @@ -563,7 +568,7 @@ def fit_gaussian(self, dist): bin_centers = [(b1 + b2) / 2 for b1, b2 in zip(bins, bins[1:])] # Gaussian distribution with all fit parameters - def gaussian(x, a, mean, sigma): + def __gaussian(x, a, mean, sigma): return ( a / (math.sqrt(2 * math.pi) * sigma) @@ -583,7 +588,7 @@ def gaussian(x, a, mean, sigma): a = np.max(dist.data) * (math.sqrt(2 * math.pi) * sigma) popt, _ = curve_fit( - gaussian, bin_centers, dist.data, p0=[a, mean, sigma] + __gaussian, bin_centers, dist.data, p0=[a, mean, sigma] ) except RuntimeError: # If fit failed, return empty result @@ -593,6 +598,10 @@ def gaussian(x, a, mean, sigma): mu = float(f"{popt[1]:.2e}") # < formatting the sig. digits sig = float(f"{popt[2]:.2e}") newline = "\n" + plot_label = ( + rf"gaussian fit:{newline}$\mu$ = {mu:.2e}" + + rf"{newline}$\sigma$ = {abs(sig):.2e}" + ) # Generate points for the curve min_val = min(bin_centers) @@ -600,25 +609,19 @@ def gaussian(x, a, mean, sigma): step = (max_val - min_val) / 1000 x = [v for v in np.arange(min_val, max_val + step, step)] - dist.ax.plot( + dist.axes.plot( x, - gaussian(x, *popt), - label=rf"gaussian fit:{newline}$\mu$ = {mu:.2e}" - + rf"{newline}$\sigma$ = {abs(sig):.2e}", - color="tab:orange", + __gaussian(x, *popt), + label=plot_label, + color=color, ) # Update legend - lgd = dist.lgd - handles, labels = lgd.axes.get_legend_handles_labels() - lgd._legend_box = None - lgd._init_legend_box(handles, labels) - lgd._set_loc(lgd._loc) - lgd.set_title(lgd.get_title().get_text()) + self.__update_legend(dist.lgd) # Adjust spacing in box - lgd.legend_handles[0].set_visible(False) - for vpack in lgd._legend_handle_box.get_children()[:-1]: + dist.lgd.legend_handles[0].set_visible(False) + for vpack in dist.lgd._legend_handle_box.get_children()[:-1]: for hpack in vpack.get_children(): hpack.get_children()[0].set_width(0) @@ -626,7 +629,7 @@ def gaussian(x, a, mean, sigma): return None, None - """ Safe a plot to disk """ + """ Write a plot to disk """ def write_plot( self, plot_data, name="plot", file_format="svg", out_prefix="", dpi=450 @@ -639,20 +642,17 @@ def write_plot( plot_data.fig.savefig(file_name, dpi=dpi) plt.close(plot_data.fig) - """ Safe a plot as svg """ + """ Write a plot as svg """ def write_svg(self, plot_data, name, out_prefix=""): - self.write_plot(plot_data, name, ".svg", out_prefix) - """ Safe a plot as pdf """ + """ Write a plot as pdf """ def write_pdf(self, plot_data, name, out_prefix=""): - self.write_plot(plot_data, name, ".pdf", out_prefix) - """ Safe a plot as png """ + """ Write a plot as png """ def write_png(self, plot_data, name, out_prefix=""): - self.write_plot(plot_data, name, ".png", out_prefix) diff --git a/tests/tools/python/utils/__init__.py b/tests/tools/python/utils/__init__.py new file mode 100644 index 000000000..271e19824 --- /dev/null +++ b/tests/tools/python/utils/__init__.py @@ -0,0 +1,6 @@ +from .io_utils import read_detector_name, get_p_range +from .io_utils import ( + add_propagation_args, + add_track_generator_args, + add_detector_io_args, +) diff --git a/tests/tools/python/utils/io_utils.py b/tests/tools/python/utils/io_utils.py new file mode 100644 index 000000000..8e9f24192 --- /dev/null +++ b/tests/tools/python/utils/io_utils.py @@ -0,0 +1,155 @@ +# Detray library, part of the ACTS project (R&D line) +# +# (c) 2025 CERN for the benefit of the ACTS project +# +# Mozilla Public License Version 2.0 + +import json +import os +import sys + + +""" Read the detector name from geometry json file """ + + +def read_detector_name(geometry_file_name, logging): + if not os.path.isfile(geometry_file_name): + logging.error(f"Geometry json file not found! ({geometry_file_name})") + return "unknown_detector" + + with open(geometry_file_name) as geo_file: + json_geo = json.loads(geo_file.read()) + det_name = json_geo["header"]["common"]["detector"] + + return det_name + + +""" Uniform access to the momentum range from the CLI arguments """ + + +def get_p_range(parsed_args, logging): + + if parsed_args.p_range is not None: + return parsed_args.p_range[0], parsed_args.p_range[1] + elif parsed_args.pT_range is not None: + return parsed_args.pT_range[0], parsed_args.pT_range[1] + else: + logging.error("No momentum configured") + sys.exit(1) + + +""" Add CLI arguments from the detector IO options in args """ + + +def add_detector_io_args(arg_list, parsed_args): + + # Always required + arg_list.extend( + [ + "--geometry_file", + parsed_args.geometry_file, + ] + ) + + # Optional + if parsed_args.grid_file: + arg_list.extend(["--grid_file", parsed_args.grid_file]) + + if parsed_args.material_file: + arg_list.extend(["--material_file", parsed_args.material_file]) + + +""" Add CLI arguments from the track generator options in args """ + + +def add_track_generator_args(arg_list, parsed_args): + + arg_list.extend( + [ + "--random_seed", + str(parsed_args.random_seed), + "--eta_range", + str(parsed_args.eta_range[0]), + str(parsed_args.eta_range[1]), + ] + ) + + has_p_range = parsed_args.p_range is not None + has_pT_range = parsed_args.pT_range is not None + if has_p_range and has_pT_range: + print( + "ERROR: Cannot set total momentum and transverse momentum at the same time" + ) + sys.exit(1) + elif has_p_range: + arg_list.extend( + ["--p_range", str(parsed_args.p_range[0]), str(parsed_args.p_range[1])] + ) + elif has_pT_range: + arg_list.extend( + ["--pT_range", str(parsed_args.pT_range[0]), str(parsed_args.pT_range[1])] + ) + else: + # Random track generator + arg_list.extend(["--p_range", "1", "1"]) + + if parsed_args.randomize_charge: + arg_list.extend(["--randomize_charge"]) + + if hasattr(parsed_args, "n_tracks"): + # Random track generator + arg_list.extend(["--n_tracks", str(parsed_args.n_tracks)]) + else: + # Uniform track generator + arg_list.extend( + [ + "--phi_steps", + str(parsed_args.phi_steps), + "--eta_steps", + str(parsed_args.eta_steps), + ] + ) + + +""" Add CLI arguments from the propagation options in args """ + + +def add_propagation_args(arg_list, parsed_args): + + arg_list.extend( + [ + "--min_mask_tolerance", + str(parsed_args.min_mask_tol), + "--max_mask_tolerance", + str(parsed_args.max_mask_tol), + "--mask_tolerance_scalor", + str(parsed_args.mask_tol_scalor), + "--overstep_tolerance", + str(parsed_args.overstep_tol), + "--path_tolerance", + str(parsed_args.path_tol), + "--search_window", + str(parsed_args.search_window[0]), + str(parsed_args.search_window[1]), + "--rk-tolerance", + str(parsed_args.rk_error_tol), + "--path_limit", + str(parsed_args.path_limit), + "--minimum_stepsize", + str(parsed_args.min_step_size), + "--step_contraint", + str(parsed_args.max_step_size), + ] + ) + + if parsed_args.covariance_transport: + arg_list.extend(["--covariance_transport"]) + + if parsed_args.bethe_energy_loss: + arg_list.extend(["--mean_energy_loss"]) + + if parsed_args.energy_loss_grad: + arg_list.extend(["--eloss_gradient"]) + + if parsed_args.bfield_grad: + arg_list.extend(["--bfield_gradient"]) diff --git a/tests/tools/src/cpu/detector_validation.cpp b/tests/tools/src/cpu/detector_validation.cpp index e083c9cbb..9b1405290 100644 --- a/tests/tools/src/cpu/detector_validation.cpp +++ b/tests/tools/src/cpu/detector_validation.cpp @@ -147,6 +147,7 @@ int main(int argc, char** argv) { hel_nav_cfg.name(det_name + "_helix_navigation"); // Number of tracks to check hel_nav_cfg.n_tracks(hel_scan_cfg.track_generator().n_tracks()); + hel_nav_cfg.p_range(hel_scan_cfg.track_generator().mom_range()); hel_nav_cfg.intersection_file(hel_scan_cfg.intersection_file()); hel_nav_cfg.track_param_file(hel_scan_cfg.track_param_file());