diff --git a/CMakeLists.txt b/CMakeLists.txt index e4b6149cf5d..e1488666cd2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -364,22 +364,27 @@ include(ActsExternSources) if(ACTS_SETUP_BOOST) # Enable both program_options and unit_test_framework to reduce complexity # Also Cuda tests seem to use program_options + set(_acts_boost_components) if( ACTS_BUILD_ANALYSIS_APPS OR ACTS_BUILD_UNITTESTS OR ACTS_BUILD_INTEGRATIONTESTS OR ACTS_BUILD_BENCHMARKS ) - find_package( - Boost - ${_acts_boost_version} - REQUIRED - COMPONENTS program_options unit_test_framework - ) - else() - find_package(Boost ${_acts_boost_version} REQUIRED COMPONENTS) + list(APPEND _acts_boost_components program_options unit_test_framework) endif() + # Only the Mille plugin needs process/filesystem, and neither is part of + # every Boost installation (Boost.Process is a compiled library since Boost + # 1.88). The plugin reports if the one it needs is missing. + find_package( + Boost + ${_acts_boost_version} + REQUIRED + COMPONENTS ${_acts_boost_components} + OPTIONAL_COMPONENTS process filesystem + ) + if(Boost_VERSION VERSION_EQUAL "1.85.0") set(_boost_version_severity WARNING) if(ACTS_BUILD_EXAMPLES) diff --git a/Plugins/Mille/CMakeLists.txt b/Plugins/Mille/CMakeLists.txt index e628bce57f2..181edf140d4 100644 --- a/Plugins/Mille/CMakeLists.txt +++ b/Plugins/Mille/CMakeLists.txt @@ -2,6 +2,10 @@ acts_add_library( PluginMille src/Helpers.cpp src/ActsToMille.cpp + src/MillePedeSolver.cpp + src/MillePedeError.cpp + src/detail/RunSolverProcess.cpp + src/detail/WrappedFileHandle.cpp ACTS_INCLUDE_FOLDER include/ActsPlugins ) @@ -12,6 +16,49 @@ target_include_directories( $ ) -target_link_libraries(ActsPluginMille PUBLIC ActsAlignment Mille2) +# The solver runs `pede` as a child process via Boost.Process: the compiled +# library from Boost 1.88 on, the header-only v1 API (which builds on +# Boost.Filesystem) before that. Both are probed as optional components at the +# top level, since only this plugin needs them. +if(Boost_VERSION VERSION_GREATER_EQUAL 1.88.0) + set(_mille_boost_component process) + set(_mille_boost_reason "which is a compiled library since Boost 1.88") + target_compile_definitions( + ActsPluginMille + PRIVATE ACTS_MILLE_USE_BOOST_PROCESS_V2 + ) +else() + set(_mille_boost_component filesystem) + set(_mille_boost_reason "whose pre-1.88 API is built on Boost.Filesystem") +endif() + +if(NOT TARGET Boost::${_mille_boost_component}) + foreach(_component process filesystem) + if(TARGET Boost::${_component}) + string(APPEND _mille_boost_found " ${_component}") + endif() + endforeach() + if(NOT _mille_boost_found) + set(_mille_boost_found " none of them") + endif() + + message( + FATAL_ERROR + "The Mille plugin runs `pede` as a child process via Boost.Process, " + "${_mille_boost_reason}, so it needs the Boost " + "${_mille_boost_component} library. The Boost ${Boost_VERSION} " + "installation in ${Boost_DIR} provides:${_mille_boost_found}. Please " + "enable ${_mille_boost_component} in the Boost build (with spack: " + "`boost +${_mille_boost_component}`), or set " + "ACTS_BUILD_PLUGIN_MILLE=OFF." + ) +endif() + +# Boost is only used by the solver implementation, not by any public header +target_link_libraries( + ActsPluginMille + PUBLIC ActsAlignment Mille2 + PRIVATE Boost::boost Boost::${_mille_boost_component} +) acts_compile_headers(PluginMille GLOB include/**/*.hpp) diff --git a/Plugins/Mille/include/ActsPlugins/Mille/MillePedeError.hpp b/Plugins/Mille/include/ActsPlugins/Mille/MillePedeError.hpp new file mode 100644 index 00000000000..12c1c1adbb5 --- /dev/null +++ b/Plugins/Mille/include/ActsPlugins/Mille/MillePedeError.hpp @@ -0,0 +1,38 @@ +// This file is part of the ACTS project. +// +// Copyright (C) 2016 CERN for the benefit of the ACTS project +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma once + +#include +#include + +namespace ActsPlugins { +/// Error codes for Millepede run +/// @ingroup errors +enum class MillePedeError { + InstallationNotFound = 1, // no valid install found + SteeringNotFound = 2, // steering file not found + SolverCrash = 3, // solver crashed + InvalidSolution = 4, // solver finished but the solution is invalid + SolutionNotReadable = 5, // solution file could not be read +}; + +/// @cond +/// Create error code from MillePedeError +/// @param e The error code enum value +/// @return Standard error code +std::error_code make_error_code(MillePedeError e); +/// @endcond + +} // namespace ActsPlugins + +namespace std { +// register with STL +template <> +struct is_error_code_enum : std::true_type {}; +} // namespace std diff --git a/Plugins/Mille/include/ActsPlugins/Mille/MillePedeSolver.hpp b/Plugins/Mille/include/ActsPlugins/Mille/MillePedeSolver.hpp new file mode 100644 index 00000000000..2857f9c4de3 --- /dev/null +++ b/Plugins/Mille/include/ActsPlugins/Mille/MillePedeSolver.hpp @@ -0,0 +1,113 @@ +// This file is part of the ACTS project. +// +// Copyright (C) 2016 CERN for the benefit of the ACTS project +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma once +#include "Acts/Utilities/Logger.hpp" +#include "Acts/Utilities/Result.hpp" +#include "ActsPlugins/Mille/MillePedeError.hpp" + +#include +#include + +namespace ActsPlugins { + +/// class wrapping an external call to the 'pede' solver +/// program of the Millepede-II alignment toolkit. +class MillePedeSolver { + public: + /// @brief abstract summary of the exit codes returned by pede. + enum class MpExitStatus { + NotFinishedOrCrashed, /// job is still running or crashed before exiting + NominalExit, /// nominal exit + TolerableWarnings, /// exit with tolerable warnings, considered ok + SeriousWarnings, /// exit with serious warnings, should investigate + NoSolution, /// exit without solution (usually: rank deficit) + Aborted /// Aborted due to errors + }; + + /// @brief configuration for running pede. + /// Currently, most details are delegated to the steering file syntax. + struct Config { + /// steering file with run options + std::string steeringFile = "pedeSteerMaster.txt"; + + /// directory to run in. Default: current work dir + std::optional workDir; + + /// extra CLI options + std::vector extraOpts = {}; + + /// destination for result file - default: keep original + std::optional resFileName; + /// destination for the cout/cerr printout + /// from pede - default: print to terminal + std::optional redirectStdout; + /// destination for the log file - default: keep original + std::optional logFileName; + /// destination for the histogram file - default: keep original + std::optional histoFileName; + /// destination for the eigenvector file - default: keep original + std::optional evFileName; + }; + + /// @brief package the result of the alignment fit + struct MpResult { + int exitCode = -1; /// raw pede exit code + MpExitStatus exitStatus = + MpExitStatus::NotFinishedOrCrashed; /// summary exit status + std::string exitMessage = ""; /// detailed exit message + std::filesystem::path resultsFile; /// file containing parameter results + std::filesystem::path logFile; /// log file + std::filesystem::path histoFile; /// file with validation histograms + std::filesystem::path evFile; /// file with eigenvectors + }; + + /// @brief constructor - nothing to do as minimal internal state carried + explicit MillePedeSolver(Acts::Logging::Level level = Acts::Logging::INFO) + : m_logger(Acts::getDefaultLogger("MillePedeSolver", level)) {} + + /// @brief Runs the solving. + /// Can take some time for large fits. + /// Will invoke pede, await the exit, and parse + /// the output. + /// @param cfg: The configuration to use + Acts::Result solve(const Config& cfg) const; + + private: + /// @brief translation of the detailed pede code to a summary status + /// Will translate the range of ~30 possible MP exit codes to a simplified + /// enum value that gives a high-level summary of the status. + /// @param theExitCode: The integer exit code found in the millepede.end file + static MpExitStatus interpretExit(int theExitCode); + + /// @brief Reads the `millepede.end` file and parses its content + /// @param mpend: The file to read, assumed that the user has checked for existence before + /// @return a tuple containing the original integer exit code, a simplified exit status enum, + /// and the additional status message emitted by pede. + std::tuple readDetailedExit( + const std::filesystem::path& mpend) const; + + /// @brief Utility method to check if an output exists, copy it if the user requested a relocation, + /// and returns the final resolved output location, which will be: + /// - if the output does not exist: empty path + /// - if the output does exist and was not relocated: original location + /// - if the output does exist, and was relocated: user-specified location + /// - if the output does exist, was requested for relocation but the copy + /// failed: original location + /// @param originalLoc: Original expected location of the file + /// @param userLoc: Desired final location of the file. If empty, no copy will be made. + std::filesystem::path copyIfRequested( + const std::filesystem::path& originalLoc, + const std::optional& userLoc) const; + + std::unique_ptr m_logger; /// logger + + /// Private access to the logger + const Acts::Logger& logger() const { return *m_logger; } +}; +} // namespace ActsPlugins diff --git a/Plugins/Mille/include/ActsPlugins/Mille/detail/RunSolverProcess.hpp b/Plugins/Mille/include/ActsPlugins/Mille/detail/RunSolverProcess.hpp new file mode 100644 index 00000000000..e3b98e1af1b --- /dev/null +++ b/Plugins/Mille/include/ActsPlugins/Mille/detail/RunSolverProcess.hpp @@ -0,0 +1,47 @@ +// This file is part of the ACTS project. +// +// Copyright (C) 2016 CERN for the benefit of the ACTS project +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma once + +/// Helper to define a "simple" interface for running pede +/// as a child process, without bringing in additional +/// external dependencies. + +#include "Acts/Utilities/Logger.hpp" + +#include +#include +#include +#include + +namespace ActsPlugins { + +enum class MpSolverStatus { + OK = 0, + ProgNotFound = 1, + FailedRedirectStdout = 2, + FailedWorkDir = 3, + FailedRun = 4, + UnknownError = 5 +}; + +/// @brief attempt to run a program as a child process. +/// @param program: Program name (search on PATH) +/// @param args: command line args +/// @param runDir: Directory to run in. Caller is responsible for ensuring +/// validity. +/// @param logger: A logger instance +/// @param redirectOutput: if set, redirect pede output to file +/// of given name instead of printing to the host stdout +/// @return the call outcome as a status code +MpSolverStatus runSolverProcess( + const std::string& program, const std::vector& args, + const std::filesystem::path& runDir, const Acts::Logger& logger, + const std::optional& redirectOutput = std::nullopt); + +} // namespace ActsPlugins diff --git a/Plugins/Mille/include/ActsPlugins/Mille/detail/WrappedFileHandle.hpp b/Plugins/Mille/include/ActsPlugins/Mille/detail/WrappedFileHandle.hpp new file mode 100644 index 00000000000..988ede3a920 --- /dev/null +++ b/Plugins/Mille/include/ActsPlugins/Mille/detail/WrappedFileHandle.hpp @@ -0,0 +1,35 @@ +// This file is part of the ACTS project. +// +// Copyright (C) 2016 CERN for the benefit of the ACTS project +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma once +#include +#include + +namespace ActsPlugins { + +/// @brief helper to wrap a file handle +class WrappedFileHandle { + public: + explicit WrappedFileHandle(const std::filesystem::path& outf = ""); + ~WrappedFileHandle(); + WrappedFileHandle(const WrappedFileHandle&) = delete; + WrappedFileHandle& operator=(const WrappedFileHandle&) = delete; + + WrappedFileHandle(WrappedFileHandle&& other) noexcept; + + WrappedFileHandle& operator=(WrappedFileHandle&& other) noexcept; + FILE* operator()() const; + bool isRedirected() const; + const std::filesystem::path& path() const; + + private: + FILE* m_handle = nullptr; + std::filesystem::path m_path{}; +}; + +} // namespace ActsPlugins diff --git a/Plugins/Mille/src/MillePedeError.cpp b/Plugins/Mille/src/MillePedeError.cpp new file mode 100644 index 00000000000..ebff3061675 --- /dev/null +++ b/Plugins/Mille/src/MillePedeError.cpp @@ -0,0 +1,46 @@ +// This file is part of the ACTS project. +// +// Copyright (C) 2016 CERN for the benefit of the ACTS project +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "ActsPlugins/Mille/MillePedeError.hpp" + +#include + +namespace { + +class MillePedeErrorCategory : public std::error_category { + public: + // Return a short descriptive name for the category. + const char* name() const noexcept final { return "MillePedeError"; } + + // Return what each enum means in text. + std::string message(int c) const final { + using ActsPlugins::MillePedeError; + using enum MillePedeError; + + switch (static_cast(c)) { + case InstallationNotFound: + return "Installation of the 'pede' program was not found"; + case SteeringNotFound: + return "Steering file for `pede` was not found at configured location"; + case SolverCrash: + return "The solver crashed"; + case InvalidSolution: + return "The solver encountered a serious error and found no valid " + "solution"; + default: + return "unknown"; + } + } +}; + +} // namespace + +std::error_code ActsPlugins::make_error_code(ActsPlugins::MillePedeError e) { + static MillePedeErrorCategory c; + return {static_cast(e), c}; +} diff --git a/Plugins/Mille/src/MillePedeSolver.cpp b/Plugins/Mille/src/MillePedeSolver.cpp new file mode 100644 index 00000000000..d80f1e2434b --- /dev/null +++ b/Plugins/Mille/src/MillePedeSolver.cpp @@ -0,0 +1,213 @@ +// This file is part of the ACTS project. +// +// Copyright (C) 2016 CERN for the benefit of the ACTS project +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "ActsPlugins/Mille/MillePedeSolver.hpp" + +#include "Acts/Utilities/Logger.hpp" +#include "Acts/Utilities/Result.hpp" +#include "ActsPlugins/Mille/MillePedeError.hpp" +#include "ActsPlugins/Mille/detail/RunSolverProcess.hpp" + +#include +#include +#include +#include +#include + +namespace ActsPlugins { + +Acts::Result MillePedeSolver::solve( + const Config& cfg) const { + ACTS_INFO("=== Proceeding to run Millepede-II alignment fit ==="); + + // determine where the user wishes to run + std::filesystem::path workDir = + cfg.workDir.value_or(std::filesystem::current_path()); + + // create the work dir if it does not exist already + if (!std::filesystem::exists(workDir)) { + std::filesystem::create_directories(workDir); + } + + ACTS_DEBUG("Will run the alignment fit in folder '" + << std::filesystem::absolute(workDir) << "'"); + + // make sure the steering file exists (or is empty) + std::vector mpArgs = cfg.extraOpts; + + if (std::filesystem::path steerPath = cfg.steeringFile; !steerPath.empty()) { + if (steerPath.is_relative()) { + steerPath = std::filesystem::current_path() / steerPath; + } + if (!std::filesystem::exists(steerPath)) { + ACTS_ERROR("Steering file " << steerPath + << " does not seem to exist - aborting!"); + return Acts::Result::failure(MillePedeError::SteeringNotFound); + } + mpArgs.push_back(steerPath); + } + + ACTS_INFO(" Calling pede, this may take a while depending on problem size"); + // now run the fit + MpSolverStatus pedeProcessStatus = + runSolverProcess("pede", mpArgs, workDir, logger(), cfg.redirectStdout); + + if (pedeProcessStatus == MpSolverStatus::ProgNotFound) { + ACTS_ERROR("Pede executable could not be found, aborting!"); + return Acts::Result::failure( + MillePedeError::InstallationNotFound); + } + if (pedeProcessStatus == MpSolverStatus::FailedWorkDir) { + ACTS_ERROR("Could not navigate to the run directory `" << workDir + << "`, aborting!"); + return Acts::Result::failure(MillePedeError::SolverCrash); + } else if (pedeProcessStatus != MpSolverStatus::OK) { + ACTS_ERROR("Pede invocation failed. Did not run alignment."); + return Acts::Result::failure(MillePedeError::SolverCrash); + } + + /// now check the detailed exit code - pede speaks fortranese, so we need to + /// read this from a file + + // step 1: Ensure the file is actually there + std::filesystem::path mpExit = + workDir / std::filesystem::path("millepede.end"); + if (!std::filesystem::exists(mpExit)) { + ACTS_ERROR( + "Failed to find the Pede exit code file. The alignment has likely " + "failed."); + return Acts::Result::failure(MillePedeError::SolverCrash); + } + + // step 2: Read and interpret the exit info file + auto [exitCode, exitStatus, exitMessage] = readDetailedExit(mpExit); + + // step 3: Tell the user what happened + + if (exitStatus == MpExitStatus::Aborted) { + ACTS_ERROR("Pede Aborted due to errors:\n " + << exitMessage << "\n" + << "Please check the millepede log files in " << workDir + << " for more information."); + return Acts::Result::failure(MillePedeError::InvalidSolution); + } + + else if (exitStatus == MpExitStatus::NoSolution) { + ACTS_WARNING("Pede did not find a solution:\n " + << exitMessage << "\n" + << "Please check the millepede log files in " << workDir + << " for more information."); + } + + else if (exitStatus == MpExitStatus::SeriousWarnings) { + ACTS_WARNING("Pede exited with severe warnings:\n " + << exitMessage << "\n" + << "You should check the millepede log files in " << workDir + << " for more information."); + } + + else if (exitStatus == MpExitStatus::TolerableWarnings) { + ACTS_INFO("Pede exited with tolerable warnings:\n " + << exitMessage << "\n" + << "You can check the millepede log files in " << workDir + << " for more information."); + } else { + ACTS_INFO("Pede exited nominally:\n " + << exitMessage << "\n" + << "You can check the millepede log files in " << workDir + << " for more information."); + } + + // step 4: Relocate output if configured by the user. + + auto outputResultFile = + copyIfRequested(workDir / "millepede.res", cfg.resFileName); + auto outputLogFile = + copyIfRequested(workDir / "millepede.log", cfg.logFileName); + auto outputHisFile = + copyIfRequested(workDir / "millepede.his", cfg.histoFileName); + auto outputEvFile = + copyIfRequested(workDir / "millepede.eve", cfg.evFileName); + + /// the one file we **always** expect to exist is the result file. + /// The others may not exist depending on the user steering options. + if (outputResultFile.empty()) { + ACTS_ERROR("Failed to find Millepede result file expected at " + << workDir / "millepede.res"); + return Acts::Result::failure(MillePedeError::SolutionNotReadable); + } + + ACTS_INFO("=== Congratulations, the alignment finished! ==="); + + return MpResult{ + exitCode, exitStatus, exitMessage, + outputResultFile, /// file containing parameter results + outputLogFile, /// log file + outputHisFile, /// file with validation histograms + outputEvFile, /// file with eigenvectors + }; +} + +std::tuple +MillePedeSolver::readDetailedExit(const std::filesystem::path& mpend) const { + std::ifstream exitCodeFile(mpend); // open the Pede exit code file + std::string statusMessage = ""; + int retCode = 0; + // read the pede exit code from the file. + exitCodeFile >> retCode; + // some acrobatics to get a nice formatting of the remaining text + std::stringstream remainingCont; + remainingCont << exitCodeFile.rdbuf(); + statusMessage = remainingCont.str(); + statusMessage = statusMessage.substr(statusMessage.find_first_not_of(' ')); + statusMessage = + statusMessage.substr(0, statusMessage.find_last_not_of(" \n") + 1); + exitCodeFile.close(); + + MpExitStatus status = interpretExit(retCode); + return std::make_tuple(retCode, status, statusMessage); +} + +MillePedeSolver::MpExitStatus MillePedeSolver::interpretExit(int theExitCode) { + // implementation follows + // https://millepede.pages.desy.de/millepede-ii/exit_code_page.html + using enum MillePedeSolver::MpExitStatus; + if (theExitCode < 0) { + return NotFinishedOrCrashed; + } else if (theExitCode == 0) { + return NominalExit; + } else if (theExitCode == 1) { + return TolerableWarnings; + } else if (theExitCode <= 4) { + return SeriousWarnings; + } else if (theExitCode == 5) { + return NoSolution; + } else { + return Aborted; + } +} + +std::filesystem::path MillePedeSolver::copyIfRequested( + const std::filesystem::path& originalLoc, + const std::optional& userLoc) const { + if (!std::filesystem::exists(originalLoc)) { + return ""; + } else if (!userLoc.has_value()) { + return originalLoc; + } else { + if (!std::filesystem::copy_file( + originalLoc, *userLoc, + std::filesystem::copy_options::overwrite_existing)) { + ACTS_WARNING(" Failed to copy output file " << originalLoc << " to " + << *userLoc); + return originalLoc; + } + return *userLoc; + } +} +} // namespace ActsPlugins diff --git a/Plugins/Mille/src/detail/RunSolverProcess.cpp b/Plugins/Mille/src/detail/RunSolverProcess.cpp new file mode 100644 index 00000000000..d5dd4472869 --- /dev/null +++ b/Plugins/Mille/src/detail/RunSolverProcess.cpp @@ -0,0 +1,46 @@ +// This file is part of the ACTS project. +// +// Copyright (C) 2016 CERN for the benefit of the ACTS project +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "ActsPlugins/Mille/detail/RunSolverProcess.hpp" + +#include "Acts/Utilities/Logger.hpp" +#include "ActsPlugins/Mille/detail/WrappedFileHandle.hpp" + +#include + +// The mechanics of launching a child in Boost::Process vary +// between versions before and after 1.88. Make sure to +// pick up the one supported by the user's boost install. +#ifdef ACTS_MILLE_USE_BOOST_PROCESS_V2 +#include "SolverProcessCall_BoostV2.hpp" +#else +#include "SolverProcessCall_BoostV1.hpp" +#endif + +ActsPlugins::MpSolverStatus ActsPlugins::runSolverProcess( + const std::string& program, const std::vector& args, + const std::filesystem::path& runDir, const Acts::Logger& logger, + const std::optional& redirectOutput) { + // work dir has to exist - caller's responsibility + if (!std::filesystem::exists(runDir)) { + ACTS_ERROR("Run directory '" << runDir + << "' does not exist, will not run solver"); + return ActsPlugins::MpSolverStatus::FailedWorkDir; + } + + WrappedFileHandle outputHandle; // defaults to "do not redirect" + + if (redirectOutput.has_value()) { + outputHandle = WrappedFileHandle(*redirectOutput); + if (!outputHandle.isRedirected()) { + ACTS_ERROR("Failed to redirect output to '" << *redirectOutput << "'"); + return ActsPlugins::MpSolverStatus::FailedRedirectStdout; + } + } + return runChildProcessBoost(program, args, runDir, outputHandle, logger); +} diff --git a/Plugins/Mille/src/detail/SolverProcessCall_BoostV1.hpp b/Plugins/Mille/src/detail/SolverProcessCall_BoostV1.hpp new file mode 100644 index 00000000000..285bedee53c --- /dev/null +++ b/Plugins/Mille/src/detail/SolverProcessCall_BoostV1.hpp @@ -0,0 +1,43 @@ +// This file is part of the ACTS project. +// +// Copyright (C) 2016 CERN for the benefit of the ACTS project +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma once + +#include + +namespace ActsPlugins { +ActsPlugins::MpSolverStatus runChildProcessBoost( + const std::string& program, const std::vector& args, + const std::filesystem::path& runDir, const WrappedFileHandle& outputHandle, + const Acts::Logger& logger) { + namespace bp = boost::process; + + // find the pede installation + auto thePede = bp::search_path(program); + if (thePede.empty()) { + ACTS_ERROR("Failed to find the solver program '" << program << "'"); + return ActsPlugins::MpSolverStatus::ProgNotFound; + } + + bp::child theProcess; + if (outputHandle.isRedirected()) { + theProcess = bp::child(thePede, args, bp::start_dir = runDir.string(), + (bp::std_out & bp::std_err) > outputHandle()); + } else { + theProcess = bp::child(thePede, args, bp::start_dir = runDir.string()); + } + + theProcess.wait(); + if (theProcess.exit_code() != 0) { + return MpSolverStatus::FailedRun; + ACTS_ERROR("Failed to run the solver process!"); + } + return MpSolverStatus::OK; +} + +} // namespace ActsPlugins diff --git a/Plugins/Mille/src/detail/SolverProcessCall_BoostV2.hpp b/Plugins/Mille/src/detail/SolverProcessCall_BoostV2.hpp new file mode 100644 index 00000000000..50c828f5405 --- /dev/null +++ b/Plugins/Mille/src/detail/SolverProcessCall_BoostV2.hpp @@ -0,0 +1,48 @@ +// This file is part of the ACTS project. +// +// Copyright (C) 2016 CERN for the benefit of the ACTS project +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma once + +#include +#include +#include +#include +#include + +namespace ActsPlugins { +// With boost v1.88+, the v2 process API is default +ActsPlugins::MpSolverStatus runChildProcessBoost( + const std::string& program, const std::vector& args, + const std::filesystem::path& runDir, const WrappedFileHandle& outputHandle, + const Acts::Logger& logger) { + using namespace boost::process; + + // find the pede installation + auto thePede = environment::find_executable(program); + if (thePede.empty()) { + ACTS_ERROR("Failed to find the solver program '" << program << "'"); + return ActsPlugins::MpSolverStatus::ProgNotFound; + } + + boost::asio::io_context io; + + process_stdio stdio{}; + if (outputHandle.isRedirected()) { + stdio = process_stdio{{}, outputHandle(), outputHandle()}; + } + // now run the fit + process theProcess(io, thePede, args, process_start_dir(runDir.string()), + stdio); + theProcess.wait(); + if (theProcess.exit_code() != 0) { + ACTS_ERROR("Failed to run the solver process!"); + return MpSolverStatus::FailedRun; + } + return MpSolverStatus::OK; +} +} // namespace ActsPlugins diff --git a/Plugins/Mille/src/detail/WrappedFileHandle.cpp b/Plugins/Mille/src/detail/WrappedFileHandle.cpp new file mode 100644 index 00000000000..6ebbd1c1510 --- /dev/null +++ b/Plugins/Mille/src/detail/WrappedFileHandle.cpp @@ -0,0 +1,57 @@ +// This file is part of the ACTS project. +// +// Copyright (C) 2016 CERN for the benefit of the ACTS project +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "ActsPlugins/Mille/detail/WrappedFileHandle.hpp" + +#include +#include +#include + +namespace ActsPlugins { + +/// @brief helper to wrap a file handle +WrappedFileHandle::WrappedFileHandle(const std::filesystem::path& outf) + : m_path(outf) { + if (!outf.empty()) { + m_handle = std::fopen(outf.c_str(), "w"); + } +} + +WrappedFileHandle::~WrappedFileHandle() { + if (m_handle != nullptr) { + std::fclose(m_handle); + } +} + +WrappedFileHandle::WrappedFileHandle(WrappedFileHandle&& other) noexcept + : m_handle(std::exchange(other.m_handle, nullptr)), + m_path(std::move(other.m_path)) {} + +WrappedFileHandle& WrappedFileHandle::operator=( + WrappedFileHandle&& other) noexcept { + if (this != &other) { + if (m_handle != nullptr) { + std::fclose(m_handle); + } + m_handle = std::exchange(other.m_handle, nullptr); + m_path = std::move(other.m_path); + } + + return *this; +} +FILE* WrappedFileHandle::operator()() const { + return m_handle; +} +bool WrappedFileHandle::isRedirected() const { + return m_handle != nullptr; +} +const std::filesystem::path& WrappedFileHandle::path() const { + return m_path; +} + +} // namespace ActsPlugins diff --git a/Python/Plugins/CMakeLists.txt b/Python/Plugins/CMakeLists.txt index 2729aa7a36e..6981cd455ac 100644 --- a/Python/Plugins/CMakeLists.txt +++ b/Python/Plugins/CMakeLists.txt @@ -60,6 +60,7 @@ add_plugin_binding_if(Json Acts::PluginJson ACTS_BUILD_PLUGIN_JSON) add_plugin_binding_if(Svg Acts::PluginActSVG ACTS_BUILD_PLUGIN_ACTSVG) add_plugin_binding_if(TGeo Acts::PluginRoot ACTS_BUILD_PLUGIN_ROOT) add_plugin_binding_if(Root Acts::PluginRoot ACTS_BUILD_PLUGIN_ROOT) +add_plugin_binding_if(Mille Acts::PluginMille ACTS_BUILD_PLUGIN_MILLE) # Propagate the list of built plugins to the parent scope set(_plugins_built "${_plugins_built}" PARENT_SCOPE) diff --git a/Python/Plugins/src/Mille.cpp b/Python/Plugins/src/Mille.cpp new file mode 100644 index 00000000000..e13e5c95d95 --- /dev/null +++ b/Python/Plugins/src/Mille.cpp @@ -0,0 +1,37 @@ +// This file is part of the ACTS project. +// +// Copyright (C) 2016 CERN for the benefit of the ACTS project +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "ActsPlugins/Mille/MillePedeSolver.hpp" +#include "ActsPython/Utilities/Helpers.hpp" +#include "ActsPython/Utilities/Macros.hpp" + +#include + +namespace py = pybind11; +using namespace pybind11::literals; + +using ActsPlugins::MillePedeSolver; + +PYBIND11_MODULE(ActsPluginsPythonBindingsMille, mille) { + { + auto ps = py::class_>( + mille, "MillePedeSolver") + .def(py::init()) + .def("solve", &MillePedeSolver::solve); + + auto mr = + py::class_(ps, "MpResult").def(py::init<>()); + ACTS_PYTHON_STRUCT(mr, exitCode, exitStatus, exitMessage, resultsFile, + logFile, histoFile, evFile); + + auto sc = + py::class_(ps, "Config").def(py::init<>()); + ACTS_PYTHON_STRUCT(sc, steeringFile, workDir, extraOpts, resFileName, + logFileName, histoFileName, evFileName); + } +} diff --git a/Tests/UnitTests/Plugins/Mille/CMakeLists.txt b/Tests/UnitTests/Plugins/Mille/CMakeLists.txt index eaebe537293..caebdad4874 100644 --- a/Tests/UnitTests/Plugins/Mille/CMakeLists.txt +++ b/Tests/UnitTests/Plugins/Mille/CMakeLists.txt @@ -1,4 +1,13 @@ set(unittest_extra_libraries ActsPluginMille) +# Need the `pede` program for the solver test. +# This is not found when running the "ACTS-internal" MP2 build +# as part of the ACTS CI. Hence this test is only run +# when we have a complete Millepede-II installation in place. +if(ACTS_USE_SYSTEM_MP2) + add_unittest(MillePedeSolverBuiltIn MillePedeSolverBuiltIn.cpp) +endif() + add_unittest(ActsKalmanToMilleConsistency ActsKalmanToMilleConsistency.cpp) add_unittest(ActsMilleBasicCalls ActsMilleBasicCalls.cpp) +add_unittest(SolverProcessTests SolverProcessTests.cpp) diff --git a/Tests/UnitTests/Plugins/Mille/MillePedeSolverBuiltIn.cpp b/Tests/UnitTests/Plugins/Mille/MillePedeSolverBuiltIn.cpp new file mode 100644 index 00000000000..5bc5888cf60 --- /dev/null +++ b/Tests/UnitTests/Plugins/Mille/MillePedeSolverBuiltIn.cpp @@ -0,0 +1,41 @@ +// This file is part of the ACTS project. +// +// Copyright (C) 2016 CERN for the benefit of the ACTS project +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#include +#include + +#include "ActsPlugins/Mille/MillePedeError.hpp" +#include "ActsPlugins/Mille/MillePedeSolver.hpp" + +using namespace ActsPlugins; + +BOOST_AUTO_TEST_SUITE(MillePedeSolverBuiltin) + +/// run the built-in silicon alignment test +BOOST_AUTO_TEST_CASE(BuiltInSiTracker) { + MillePedeSolver::Config testSolverCfg; + // this tells the solver we do not use a steering file + testSolverCfg.steeringFile = ""; + testSolverCfg.workDir = "mpTest"; + testSolverCfg.redirectStdout = "mpStdout.txt"; + // this triggers the built-in test case + testSolverCfg.extraOpts = {" -t=BRLF"}; + + MillePedeSolver solver; + auto res = solver.solve(testSolverCfg); + // this one should be ok! + BOOST_CHECK(res.ok()); + // the internal exit code should be zero + BOOST_CHECK_EQUAL(res->exitCode, 0); + // and the zero exit code should resolve to a "nominal exit" + BOOST_CHECK_EQUAL( + static_cast(res->exitStatus), + static_cast(MillePedeSolver::MpExitStatus::NominalExit)); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/Tests/UnitTests/Plugins/Mille/SolverProcessTests.cpp b/Tests/UnitTests/Plugins/Mille/SolverProcessTests.cpp new file mode 100644 index 00000000000..cdd49be4373 --- /dev/null +++ b/Tests/UnitTests/Plugins/Mille/SolverProcessTests.cpp @@ -0,0 +1,76 @@ +// This file is part of the ACTS project. +// +// Copyright (C) 2016 CERN for the benefit of the ACTS project +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#include +#include + +#include "Acts/Utilities/Logger.hpp" +#include "ActsPlugins/Mille/detail/RunSolverProcess.hpp" + +#include +#include + +using namespace ActsPlugins; + +BOOST_AUTO_TEST_SUITE(ChildProcessTests) +auto logger = Acts::getDefaultLogger("MillePedeSolver", Acts::Logging::INFO); +/// catch a missing steering file +BOOST_AUTO_TEST_CASE(MissingProg) { +// temporarily disable log failure threshold +// as we intentionally test an error condition +#ifdef ACTS_ENABLE_LOG_FAILURE_THRESHOLD + auto level = Acts::Logging::getFailureThreshold(); + Acts::Logging::setFailureThreshold(Acts::Logging::MAX); +#endif + MpSolverStatus status = + runSolverProcess("tHiSsur3lyD0esn.otExist.exe", {}, + std::filesystem::current_path(), *logger); + BOOST_CHECK(status == MpSolverStatus::ProgNotFound); +#ifdef ACTS_ENABLE_LOG_FAILURE_THRESHOLD + Acts::Logging::setFailureThreshold(level); +#endif +} + +/// catch an invalid work dir +BOOST_AUTO_TEST_CASE(WrongWD) { +// temporarily disable log failure threshold +// as we intentionally test an error condition +#ifdef ACTS_ENABLE_LOG_FAILURE_THRESHOLD + auto level = Acts::Logging::getFailureThreshold(); + Acts::Logging::setFailureThreshold(Acts::Logging::MAX); +#endif + MpSolverStatus status = runSolverProcess( + "echo", {"Hello World"}, "o/hNo/Invalid/WorkDirectory", *logger); + BOOST_CHECK(status == MpSolverStatus::FailedWorkDir); +#ifdef ACTS_ENABLE_LOG_FAILURE_THRESHOLD + Acts::Logging::setFailureThreshold(level); +#endif +} + +/// successful call +BOOST_AUTO_TEST_CASE(GoodCall) { + MpSolverStatus status = runSolverProcess( + "echo", {"Hello World"}, std::filesystem::current_path(), *logger); + BOOST_CHECK(status == MpSolverStatus::OK); +} + +/// redirect stdout +BOOST_AUTO_TEST_CASE(Redirect) { + const std::string testMessage = "Hello ACTS!"; + MpSolverStatus status = + runSolverProcess("echo", {testMessage}, std::filesystem::current_path(), + *logger, "teststdout.txt"); + BOOST_CHECK(status == MpSolverStatus::OK); + std::ifstream in("teststdout.txt"); + BOOST_CHECK(in.is_open()); + std::string read = ""; + BOOST_CHECK(std::getline(in, read).good()); + BOOST_CHECK(read == testMessage); +} + +BOOST_AUTO_TEST_SUITE_END()