diff --git a/CMakeLists.txt b/CMakeLists.txt index a564afe..1ebe4ae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,11 +9,14 @@ set_property(GLOBAL PROPERTY WARTHOG_warthog-core ON) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED TRUE) +option(WARTHOG_POSTHOC "Compile trace support generation for posthoc" OFF) option(WARTHOG_INT128 "Enable support for __int128 on gcc and clang" OFF) option(WARTHOG_BMI "Enable support cpu BMI for WARTHOG_INTRIN_HAS(BMI)" OFF) option(WARTHOG_BMI2 "Enable support cpu BMI2 for WARTHOG_INTRIN_HAS(BMI2), use for Zen 3+" OFF) option(WARTHOG_INTRIN_ALL "Enable march=native and support x86 intrinsics if able (based on system), supersedes all manual instruction sets" OFF) +find_package(Threads REQUIRED) + include(cmake/warthog.cmake) add_library(warthog_compile INTERFACE) @@ -30,7 +33,7 @@ target_include_directories(warthog_core PUBLIC $ $ ) -target_link_libraries(warthog_core PUBLIC warthog::compile) +target_link_libraries(warthog_core PUBLIC warthog::compile Threads::Threads) include(cmake/headers.cmake) add_executable(warthog_app) diff --git a/CONTRIB.md b/CONTRIB.md new file mode 100644 index 0000000..a708e1f --- /dev/null +++ b/CONTRIB.md @@ -0,0 +1,18 @@ +Contributors to the Warthog Pathfinding Library +=============================================== + +Maintainers +----------- + +Daniel Harabor @dharabor +Ryan Hechenberger @heavenfall + +Contributors +------------ + +Arthur Maheo +Shizhe Zhao +Saman Ahmadi +Mark Carlson +Thomas Nobes +Massimo Bono diff --git a/apps/cfg.cpp b/apps/cfg.cpp index 12ff66b..a9d9f52 100644 --- a/apps/cfg.cpp +++ b/apps/cfg.cpp @@ -39,7 +39,7 @@ warthog::util::cfg::parse_args( std::string warthog::util::cfg::get_param_value(std::string param_name) { - std::string ret(""); + std::string ret; std::map>::iterator it = params_.find(param_name); if(it != params_.end()) diff --git a/apps/cfg.h b/apps/cfg.h index 3f804d9..8a1e8c0 100644 --- a/apps/cfg.h +++ b/apps/cfg.h @@ -1,7 +1,7 @@ #ifndef WARTHOG_APP_CFG_H #define WARTHOG_APP_CFG_H -#include "getopt.h" +#include #include #include diff --git a/apps/warthog.cpp b/apps/warthog.cpp index a5d4ef1..c8a420a 100644 --- a/apps/warthog.cpp +++ b/apps/warthog.cpp @@ -20,6 +20,9 @@ #include #include #include +#ifdef WARTHOG_POSTHOC +#include +#endif #include "cfg.h" #include @@ -30,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -44,6 +48,16 @@ int checkopt = 0; int verbose = 0; // display program help on startup int print_help = 0; +// run only this query, or -1 for all +int filter_id = -1; +#ifdef WARTHOG_POSTHOC +// write trace to file, empty string to disable +std::string trace_file; +using listener_grid = ::warthog::io::grid_trace; +using listener_type = std::tuple; +#else +using listener_type = std::tuple<>; +#endif void help(std::ostream& out) @@ -66,6 +80,11 @@ help(std::ostream& out) "values in the scen file)\n" << "\t--verbose (optional; prints debugging info when compiled " "with debug symbols)\n" + << "\t--filter [id] (optional; run only query [id])\n" +#ifdef WARTHOG_POSTHOC + << "\t--trace [.trace.yaml file] (optional; write posthoc trace for " + "first query to [file])\n" +#endif << "Invoking the program this way solves all instances in [scen " "file] with algorithm [alg]\n" << "Currently recognised values for [alg]:\n" @@ -104,6 +123,12 @@ check_optimality( return true; } +#ifdef WARTHOG_POSTHOC +#define WARTHOG_POSTHOC_DO(f) f +#else +#define WARTHOG_POSTHOC_DO(f) +#endif + template int run_experiments( @@ -111,14 +136,36 @@ run_experiments( warthog::util::scenario_manager& scenmgr, bool verbose, bool checkopt, std::ostream& out) { + WARTHOG_GINFO_FMT("start search with algorithm {}", alg_name); warthog::search::search_parameters par; warthog::search::solution sol; auto* expander = algo.get_expander(); if(expander == nullptr) return 1; + out << "id\talg\texpanded\tgenerated\treopen\tsurplus\theapops" << "\tnanos\tplen\tpcost\tscost\tmap\n"; - for(unsigned int i = 0; i < scenmgr.num_experiments(); i++) + for(uint32_t i = filter_id >= 0 ? static_cast(filter_id) : 0, + ie = filter_id >= 0 + ? i + 1 + : static_cast(scenmgr.num_experiments()); + i < ie; i++) { +#ifdef WARTHOG_POSTHOC + std::optional + trace_stream; // open and pass to trace if used + if constexpr(std::same_as< + listener_type, + std::remove_cvref_t>) + { + if(i == filter_id && !trace_file.empty()) + { + listener_grid& l + = std::get(algo.get_listeners()); + trace_stream.emplace(trace_file); + l.open(*trace_stream); + } + } +#endif warthog::util::experiment* exp = scenmgr.get_experiment(i); warthog::pack_id startid @@ -130,21 +177,40 @@ run_experiments( algo.get_path(&pi, &par, &sol); +#ifdef WARTHOG_POSTHOC + if constexpr(std::same_as< + listener_type, + std::remove_cvref_t>) + { + if(trace_stream.has_value()) + { + // close + std::get(algo.get_listeners()).close(); + } + } +#endif + out << i << "\t" << alg_name << "\t" << sol.met_.nodes_expanded_ << "\t" << sol.met_.nodes_generated_ << "\t" << sol.met_.nodes_reopen_ << "\t" << sol.met_.nodes_surplus_ << "\t" << sol.met_.heap_ops_ << "\t" << sol.met_.time_elapsed_nano_.count() << "\t" - << (sol.path_.size() - 1) << "\t" << sol.sum_of_edge_costs_ << "\t" - << exp->distance() << "\t" << scenmgr.last_file_loaded() - << std::endl; + << (!sol.path_.empty() ? sol.path_.size() - 1 : 0) << "\t" + << sol.sum_of_edge_costs_ << "\t" << exp->distance() << "\t" + << scenmgr.last_file_loaded() << std::endl; if(checkopt) { - if(!check_optimality(sol, exp)) return 4; + if(!check_optimality(sol, exp)) + { + WARTHOG_GCRIT("search error: failed suboptimal 4"); + return 4; + } } } + WARTHOG_GINFO_FMT( + "search complete; total memory: {}", algo.mem() + scenmgr.mem()); return 0; } @@ -158,17 +224,12 @@ run_astar( warthog::heuristic::octile_heuristic heuristic(map.width(), map.height()); warthog::util::pqueue_min open; - warthog::search::unidirectional_search astar(&heuristic, &expander, &open); + warthog::search::unidirectional_search astar( + &heuristic, &expander, &open, listener_type(WARTHOG_POSTHOC_DO(&map))); int ret = run_experiments( astar, alg_name, scenmgr, verbose, checkopt, std::cout); - if(ret != 0) - { - std::cerr << "run_experiments error code " << ret << std::endl; - return ret; - } - std::cerr << "done. total memory: " << astar.mem() + scenmgr.mem() << "\n"; - return 0; + return ret; } int @@ -182,17 +243,12 @@ run_astar4c( map.width(), map.height()); warthog::util::pqueue_min open; - warthog::search::unidirectional_search astar(&heuristic, &expander, &open); + warthog::search::unidirectional_search astar( + &heuristic, &expander, &open, listener_type(WARTHOG_POSTHOC_DO(&map))); int ret = run_experiments( astar, alg_name, scenmgr, verbose, checkopt, std::cout); - if(ret != 0) - { - std::cerr << "run_experiments error code " << ret << std::endl; - return ret; - } - std::cerr << "done. total memory: " << astar.mem() + scenmgr.mem() << "\n"; - return 0; + return ret; } int @@ -205,17 +261,12 @@ run_dijkstra( warthog::heuristic::zero_heuristic heuristic; warthog::util::pqueue_min open; - warthog::search::unidirectional_search astar(&heuristic, &expander, &open); + warthog::search::unidirectional_search astar( + &heuristic, &expander, &open, listener_type(WARTHOG_POSTHOC_DO(&map))); int ret = run_experiments( astar, alg_name, scenmgr, verbose, checkopt, std::cout); - if(ret != 0) - { - std::cerr << "run_experiments error code " << ret << std::endl; - return ret; - } - std::cerr << "done. total memory: " << astar.mem() + scenmgr.mem() << "\n"; - return 0; + return ret; } int @@ -242,13 +293,7 @@ run_wgm_astar( int ret = run_experiments( astar, alg_name, scenmgr, verbose, checkopt, std::cout); - if(ret != 0) - { - std::cerr << "run_experiments error code " << ret << std::endl; - return ret; - } - std::cerr << "done. total memory: " << astar.mem() + scenmgr.mem() << "\n"; - return 0; + return ret; } } // namespace @@ -258,13 +303,17 @@ main(int argc, char** argv) { // parse arguments warthog::util::param valid_args[] - = {{"alg", required_argument, 0, 1}, + = {{"alg", required_argument, 0, 0}, {"scen", required_argument, 0, 0}, - {"map", required_argument, 0, 1}, + {"map", required_argument, 0, 0}, // {"gen", required_argument, 0, 3}, {"help", no_argument, &print_help, 1}, {"checkopt", no_argument, &checkopt, 1}, {"verbose", no_argument, &verbose, 1}, + {"filter", required_argument, &filter_id, 1}, +#ifdef WARTHOG_POSTHOC + {"trace", required_argument, 0, 0}, +#endif {"costs", required_argument, 0, 1}, {0, 0, 0, 0}}; @@ -283,6 +332,14 @@ main(int argc, char** argv) std::string mapfile = cfg.get_param_value("map"); std::string costfile = cfg.get_param_value("costs"); + if(filter_id == 1) + { + filter_id = std::stoi(cfg.get_param_value("filter")); + } +#ifdef WARTHOG_POSTHOC + trace_file = cfg.get_param_value("trace"); +#endif + // if(gen != "") // { // warthog::util::scenario_manager sm; diff --git a/cmake/config.h.in b/cmake/config.h.in index b2c1f33..85e3736 100644 --- a/cmake/config.h.in +++ b/cmake/config.h.in @@ -5,6 +5,7 @@ #define WARTHOG_VERSION_MINOR @CMAKE_PROJECT_VERSION_MINOR@ #define WARTHOG_VERSION_REVISON @CMAKE_PROJECT_VERSION_PATCH@ +#cmakedefine WARTHOG_POSTHOC #cmakedefine WARTHOG_INT128 #cmakedefine WARTHOG_INTRIN_ALL #cmakedefine WARTHOG_BMI diff --git a/cmake/headers.cmake b/cmake/headers.cmake index b7d60c5..46e1ba6 100644 --- a/cmake/headers.cmake +++ b/cmake/headers.cmake @@ -27,6 +27,7 @@ include/warthog/heuristic/octile_heuristic.h include/warthog/heuristic/zero_heuristic.h include/warthog/io/grid.h +include/warthog/io/log.h include/warthog/memory/arraylist.h include/warthog/memory/bittable.h @@ -56,7 +57,6 @@ include/warthog/util/file_utils.h include/warthog/util/gm_parser.h include/warthog/util/helpers.h include/warthog/util/intrin.h -include/warthog/util/log.h include/warthog/util/macros.h include/warthog/util/pqueue.h include/warthog/util/scenario_manager.h diff --git a/include/warthog/io/grid_trace.h b/include/warthog/io/grid_trace.h new file mode 100644 index 0000000..6a76ff0 --- /dev/null +++ b/include/warthog/io/grid_trace.h @@ -0,0 +1,63 @@ +#ifndef WARTHOG_IO_GRID_TRACE_H +#define WARTHOG_IO_GRID_TRACE_H + +// io/grid_trace.h +// +// Basic posthoc_trace for use with gridmap. +// +// @author: Ryan Hechenberger +// @created: 2025-08-07 +// + +#include "posthoc_trace.h" +#include +#include +#include +#include + +namespace warthog::io +{ + +/// @brief class that produces a posthoc trace for the gridmap domain, grid +/// must be set. +class grid_trace : public posthoc_trace +{ +public: + using node = search::search_node; + + grid_trace() = default; + grid_trace(domain::gridmap* grid) : grid_(grid) { } + + void + set_grid(domain::gridmap* grid) noexcept + { + grid_ = grid; + } + + void + print_posthoc_header() override; + + void + begin_search(int id, const search::search_problem_instance& pi); + + void + expand_node(const node& current) const; + + void + relax_node(const node& current) const; + + void + generate_node( + const node* parent, const node& child, cost_t edge_cost, + uint32_t edge_id) const; + + void + close_node(const node& current) const; + +protected: + domain::gridmap* grid_; +}; + +} // namespace warthog::io + +#endif // WARTHOG_IO_GRID_TRACE_H diff --git a/include/warthog/io/log.h b/include/warthog/io/log.h new file mode 100644 index 0000000..8c61726 --- /dev/null +++ b/include/warthog/io/log.h @@ -0,0 +1,471 @@ +#ifndef WARTHOG_IO_LOG_H +#define WARTHOG_IO_LOG_H + +// io/log.h +// +// Logging utility framework, where a user can provide at a high-level +// data structure with function pointers to log messages as a single string +// with a specific log level. +// This will call a user-defined function (if able) that will output this +// message as the user desires. +// Default functions (output to stderr or file) are defined here. +// +// The log_sink is a non-owning copyable struct that points to the data and +// logging function calls. All logging is performed through log_sink. All +// classes here are thread safe, follow comments for outlying cases. +// +// Special classes inherit log_sink to provide default functionality. +// log_sink_std should be used to write to std::cout and std::cerr. +// log_sink_stream pipe to a stream or open a file stream. +// +// The logs are made to be logged to a certain log_level. +// The WARTHOG_TRACE and others in this header provide interfaces to log to a +// special logger<> class. This logger<> class knows at compile time the +// minimum level to log, and with the use of a macro will be compiled out if +// that level was not set at compile time when used with the logging macros. +// +// The global logger can be acquired with the glog() (logger<>) or glogs() +// (log_sink), and set with set_glog(log_sink). The log level of the global +// logger is set though the WARTHOG_LOG definition, with a default of 1(debug) +// for debug builds, and 2(information) for release (NDEBUG defined). Macros +// like WARTHOG_GTRACE will automatically write to the global logger. +// +// The _IF will only log if a runtime if true. +// The _FMT uses the std::format from C++20 to format the output messages. +// The logging utilities uses dynamic memory std::strings to produce the final +// log message strings, thus is better to be disabled for release builds +// through the log level. +// +// Log messages produce a messaged as "[TIME LEVEL] msg". +// The time is formatted in ISO with space (yyyy-mm-dd hh:mm:ss), but can be +// overridden with define cstring WARTHOG_LOG_TIME. Clock is in local system +// time, but can be set to UTC by definiong WARTHOG_LOG_CLOCK_UTC. +// +// @author: Ryan Hechenberger +// @created: 2025-09-09 +// + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// default log levels, also for global logger +#ifdef WARTHOG_LOG +#define WARTHOG_DEFAULT_LOG_LEVEL WARTHOG_LOG +#elif !defined(NDEBUG) +#define WARTHOG_DEFAULT_LOG_LEVEL 1 +#else +#define WARTHOG_DEFAULT_LOG_LEVEL 2 +#endif + +// specify the log time format +#ifdef WARTHOG_LOG_TIME +#define WARTHOG_LOG_TIME_FORMAT WARTHOG_LOG_TIME +#else +#define WARTHOG_LOG_TIME_FORMAT "%F %T" +#endif + +// set to use utc time instead of local +#ifdef WARTHOG_LOG_CLOCK_UTC +#define WARTHOG_LOG_NOW (std::chrono::utc_clock::now()) +#else +#define WARTHOG_LOG_NOW \ + (std::chrono::current_zone()->to_local(std::chrono::system_clock::now())) +#endif + +namespace warthog::io +{ + +enum class log_level +{ + TRACE = 0, + DEBUG = 1, + INFORMATION = 2, + WARNING = 3, + ERROR = 4, + CRITICAL = 5, + NONE = 6 +}; + +/// @brief the log class used to write out to a log. +/// +/// Takes a pointer for data (must not be null to write). +/// The array call is the logger for each level. +/// log will write if able (data != null && call[level] != null). +struct log_sink +{ + using call_type = void(void*, std::string_view msg); + void* data = nullptr; + std::array(log_level::NONE)> call = {}; + + constexpr void + log(log_level level, std::string_view msg) const + { + if(can_log(level)) { (*call[static_cast(level)])(data, msg); } + } + constexpr bool + can_log(log_level level) const noexcept + { + return static_cast(level) < call.size() && data != nullptr + && call[static_cast(level)] != nullptr; + } + + constexpr const log_sink& + sink() const noexcept + { + return *this; + } +}; + +/// @brief concept that check if class is shaped with a log_sink. +template +concept LogSink = requires(Sink S, log_level level, std::string_view msg) { + S.log(level, msg); + { S.can_log(level) } -> std::same_as; + { std::as_const(S).sink() } -> std::convertible_to; +}; + +/// @brief sink to cout|cerr +/// +/// Is thread safe as long as std::ios_base::sync_with_stdio(false) has not +/// been called. Can copy go log_sink and then be destructed without issue. +struct log_sink_std : log_sink +{ + // cerr = true will sink to cerr, false to cout + log_sink_std(); + log_sink_std(bool cerr); + static void + write_trace(void*, std::string_view msg); + static void + write_debug(void*, std::string_view msg); + static void + write_information(void*, std::string_view msg); + static void + write_warning(void*, std::string_view msg); + static void + write_error(void*, std::string_view msg); + static void + write_critical(void*, std::string_view msg); +}; + +/// @brief sink to a passed stream, or open and own a filestream. +/// +/// This class is thread-safe through use of a mutex. +/// Must be kept in scope at all times while its log_sink is in use. +class log_sink_stream : public log_sink +{ +public: + log_sink_stream(); + log_sink_stream( + const std::filesystem::path& filename, + std::ios_base::openmode mode + = std::ios_base::out | std::ios_base::app); + log_sink_stream(std::ostream* stream); + ~log_sink_stream() = default; + + void + open( + const std::filesystem::path& filename, + std::ios_base::openmode mode + = std::ios_base::out | std::ios_base::app); + void + open(std::ostream& stream); + + static void + write_trace(void*, std::string_view msg); + static void + write_debug(void*, std::string_view msg); + static void + write_information(void*, std::string_view msg); + static void + write_warning(void*, std::string_view msg); + static void + write_error(void*, std::string_view msg); + static void + write_critical(void*, std::string_view msg); + +protected: + std::ostream* log_stream_ = nullptr; + std::unique_ptr owned_file_; + std::mutex lock_; +}; +static_assert(LogSink, "log_stream_sink must be a log_sink."); + +/// @brief the logger class that manipulates a log_sink. +/// @tparam MinLevel the minimum logging level, derived at compile time. +/// Defaults to the programs default log level. +/// +/// A wrapper class for a log_sink that allows to control the log_level. +/// If just using a log_sink, it will always log at every level. +/// This class is handled specially by the logging macros to only log if +/// log_level is a min_level. When setting a long_sink, will clear all pointers +/// below MinLevel. +template< + log_level MinLevel = static_cast(WARTHOG_DEFAULT_LOG_LEVEL)> +struct logger : log_sink +{ + constexpr logger() noexcept = default; + constexpr logger(log_sink sink) : log_sink(sink) + { + for(auto& c : call | std::views::take(static_cast(MinLevel))) + { + c = nullptr; + } + } + + constexpr logger& + operator=(const logger&) noexcept + = default; + constexpr logger& + operator=(log_sink sink) noexcept + { + for(auto& c : call | std::views::take(static_cast(MinLevel))) + { + c = nullptr; + } + return *this; + } + + /// @brief Overrides MinLevel to level. Can only raise, not lower it. + /// @param level + constexpr void + set_min_level(log_level level) noexcept + { + assert( + static_cast(level) + <= static_cast(log_level::NONE)); + for(auto& c : call | std::views::take(static_cast(level))) + { + c = nullptr; + } + } + + constexpr static log_level min_level = MinLevel; + template + constexpr static bool supports_level = MinLevel != log_level::NONE + && static_cast(L) >= static_cast(MinLevel); +}; + +namespace details +{ +template +struct is_logger : std::false_type +{ }; +template +struct is_logger> : std::true_type +{ }; +}; + +template +concept Logger = details::is_logger::value; +template +concept LoggerLevel + = Logger && requires { requires Log::template supports_level; }; + +#define WARTHOG_LOG_LEVEL_(cond, lg, level, msg) \ + { \ + if constexpr(::warthog::io::Logger) \ + { \ + if constexpr(::warthog::io::LoggerLevel) \ + { \ + if(cond) (lg).log(level, msg); \ + } \ + } \ + else \ + { \ + if(cond) (lg).log(level, msg); \ + } \ + } + +#define WARTHOG_LOG(lg, level, msg) (lg).log(level, msg) +#define WARTHOG_LOG_IF(cond, lg, level, msg) \ + { \ + if(cond) (lg).log(level, msg); \ + } + +// Write msg (string_view) to log lg. +#define WARTHOG_TRACE(lg, msg) \ + WARTHOG_LOG_LEVEL_(true, lg, ::warthog::io::log_level::TRACE, msg) +#define WARTHOG_DEBUG(lg, msg) \ + WARTHOG_LOG_LEVEL_(true, lg, ::warthog::io::log_level::DEBUG, msg) +#define WARTHOG_INFO(lg, msg) \ + WARTHOG_LOG_LEVEL_(true, lg, ::warthog::io::log_level::INFORMATION, msg) +#define WARTHOG_WARN(lg, msg) \ + WARTHOG_LOG_LEVEL_(true, lg, ::warthog::io::log_level::WARNING, msg) +#define WARTHOG_ERROR(lg, msg) \ + WARTHOG_LOG_LEVEL_(true, lg, ::warthog::io::log_level::ERROR, msg) +#define WARTHOG_CRIT(lg, msg) \ + WARTHOG_LOG_LEVEL_(true, lg, ::warthog::io::log_level::CRITICAL, msg) + +// Write msg (string_view) to log lg if runtime cond is true. +#define WARTHOG_TRACE_IF(cond, lg, msg) \ + WARTHOG_LOG_LEVEL_(cond, lg, ::warthog::io::log_level::TRACE, msg) +#define WARTHOG_DEBUG_IF(cond, lg, msg) \ + WARTHOG_LOG_LEVEL_(cond, lg, ::warthog::io::log_level::DEBUG, msg) +#define WARTHOG_INFO_IF(cond, lg, msg) \ + WARTHOG_LOG_LEVEL_(cond, lg, ::warthog::io::log_level::INFORMATION, msg) +#define WARTHOG_WARN_IF(cond, lg, msg) \ + WARTHOG_LOG_LEVEL_(cond, lg, ::warthog::io::log_level::WARNING, msg) +#define WARTHOG_ERROR_IF(cond, lg, msg) \ + WARTHOG_LOG_LEVEL_(cond, lg, ::warthog::io::log_level::ERROR, msg) +#define WARTHOG_CRIT_IF(cond, lg, msg) \ + WARTHOG_LOG_LEVEL_(cond, lg, ::warthog::io::log_level::CRITICAL, msg) + +#define WARTHOG_LOG_LEVEL_FMT_(cond, lg, level, ...) \ + { \ + if constexpr(::warthog::io::Logger) \ + { \ + if constexpr(::warthog::io::LoggerLevel) \ + { \ + if(cond) (lg).log(level, std::format(__VA_ARGS__)); \ + } \ + } \ + else \ + { \ + if(cond) (lg).log(level, std::format(__VA_ARGS__)); \ + } \ + } + +#define WARTHOG_LOG_FMT(lg, level, ...) \ + (lg).log(level, std::format(__VA_ARGS__)) +#define WARTHOG_LOG_FMT_IF(cond, lg, level, ...) \ + { \ + if(cond) (lg).log(level, std::format(__VA_ARGS__)); \ + } + +// Write formatted message to log, pass as format,args... +#define WARTHOG_TRACE_FMT(lg, ...) \ + WARTHOG_LOG_LEVEL_FMT_( \ + true, lg, ::warthog::io::log_level::TRACE, __VA_ARGS__) +#define WARTHOG_DEBUG_FMT(lg, ...) \ + WARTHOG_LOG_LEVEL_FMT_( \ + true, lg, ::warthog::io::log_level::DEBUG, __VA_ARGS__) +#define WARTHOG_INFO_FMT(lg, ...) \ + WARTHOG_LOG_LEVEL_FMT_( \ + true, lg, ::warthog::io::log_level::INFORMATION, __VA_ARGS__) +#define WARTHOG_WARN_FMT(lg, ...) \ + WARTHOG_LOG_LEVEL_FMT_( \ + true, lg, ::warthog::io::log_level::WARNING, __VA_ARGS__) +#define WARTHOG_ERROR_FMT(lg, ...) \ + WARTHOG_LOG_LEVEL_FMT_( \ + true, lg, ::warthog::io::log_level::ERROR, __VA_ARGS__) +#define WARTHOG_CRIT_FMT(lg, ...) \ + WARTHOG_LOG_LEVEL_FMT_( \ + true, lg, ::warthog::io::log_level::CRITICAL, __VA_ARGS__) + +// Write formatted message to log if cond is true, pass as format,args... +#define WARTHOG_TRACE_FMT_IF(cond, lg, ...) \ + WARTHOG_LOG_LEVEL_FMT_( \ + cond, lg, ::warthog::io::log_level::TRACE, __VA_ARGS__) +#define WARTHOG_DEBUG_FMT_IF(cond, lg, ...) \ + WARTHOG_LOG_LEVEL_FMT_( \ + cond, lg, ::warthog::io::log_level::DEBUG, __VA_ARGS__) +#define WARTHOG_INFO_FMT_IF(cond, lg, ...) \ + WARTHOG_LOG_LEVEL_FMT_( \ + cond, lg, ::warthog::io::log_level::INFORMATION, __VA_ARGS__) +#define WARTHOG_WARN_FMT_IF(cond, lg, ...) \ + WARTHOG_LOG_LEVEL_FMT_( \ + cond, lg, ::warthog::io::log_level::WARNING, __VA_ARGS__) +#define WARTHOG_ERROR_FMT_IF(cond, lg, ...) \ + WARTHOG_LOG_LEVEL_FMT_( \ + cond, lg, ::warthog::io::log_level::ERROR, __VA_ARGS__) +#define WARTHOG_CRIT_FMT_IF(cond, lg, ...) \ + WARTHOG_LOG_LEVEL_FMT_( \ + cond, lg, ::warthog::io::log_level::CRITICAL, __VA_ARGS__) + +// global logger + +using global_logger_type = logger<>; +/// @brief get the global logger, default is set to std::cerr through +/// log_sink_std +global_logger_type& +glog(); +/// @brief get the global log_sink +const log_sink& +glogs(); +/// @brief sets the global log_sink +void +set_glog(log_sink log); + +/// @brief A logger that sets itself to glog() +/// @tparam MinLevel +template +struct global_logger : logger +{ + using logger = typename global_logger::logger; + constexpr global_logger() : logger(glog().sink()) { } + + constexpr global_logger& + operator=(const global_logger&) noexcept + = default; + + /// @brief Updates the sink to the new glog sink. Must be called if + /// set_glog was used. + void + resync_global() + { + static_cast(*this) = glog().sink(); + } +}; + +// the global version of the marcos, omits the passing of a logger. +#define WARTHOG_GLOG(level, msg) WARTHOG_LOG(::warthog::io::glog(), level, msg) +#define WARTHOG_GTRACE(msg) WARTHOG_TRACE(::warthog::io::glog(), msg) +#define WARTHOG_GDEBUG(msg) WARTHOG_DEBUG(::warthog::io::glog(), msg) +#define WARTHOG_GINFO(msg) WARTHOG_INFO(::warthog::io::glog(), msg) +#define WARTHOG_GWARN(msg) WARTHOG_WARN(::warthog::io::glog(), msg) +#define WARTHOG_GERROR(msg) WARTHOG_ERROR(::warthog::io::glog(), msg) +#define WARTHOG_GCRIT(msg) WARTHOG_CRIT(::warthog::io::glog(), msg) +#define WARTHOG_GLOG_IF(cond, level, msg) \ + WARTHOG_LOG_IF(cond, ::warthog::io::glog(), level, msg) +#define WARTHOG_GTRACE_IF(cond, msg) \ + WARTHOG_TRACE_IF(cond, ::warthog::io::glog(), msg) +#define WARTHOG_GDEBUG_IF(cond, msg) \ + WARTHOG_DEBUG_IF(cond, ::warthog::io::glog(), msg) +#define WARTHOG_GINFO_IF(cond, msg) \ + WARTHOG_INFO_IF(cond, ::warthog::io::glog(), msg) +#define WARTHOG_GWARN_IF(cond, msg) \ + WARTHOG_WARN_IF(cond, ::warthog::io::glog(), msg) +#define WARTHOG_GERROR_IF(cond, msg) \ + WARTHOG_ERROR_IF(cond, ::warthog::io::glog(), msg) +#define WARTHOG_GCRIT_IF(cond, msg) \ + WARTHOG_CRIT_IF(cond, ::warthog::io::glog(), msg) +#define WARTHOG_GLOG_FMT(level, ...) \ + WARTHOG_LOG_FMT(::warthog::io::glog(), level, __VA_ARGS__) +#define WARTHOG_GTRACE_FMT(...) \ + WARTHOG_TRACE_FMT(::warthog::io::glog(), __VA_ARGS__) +#define WARTHOG_GDEBUG_FMT(...) \ + WARTHOG_DEBUG_FMT(::warthog::io::glog(), __VA_ARGS__) +#define WARTHOG_GINFO_FMT(...) \ + WARTHOG_INFO_FMT(::warthog::io::glog(), __VA_ARGS__) +#define WARTHOG_GWARN_FMT(...) \ + WARTHOG_WARN_FMT(::warthog::io::glog(), __VA_ARGS__) +#define WARTHOG_GERROR_FMT(...) \ + WARTHOG_ERROR_FMT(::warthog::io::glog(), __VA_ARGS__) +#define WARTHOG_GCRIT_FMT(...) \ + WARTHOG_CRIT_FMT(::warthog::io::glog(), __VA_ARGS__) +#define WARTHOG_GLOG_FMT_IF(cond, level, ...) \ + WARTHOG_LOG_FMT_IF(cond, ::warthog::io::glog(), level, __VA_ARGS__) +#define WARTHOG_GTRACE_FMT_IF(cond, ...) \ + WARTHOG_TRACE_FMT_IF(cond, ::warthog::io::glog(), __VA_ARGS__) +#define WARTHOG_GDEBUG_FMT_IF(cond, ...) \ + WARTHOG_DEBUG_FMT_IF(cond, ::warthog::io::glog(), __VA_ARGS__) +#define WARTHOG_GINFO_FMT_IF(cond, ...) \ + WARTHOG_INFO_FMT_IF(cond, ::warthog::io::glog(), __VA_ARGS__) +#define WARTHOG_GWARN_FMT_IF(cond, ...) \ + WARTHOG_WARN_FMT_IF(cond, ::warthog::io::glog(), __VA_ARGS__) +#define WARTHOG_GERROR_FMT_IF(cond, ...) \ + WARTHOG_ERROR_FMT_IF(cond, ::warthog::io::glog(), __VA_ARGS__) +#define WARTHOG_GCRIT_FMT_IF(cond, ...) \ + WARTHOG_CRIT_FMT_IF(cond, ::warthog::io::glog(), __VA_ARGS__) + +} // namespace warthog::io + +#endif // WARTHOG_IO_LOG_H diff --git a/include/warthog/io/observer.h b/include/warthog/io/observer.h new file mode 100644 index 0000000..d617bd1 --- /dev/null +++ b/include/warthog/io/observer.h @@ -0,0 +1,122 @@ +#ifndef WARTHOG_IO_OBSERVER_H +#define WARTHOG_IO_OBSERVER_H + +// io/observer.h +// +// Defines use of an observer pattern, in which observer object is registered +// to an observable, and the observable will trigger events to all relevant +// observers. +// The observer is passed to an observable as a list of tuples, +// and when triggering an event will notify all observers by function call of +// the event name that is callable to the observer. +// Observer are either stored as value in the tuple or pointer to an observer. +// +// These function names must be registered before use, common ones registered +// here. +// +// To register a new function name, use WARTHOG_OBSERVER_DEFINE([function]). +// Invoke event with observer_[function](listeners, args...) where listeners +// are tuple of observers. This will run through each element in tuple (i) and +// call i.[function](args...) if able. If i.event([function],args...) is a +// valid callable, calls this function first, also tries i.event([function]). +// +// @author: Ryan Hechenberger +// @created: 2025-08-06 +// + +#include + +/// @brief Creates a concept observer_has_[func_name] that checks of function +/// Listener.func_name(args...) is callable. +#define WARTHOG_OBSERVER_DEFINE_HAS(func_name) \ + template \ + concept observer_has_##func_name = requires(Listener L, Args&&... args) { \ + { L.func_name(std::forward(args)...) }; \ + }; +/// @brief Creates function observer_[func_name] that triggers an event to the +/// observer. +/// +/// Creates function observer_[func_name], which will trigger relevant observer +/// function to all observers in tuple. Requires +/// WARTHOG_OBSERVER_DEFINE_HAS([func_name]). Operates as +/// observer_[func_name](observers, args...), where observers is a tuple of +/// observers. If an observer is a value, is owned by the owner of the tuple. +/// If an observer is a pointer, is not owned by the tuple, and will only +/// trigger events to an observer that is not null. +/// +/// Events are triggered for observers in order of the tuple, and events +/// triggered in order if exists in observer: +/// - event([func_name], args...) if exists otherwise event([func_name]) if +/// exists +/// - [func_name](args...) if exists +#define WARTHOG_OBSERVER_DEFINE_CALL(func_name) \ + template \ + void observer_##func_name(Listeners& L, Args&&... args) \ + { \ + if constexpr(I < std::tuple_size_v) \ + { \ + using T = std::tuple_element_t; \ + using Tb = std::remove_pointer_t; \ + constexpr bool has_func = observer_has_##func_name; \ + if constexpr(::warthog::io::observer_has_event< \ + Tb, const char*, Args...>) \ + { \ + if constexpr(std::is_pointer_v) \ + { \ + if(auto* p = std::get(L); p != nullptr) \ + p->event(#func_name, std::forward(args)...); \ + } \ + else \ + { \ + std::get(L).event( \ + #func_name, std::forward(args)...); \ + } \ + } \ + else if constexpr(::warthog::io::observer_has_event< \ + Tb, const char*>) \ + { \ + if constexpr(std::is_pointer_v) \ + { \ + if(auto* p = std::get(L); p != nullptr) \ + p->event(#func_name); \ + } \ + else { std::get(L).event(#func_name); } \ + } \ + if constexpr(has_func) \ + { \ + if constexpr(std::is_pointer_v) \ + { \ + if(auto* p = std::get(L); p != nullptr) \ + p->func_name(std::forward(args)...); \ + } \ + else \ + { \ + std::get(L).func_name(std::forward(args)...); \ + } \ + } \ + observer_##func_name(L, std::forward(args)...); \ + } \ + } + +/// @brief Defines a observer_[func_name] function and observer_has_[func_name] +/// concept. +#define WARTHOG_OBSERVER_DEFINE(func_name) \ + WARTHOG_OBSERVER_DEFINE_HAS(func_name) \ + WARTHOG_OBSERVER_DEFINE_CALL(func_name) + +namespace warthog::io +{ + +// functions used by WARTHOG_LISTENER_FN +WARTHOG_OBSERVER_DEFINE_HAS(event) + +WARTHOG_OBSERVER_DEFINE(begin_search) +WARTHOG_OBSERVER_DEFINE(end_search) +WARTHOG_OBSERVER_DEFINE(generate_node) +WARTHOG_OBSERVER_DEFINE(expand_node) +WARTHOG_OBSERVER_DEFINE(relax_node) +WARTHOG_OBSERVER_DEFINE(close_node) + +} // namespace warthog::io + +#endif // WARTHOG_IO_OBSERVER_H diff --git a/include/warthog/io/posthoc_trace.h b/include/warthog/io/posthoc_trace.h new file mode 100644 index 0000000..fcdf35a --- /dev/null +++ b/include/warthog/io/posthoc_trace.h @@ -0,0 +1,60 @@ +#ifndef WARTHOG_IO_POSTHOC_TRACE_H +#define WARTHOG_IO_POSTHOC_TRACE_H + +// io/posthoc_trace.h +// +// stream_observer that outputs a trace for use with posthoc visuliser. +// See https://posthoc-app.pathfinding.ai/ +// +// @author: Ryan Hechenberger +// @created: 2025-08-07 +// + +#include "stream_observer.h" + +namespace warthog::io +{ + +/// @brief base posthoc observer class. +/// +/// Set the search_id to set which search the trace is printed for, by default +/// will not trace. event begin_search and end_search will setup the trace to +/// print only a specified. Inherit to create new trace format by overriding +/// print_posthoc_header for custom header. Add own events to print posthoc +/// event to stream() if (*this) holds true, +/// (*this) holds true iff id == search_id for event begin_search once, and +/// ends at end_search. +class posthoc_trace : public stream_observer +{ +public: + // DO NOT INHERIT steam_observer constructors, must be set through stream() + + /// @brief override print the header if not already printed + virtual void + print_posthoc_header() + { + if(*this) + { + stream() << R"posthoc(version: 1.4.0 + events: + )posthoc"; + } + } + + /// @brief override stream setting (not virtual) to print header + /// @param stream + void + open(std::ostream& stream) noexcept + { + stream_observer::open(stream); + if(static_cast(*this)) + { + // print posthoc header on setting the stream + print_posthoc_header(); + } + } +}; + +} // namespace warthog::io + +#endif // WARTHOG_IO_POSTHOC_TRACE_H diff --git a/include/warthog/io/stream_observer.h b/include/warthog/io/stream_observer.h new file mode 100644 index 0000000..33678e8 --- /dev/null +++ b/include/warthog/io/stream_observer.h @@ -0,0 +1,142 @@ +#ifndef WARTHOG_IO_STEAM_OBSERVER_H +#define WARTHOG_IO_STEAM_OBSERVER_H + +// io/stream_observer.h +// +// The stream observer is a base class for observers that can open and own a +// filestream, or pass another filestream. +// This is designed as a many-to-one observers to stream. +// No inbuilt support for multi-threading, use locks in the observer function. +// +// Is designed to be used with observer tuples. +// Inherited class will call stream() to get the current stream for output. +// Using the observer methodology, event functions will be given that will +// write to output in certain ways. +// +// @author: Ryan Hechenberger +// @created: 2025-08-01 +// + +#include "log.h" +#include +#include + +#include +#include +#include + +namespace warthog::io +{ + +/// @brief A base-stream observer. +/// +/// Inherit and give event functions for observers to call. +/// shared_stream_t is a shared_ptr to an std::ostream, and can be passed +/// around to other stream_observer. +class stream_observer +{ +public: + /// @brief observer with no open stream + stream_observer() noexcept = default; + /// @brief copies other stream_observer stream + stream_observer(const stream_observer& stream) noexcept = default; + /// @brief ovserver with ostream, can pass std::cout, std::cerr or user + /// supplied std::ostream + stream_observer(std::ostream& stream) noexcept : stream_(&stream) { } + + /// @brief set stream + void + open(std::ostream& stream) noexcept + { + stream_ = &stream; + } + /// @brief unsets the stream + void + close() noexcept + { + stream_ = nullptr; + } + /// @brief undefined behaviour if no stream is open (asserts on debug) + std::ostream& + stream() const noexcept + { + assert(stream_ != nullptr); + return *stream_; + } + + operator bool() const noexcept { return stream_ != nullptr; } + +protected: + std::ostream* stream_ = nullptr; +}; + +/// @brief observer holds support for printing lines, send to a std::ostream or +/// connect to a log_sink. +class line_observer +{ +public: + /// @brief observer with no open stream + line_observer() noexcept = default; + /// @brief copies other line_observer + line_observer(const line_observer& stream) noexcept = default; + /// @brief ovserver with ostream, can pass std::cout, std::cerr or user + /// supplied std::ostream + line_observer(std::ostream& stream) noexcept : sink_(&stream) { } + /// @brief copies other stream_observer stream + line_observer(log_sink& logger, log_level level) noexcept + { + log(logger, level); + } + + /// @brief pass a stream object + void + open(std::ostream& stream) noexcept + { + sink_.stream = &stream; + } + /// @brief unsets the stream + void + close() noexcept + { + sink_.stream = nullptr; + } + /// @brief pass a log to write line instead of a stream + void + log(log_sink& logger, log_level level) noexcept + { + sink_.log = &logger; + type_ = (int)level; + if((uint32_t)level >= (uint32_t)log_level::NONE) + { + WARTHOG_GWARN("line_observer::log level is out of range"); + close(); + } + } + + operator bool() const noexcept { return sink_.stream != nullptr; } + + /// @brief writes line out (no newline needed), send to std::ostream if set + /// to stream, to log at + /// level if set to log_sink, otherwise do nothing + void + writeln(std::string_view msg) + { + if(sink_.stream != nullptr) + { + if(type_ < 0) { (*sink_.stream) << msg << '\n'; } + else { WARTHOG_LOG(*sink_.log, (log_level)type_, msg); } + } + } + +protected: + union + { + std::ostream* stream = nullptr; + log_sink* log; + } sink_; + int type_ = -1; ///< if <0: sink is type stream, otherwise is type log +}; + +} // namespace warthog::io + +#endif // WARTHOG_IO_STEAM_OBSERVER_H diff --git a/include/warthog/search/problem_instance.h b/include/warthog/search/problem_instance.h index 9a7a271..9f66875 100644 --- a/include/warthog/search/problem_instance.h +++ b/include/warthog/search/problem_instance.h @@ -85,12 +85,34 @@ convert_problem_instance_to_search(const problem_instance& pi, Domain& d) start, target, pi.instance_id_, pi.verbose_, pi.extra_params_}; } -} // namespace warthog::search - std::ostream& -operator<<(std::ostream& str, const warthog::search::problem_instance& pi); +operator<<(std::ostream& str, const problem_instance& pi); std::ostream& -operator<<( - std::ostream& str, const warthog::search::search_problem_instance& pi); +operator<<(std::ostream& str, const search_problem_instance& pi); + +} // namespace warthog::search + +template<::warthog::Identity STATE> +struct std::formatter<::warthog::search::problem_instance_base, char> +{ + template + constexpr auto + parse(ParseContext& ctx) const + { + return ctx.begin(); + } + + template + FmtContext::iterator + format( + const ::warthog::search::problem_instance_base& pi, + FmtContext& ctx) const + { + return std::format_to( + ctx.out(), "problem instance[{}]; start:{} target:{} search_id:{}", + typeid(typename STATE::tag).name(), pi.start_.id, pi.target_.id, + pi.instance_id_); + } +}; #endif // WARTHOG_SEARCH_PROBLEM_INSTANCE_H diff --git a/include/warthog/search/search_metrics.h b/include/warthog/search/search_metrics.h index d54108b..76b8a43 100644 --- a/include/warthog/search/search_metrics.h +++ b/include/warthog/search/search_metrics.h @@ -62,9 +62,9 @@ struct search_metrics // warthog::search_metrics& met); }; -} // namespace warthog::search - std::ostream& -operator<<(std::ostream& str, const warthog::search::search_metrics& met); +operator<<(std::ostream& str, const search_metrics& met); + +} // namespace warthog::search #endif // WARTHOG_SEARCH_SEARCH_METRICS_H diff --git a/include/warthog/search/search_node.h b/include/warthog/search/search_node.h index a0cddf9..90b9eba 100644 --- a/include/warthog/search/search_node.h +++ b/include/warthog/search/search_node.h @@ -8,6 +8,7 @@ // #include +#include #include #include @@ -273,4 +274,27 @@ struct cmp_less_search_node_f_only std::ostream& operator<<(std::ostream& str, const warthog::search::search_node& sn); +template<> +struct std::formatter<::warthog::search::search_node, char> +{ + template + constexpr auto + parse(ParseContext& ctx) const + { + return ctx.begin(); + } + + template + FmtContext::iterator + format(const ::warthog::search::search_node& s, FmtContext& ctx) const + { + return std::format_to( + ctx.out(), + "search_node id:{} p_id:{} g:{} f:{} ub:{} expanded:{} " + "search_number:{}", + s.get_id().id, s.get_parent().id, s.get_g(), s.get_f(), s.get_ub(), + s.get_expanded(), s.get_search_number()); + } +}; + #endif // WARTHOG_SEARCH_SEARCH_NODE_H diff --git a/include/warthog/search/uds_traits.h b/include/warthog/search/uds_traits.h index 8e51229..bfaac52 100644 --- a/include/warthog/search/uds_traits.h +++ b/include/warthog/search/uds_traits.h @@ -13,7 +13,7 @@ // #include "search_metrics.h" -#include +#include namespace warthog::search { @@ -99,24 +99,24 @@ feasible( if(next->get_f() > par->get_max_cost_cutoff()) { - info( - par->verbose_, "cost cutoff", next->get_f(), ">", + WARTHOG_GINFO_FMT_IF( + par->verbose_, "cost cutoff {} > {}", next->get_f(), par->get_max_cost_cutoff()); return false; } if(met->nodes_expanded_ >= par->get_max_expansions_cutoff()) { - info( - par->verbose_, "expansions cutoff", met->nodes_expanded_, ">", + WARTHOG_GINFO_FMT_IF( + par->verbose_, "expansions cutoff {} > {}", met->nodes_expanded_, par->get_max_expansions_cutoff()); return false; } if(met->time_elapsed_nano_ > par->get_max_time_cutoff()) { - info( - par->verbose_, "time cutoff", met->time_elapsed_nano_, ">", + WARTHOG_GINFO_FMT_IF( + par->verbose_, "time cutoff {} > {}", met->time_elapsed_nano_, par->get_max_time_cutoff()); return false; } diff --git a/include/warthog/search/unidirectional_search.h b/include/warthog/search/unidirectional_search.h index b8fec89..2a04e05 100644 --- a/include/warthog/search/unidirectional_search.h +++ b/include/warthog/search/unidirectional_search.h @@ -10,7 +10,6 @@ // @created: 2021-10-13 // -#include "dummy_listener.h" #include "problem_instance.h" #include "search.h" #include "search_parameters.h" @@ -18,8 +17,8 @@ #include "uds_traits.h" #include #include +#include #include -#include #include #include #include @@ -41,7 +40,8 @@ namespace warthog::search // used determine if a search should continue or terminate. // (default: search for any solution, until OPEN is exhausted) template< - class H, class E, class Q = util::pqueue_min, class L = dummy_listener, + typename H, typename E, typename Q = util::pqueue_min, + typename L = std::tuple<>, admissibility_criteria AC = admissibility_criteria::any, feasibility_criteria FC = feasibility_criteria::until_exhaustion, reopen_policy RP = reopen_policy::no> @@ -49,9 +49,9 @@ class unidirectional_search { public: unidirectional_search( - H* heuristic, E* expander, Q* queue, L* listener = nullptr) + H* heuristic, E* expander, Q* queue, L listeners = L{}) : heuristic_(heuristic), expander_(expander), open_(queue), - listener_(listener) + listeners_(listeners) { } ~unidirectional_search() { } @@ -96,27 +96,12 @@ class unidirectional_search sol->s_node_->get_id(), spi->target_, &sol->path_); heuristic_->h(&hv); } - - DO_ON_DEBUG_IF(spi->verbose_) - { - for(auto& node_id : sol->path_) - { - int32_t x, y; - expander_->get_xy(node_id, x, y); - std::cerr << "final path: (" << x << ", " << y << ")..."; - search_node* n - = expander_->generate(expander_->unget_state(node_id)); - assert(n->get_search_number() == spi->instance_id_); - n->print(std::cerr); - std::cerr << std::endl; - } - } } - void - set_listener(L* listener) + L& + get_listeners() noexcept { - listener_ = listener; + return listeners_; } E* @@ -151,7 +136,7 @@ class unidirectional_search H* heuristic_; E* expander_; Q* open_; - L* listener_; + [[no_unique_address]] L listeners_; // no copy ctor unidirectional_search(const unidirectional_search& other) { } @@ -199,8 +184,8 @@ class unidirectional_search if(n->get_ub() < sol->met_.ub_) { sol->met_.ub_ = n->get_ub(); - debug( - pi->verbose_, "NEW UB:", "Incumbent Cost", + WARTHOG_GDEBUG_FMT_IF( + pi->verbose_, "NEW UB: Incumbent Cost {}", sol->sum_of_edge_costs_); } } @@ -212,6 +197,9 @@ class unidirectional_search mytimer.start(); open_->clear(); + io::observer_begin_search( + listeners_, static_cast(pi->instance_id_), *pi); + // initialise the start node and push to OPEN { if(pi->start_ == pad_id::max()) { return; } @@ -223,9 +211,10 @@ class unidirectional_search initialise_node_(start, pad_id::max(), 0, pi, par, sol); open_->push(start); - listener_->generate_node(0, start, 0, UINT32_MAX); - user(pi->verbose_, pi); - trace(pi->verbose_, "Start node:", *start); + io::observer_generate_node( + listeners_, nullptr, *start, 0, UINT32_MAX); + WARTHOG_GINFO_FMT_IF(pi->verbose_, "{}", *pi); + WARTHOG_GINFO_FMT_IF(pi->verbose_, "Start node: {}", *start); update_ub(start, sol, pi); } @@ -247,8 +236,8 @@ class unidirectional_search current->set_expanded(true); // NB: set before generating succ sol->met_.nodes_expanded_++; sol->met_.lb_ = current->get_f(); - listener_->expand_node(current); - trace(pi->verbose_, "Expanding:", *current); + io::observer_expand_node(listeners_, *current); + WARTHOG_GINFO_FMT_IF(pi->verbose_, "Expanding: {}", *current); // Generate successors of the current node search_node* n = nullptr; @@ -258,7 +247,6 @@ class unidirectional_search expander_->get_successor(i, n, cost_to_n); sol->met_.nodes_generated_++; cost_t gval = current->get_g() + cost_to_n; - listener_->generate_node(current, n, gval, i); // Generate new search nodes, provided they're not // dominated by the current upperbound @@ -268,8 +256,10 @@ class unidirectional_search if(n->get_f() < sol->sum_of_edge_costs_) { open_->push(n); - trace(pi->verbose_, "Generate:", *n); + WARTHOG_GINFO_FMT_IF(pi->verbose_, "Generate: {}", *n); update_ub(current, sol, pi); + io::observer_generate_node( + listeners_, current, *n, gval, i); continue; } } @@ -282,12 +272,13 @@ class unidirectional_search < sol->sum_of_edge_costs_) { n->relax(gval, current->get_id()); - listener_->relax_node(n); + io::observer_relax_node(listeners_, *n); if(open_->contains(n)) { open_->decrease_key(n); - trace(pi->verbose_, "Updating;", *n); + WARTHOG_GINFO_FMT_IF( + pi->verbose_, "Updating: {}", *n); update_ub(current, sol, pi); continue; } @@ -295,42 +286,40 @@ class unidirectional_search if(reopen()) { open_->push(n); - trace(pi->verbose_, "Reopen;", *n); + WARTHOG_GINFO_FMT_IF( + pi->verbose_, "Reopen: {}", *n); update_ub(current, sol, pi); sol->met_.nodes_reopen_++; continue; } } } - trace(pi->verbose_, "Dominated;", *n); + WARTHOG_GINFO_FMT_IF(pi->verbose_, "Dominated: {}", *n); } if constexpr(FC == feasibility_criteria::until_cutoff) { // patched until AC FC RP reworked sol->met_.time_elapsed_nano_ = mytimer.elapsed_time_nano(); } + io::observer_close_node(listeners_, *current); + WARTHOG_GINFO_FMT_IF(pi->verbose_, "Expanded: {}", *current); } sol->met_.time_elapsed_nano_ = mytimer.elapsed_time_nano(); sol->met_.nodes_surplus_ = open_->size(); sol->met_.heap_ops_ = open_->get_heap_ops(); - DO_ON_DEBUG_IF(pi->verbose_) - { - if(sol->sum_of_edge_costs_ == warthog::COST_MAX) - { - warning(pi->verbose_, "Search failed; no solution exists."); - } - else { user(pi->verbose_, "Solution found", *sol->s_node_); } - } + WARTHOG_GINFO_IF( + pi->verbose_ && sol->sum_of_edge_costs_ == warthog::COST_MAX, + "Search failed; no solution exists."); } }; template< - class H, class E, class Q = util::pqueue_min, class L = dummy_listener> -unidirectional_search( - H* heuristic, E* expander, Q* queue, - L* listener = nullptr) -> unidirectional_search; + typename H, typename E, typename Q = util::pqueue_min, + typename L = std::tuple<>> +unidirectional_search(H* heuristic, E* expander, Q* queue, L listeners = L{}) + -> unidirectional_search; } // namespace warthog::search diff --git a/include/warthog/util/log.h b/include/warthog/util/log.h deleted file mode 100644 index 90dfcae..0000000 --- a/include/warthog/util/log.h +++ /dev/null @@ -1,237 +0,0 @@ -/** - * @file - * - * Simple log utility. Can work both on preprocessor time or at compile time - * - * All this function works only if: - * @li the macro @c DEBUG is active OR; - * @li the macro @c NDEBUG is not active; - * - * Otherwise, they will become just empty (or can be easily optimized away) - * - * @date Oct 1, 2018 - * @author Massimo Bono - */ - -#ifndef WARTHOG_UTIL_LOG_H -#define WARTHOG_UTIL_LOG_H - -#include "file_utils.h" -#include "macros.h" -#include -#include - -#ifndef QUICK_LOG -#define QUICK_LOG 0 -#endif - -#if QUICK_LOG <= 0 -/** - * always log an entry via std cio - * - * Does no impact performances whatsoever - * - * @code - * debug("hello", person->name, "!"); - * @endcode - * - * @param[in] ... the entries to put on stream - */ -#define debug(p, ...) _abstractLog("DEBUG", p, __VA_ARGS__) -#else -#define debug(...) {}; -#endif - -#if QUICK_LOG <= 1 -/** - * always log an entry via std cio - * - * Does no impact performances whatsoever - * - * @code - * debug("hello", person->name, "!"); - * @endcode - * - * @param[in] ... the entries to put on stream - */ -#define trace(p, ...) _abstractLog("TRACE", p, __VA_ARGS__) -#else -#define trace(...) {}; -#endif - -#if QUICK_LOG <= 4 -/** - * always log an entry via std cio - * - * Does no impact performances whatsoever - * - * @code - * debug("hello", person->name, "!"); - * @endcode - * - * @param[in] ... the entries to put on stream - */ -#define info(p, ...) _abstractLog("INFO ", p, __VA_ARGS__) -#else -#define info(...) {}; -#endif - -#if QUICK_LOG <= 5 -/** - * always log an entry via std cio - * - * Does no impact performances whatsoever - * - * @code - * debug("hello", person->name, "!"); - * @endcode - * - * @param[in] ... the entries to put on stream - */ -#define user(p, ...) _abstractLog("USER ", p, __VA_ARGS__) -#else -#define user(...) {}; -#endif - -#if QUICK_LOG <= 6 -/** - * always log an entry via std cio - * - * Does no impact performances whatsoever - * - * @code - * debug("hello", person->name, "!"); - * @endcode - * - * @param[in] ... the entries to put on stream - */ -#define warning(p, ...) _abstractLog("WARN ", p, __VA_ARGS__) -#else -#define warning(...) {}; -#endif - -#if QUICK_LOG <= 7 -/** - * always log an entry via std cio - * - * Does no impact performances whatsoever - * - * @code - * debug("hello", person->name, "!"); - * @endcode - * - * @param[in] ... the entries to put on stream - */ -#define error(...) _abstractLog("ERROR", true, __VA_ARGS__) -#else -#define error(...) {}; -#endif - -#if QUICK_LOG <= 8 -/** - * always log an entry via std cio - * - * Does no impact performances whatsoever - * - * @code - * debug("hello", person->name, "!"); - * @endcode - * - * @param[in] ... the entries to put on stream - */ -#define critical(...) _abstractLog("CRTCL", true, __VA_ARGS__) -#else -#define critical(...) {}; -#endif - -/** - * like ::log but will log only if at runtime the @c expr will evaluate to true - * - * Will impact performances (even if by little) even if the log is turned off - * - * @code - * clog(true)("hello ", this->name); - * @endcode - * - * @param[in] expr the expression to be evaluated - * @param[in] ... input for ::log - */ -#define clog(expr) _clog1(expr) - -#define _clog1(expr) \ - if(expr) \ - { \ - _clog2 -#define _clog2(...) \ - debug(__VA_ARGS__); \ - } - -/** - * Condition Statement Log utility allows to perform some previous statement - * before logging - * - * This logging macro is useful when your logging needs some local variable - * - * @code - * cslog(true)(int a = 5)("a is %d", a); - * @endcode - * - * @param[in] expr the condition to check if the log is enabled - */ -#define cslog(expr) _cslog1(expr) -#define _cslog1(expr) \ - if(expr) \ - { \ - _cslog2 -#define _cslog2(...) \ - __VA_ARGS__; \ - _cslog3 -#define _cslog3(...) \ - debug(__VA_ARGS__); \ - } - -#define _LOG_OP(context, index, x) x -#define _LOG_COMB(context, x, y) x << " " << y - -#define _debug(...) FOR_EACH(, _LOG_OP, _LOG_COMB, __VA_ARGS__) - -#if defined(DEBUG) || !defined(NDEBUG) -/** - * Perform some work if debug has been enabled - * - * - */ -#define DO_ON_DEBUG_IF(expr) if(expr) -#define DO_ON_DEBUG if(true) - -/** - * always log an entry via std cio - * - * Does no impact performances whatsoever - * - * @code - * debug("hello", person->name, "!"); - * @endcode - * - * @param[in] level the name of the log level - * @param[in] ... the entries to put on stream - */ -#define _abstractLog(level, p, ...) \ - { \ - constexpr auto src = std::source_location::current(); \ - if(p) \ - std::cerr << "[" << level << "] " \ - << warthog::util::getBaseName_(src.file_name()) << "@" \ - << __func__ << ":" << src.line() << " " \ - << _debug(__VA_ARGS__) << std::endl; \ - } - -#else - -#define DO_ON_DEBUG_IF(expr) if(false) -#define DO_ON_DEBUG if(false) -#define _abstractLog(level, ...) {}; - -#endif - -#endif // WARTHOG_UTIL_LOG_H diff --git a/include/warthog/util/vec_io.h b/include/warthog/util/vec_io.h index 6d82c92..8383c8a 100644 --- a/include/warthog/util/vec_io.h +++ b/include/warthog/util/vec_io.h @@ -1,7 +1,6 @@ #ifndef WARTHOG_UTIL_VEC_IO_H #define WARTHOG_UTIL_VEC_IO_H -#include "log.h" #include #include #include @@ -9,6 +8,7 @@ #include #include #include +#include template [[deprecated("TDB")]] @@ -95,9 +95,9 @@ load_vector(std::FILE* file) size_t stuffRead = std::fread(&v[0], sizeof(T), s, file); if((int)stuffRead != s) { - error( - "we were expecting to read ", s, " but we read", stuffRead, - "elements instead"); + WARTHOG_GERROR_FMT( + "we were expecting to read {} but we read {} elements instead", s, + stuffRead); throw std::runtime_error("std::fread failed"); } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1f53bdd..6aebdc3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -7,6 +7,8 @@ geometry/geography.cpp geometry/geom.cpp io/grid.cpp +io/grid_trace.cpp +io/log.cpp memory/node_pool.cpp @@ -26,5 +28,4 @@ util/gm_parser.cpp util/helpers.cpp util/scenario_manager.cpp util/timer.cpp - ) diff --git a/src/io/grid_trace.cpp b/src/io/grid_trace.cpp new file mode 100644 index 0000000..14bd25f --- /dev/null +++ b/src/io/grid_trace.cpp @@ -0,0 +1,155 @@ +#include +#include +#include + +namespace warthog::io +{ + +void +grid_trace::print_posthoc_header() +{ + if(*this) + { + stream() << R"posthoc(version: 1.4.0 +views: + cell: + - $: rect + width: 1 + height: 1 + x: ${{$.x}} + y: ${{$.y}} + fill: ${{$.fill}} + clear: ${{ $.clear }} + main: + - $: cell + $if: ${{ $.type == 'source' }} + fill: green + clear: false + - $: cell + $if: ${{ $.type == 'destination' }} + fill: red + clear: false + - $: cell + $if: ${{ $.type == 'expand' }} + fill: cyan + clear: false + - $: cell + $if: ${{ $.type == 'expand' }} + fill: blue + clear: close + - $: cell + $if: ${{ $.type == 'generate' }} + fill: purple + clear: false + - $: cell + $if: ${{ $.type == 'generate' }} + fill: orange + clear: true +pivot: + x: ${{ $.x + 0.5 }} + y: ${{ $.y + 0.5 }} + scale: 1 +events: +)posthoc"; + } +} + +void +grid_trace::begin_search(int id, const search::search_problem_instance& pi) +{ + if(*this) + { + if(grid_ == nullptr) + { + throw std::logic_error("grid_trace::grid_ is null"); + } + uint32_t x, y; + grid_->to_unpadded_xy(pi.start_, x, y); + stream() << std::format( + " - {{ type: source, id: {}, x: {}, y: {} }}\n", pi.start_.id, x, + y); + grid_->to_unpadded_xy(pi.target_, x, y); + stream() << std::format( + " - {{ type: destination, id: {}, x: {}, y: {} }}\n", + pi.target_.id, x, y); + } +} + +void +grid_trace::expand_node(const node& current) const +{ + if(*this) + { + assert(grid_ != nullptr); + std::string pid; + if(auto cpid = current.get_parent(); !cpid.is_none()) + { + pid = std::format(", pId: {}", cpid.id); + } + uint32_t x, y; + grid_->to_unpadded_xy(current.get_id(), x, y); + stream() << std::format( + " - {{ type: expand, id: {}{}, x: {}, y: {}, f: {}, g: {} }}\n", + current.get_id().id, pid, x, y, current.get_f(), current.get_g()); + } +} + +void +grid_trace::relax_node(const node& current) const +{ + if(*this) + { + assert(grid_ != nullptr); + std::string pid; + if(auto cpid = current.get_parent(); !cpid.is_none()) + { + pid = std::format(", pId: {}", cpid.id); + } + uint32_t x, y; + grid_->to_unpadded_xy(current.get_id(), x, y); + stream() << std::format( + " - {{ type: relax, id: {}{}, x: {}, y: {}, f: {}, g: {} }}\n", + current.get_id().id, pid, x, y, current.get_f(), current.get_g()); + } +} + +void +grid_trace::close_node(const node& current) const +{ + if(*this) + { + assert(grid_ != nullptr); + std::string pid; + if(auto cpid = current.get_parent(); !cpid.is_none()) + { + pid = std::format(", pId: {}", cpid.id); + } + uint32_t x, y; + grid_->to_unpadded_xy(current.get_id(), x, y); + stream() << std::format( + " - {{ type: close, id: {}{}, x: {}, y: {}, f: {}, g: {} }}\n", + current.get_id().id, pid, x, y, current.get_f(), current.get_g()); + } +} + +void +grid_trace::generate_node( + const node* parent, const node& child, cost_t, uint32_t) const +{ + if(*this) + { + assert(grid_ != nullptr); + std::string pid; + if(parent != nullptr) + { + pid = std::format(", pId: {}", parent->get_id().id); + } + uint32_t x, y; + grid_->to_unpadded_xy(child.get_id(), x, y); + stream() << std::format( + " - {{ type: generate, id: {}{}, x: {}, y: {}, f: {}, g: {} }}\n", + child.get_id().id, pid, x, y, child.get_f(), child.get_g()); + } +} + +} // namespace warthog::io diff --git a/src/io/log.cpp b/src/io/log.cpp new file mode 100644 index 0000000..7e7a900 --- /dev/null +++ b/src/io/log.cpp @@ -0,0 +1,189 @@ +#include +#include +#include +#include + +namespace warthog::io +{ + +log_sink_stream::log_sink_stream() + : log_sink{ + nullptr, + {{&log_sink_stream::write_trace, &log_sink_stream::write_debug, + &log_sink_stream::write_information, + &log_sink_stream::write_warning, &log_sink_stream::write_error, + &log_sink_stream::write_critical}}} +{ } + +log_sink_stream::log_sink_stream( + const std::filesystem::path& filename, std::ios_base::openmode mode) + : log_sink_stream() +{ + data = this; + owned_file_ = std::make_unique(filename, mode); + log_stream_ = owned_file_.get(); +} +log_sink_stream::log_sink_stream(std::ostream* stream) : log_sink_stream() +{ + data = this; + log_stream_ = stream; +} + +void +log_sink_stream::open( + const std::filesystem::path& filename, std::ios_base::openmode mode) +{ + data = this; + owned_file_ = std::make_unique(filename, mode); + log_stream_ = owned_file_.get(); +} +void +log_sink_stream::open(std::ostream& stream) +{ + data = this; + owned_file_.release(); + log_stream_ = &stream; +} + +// log_sink_stream + +log_sink_std::log_sink_std() + : log_sink{ + nullptr, + {{&log_sink_std::write_trace, &log_sink_std::write_debug, + &log_sink_std::write_information, &log_sink_std::write_warning, + &log_sink_std::write_error, &log_sink_std::write_critical}}} +{ } +log_sink_std::log_sink_std(bool cerr) + : log_sink{ + cerr ? &std::cerr : &std::cout, + {{&log_sink_std::write_trace, &log_sink_std::write_debug, + &log_sink_std::write_information, &log_sink_std::write_warning, + &log_sink_std::write_error, &log_sink_std::write_critical}}} +{ } + +void +log_sink_std::write_trace(void* logger, std::string_view msg) +{ + std::ostream* stream = static_cast(logger); + assert(stream != nullptr); + *stream << std::format( + "[{:" WARTHOG_LOG_TIME_FORMAT "} TRACE] {}\n", + std::chrono::floor(WARTHOG_LOG_NOW), msg); +} +void +log_sink_std::write_debug(void* logger, std::string_view msg) +{ + std::ostream* stream = static_cast(logger); + assert(stream != nullptr); + *stream << std::format( + "[{:" WARTHOG_LOG_TIME_FORMAT "} DEBUG] {}\n", + std::chrono::floor(WARTHOG_LOG_NOW), msg); +} +void +log_sink_std::write_information(void* logger, std::string_view msg) +{ + std::ostream* stream = static_cast(logger); + assert(stream != nullptr); + *stream << std::format( + "[{:" WARTHOG_LOG_TIME_FORMAT "} INFO] {}\n", + std::chrono::floor(WARTHOG_LOG_NOW), msg); +} +void +log_sink_std::write_warning(void* logger, std::string_view msg) +{ + std::ostream* stream = static_cast(logger); + assert(stream != nullptr); + *stream << std::format( + "[{:" WARTHOG_LOG_TIME_FORMAT "} WARN] {}\n", + std::chrono::floor(WARTHOG_LOG_NOW), msg); +} +void +log_sink_std::write_error(void* logger, std::string_view msg) +{ + std::ostream* stream = static_cast(logger); + assert(stream != nullptr); + *stream << std::format( + "[{:" WARTHOG_LOG_TIME_FORMAT "} ERROR] {}\n", + std::chrono::floor(WARTHOG_LOG_NOW), msg); +} +void +log_sink_std::write_critical(void* logger, std::string_view msg) +{ + std::ostream* stream = static_cast(logger); + assert(stream != nullptr); + *stream << std::format( + "[{:" WARTHOG_LOG_TIME_FORMAT "} CRIT] {}\n", + std::chrono::floor(WARTHOG_LOG_NOW), msg); +} + +void +log_sink_stream::write_trace(void* logger, std::string_view msg) +{ + log_sink_stream* log_stream = static_cast(logger); + assert(log_stream != nullptr); + std::lock_guard lock(log_stream->lock_); + log_sink_std::write_trace(log_stream->log_stream_, msg); +} +void +log_sink_stream::write_debug(void* logger, std::string_view msg) +{ + log_sink_stream* log_stream = static_cast(logger); + assert(log_stream != nullptr); + std::lock_guard lock(log_stream->lock_); + log_sink_std::write_debug(log_stream->log_stream_, msg); +} +void +log_sink_stream::write_information(void* logger, std::string_view msg) +{ + log_sink_stream* log_stream = static_cast(logger); + assert(log_stream != nullptr); + std::lock_guard lock(log_stream->lock_); + log_sink_std::write_information(log_stream->log_stream_, msg); +} +void +log_sink_stream::write_warning(void* logger, std::string_view msg) +{ + log_sink_stream* log_stream = static_cast(logger); + assert(log_stream != nullptr); + std::lock_guard lock(log_stream->lock_); + log_sink_std::write_warning(log_stream->log_stream_, msg); +} +void +log_sink_stream::write_error(void* logger, std::string_view msg) +{ + log_sink_stream* log_stream = static_cast(logger); + assert(log_stream != nullptr); + std::lock_guard lock(log_stream->lock_); + log_sink_std::write_error(log_stream->log_stream_, msg); +} +void +log_sink_stream::write_critical(void* logger, std::string_view msg) +{ + log_sink_stream* log_stream = static_cast(logger); + assert(log_stream != nullptr); + std::lock_guard lock(log_stream->lock_); + log_sink_std::write_critical(log_stream->log_stream_, msg); +} + +namespace +{ +global_logger_type global_logger_(log_sink_std(true).sink()); +} +global_logger_type& +glog() +{ + return global_logger_; +} +const log_sink& +glogs() +{ + return global_logger_.sink(); +} +void +set_glog(log_sink log) +{ + global_logger_ = log; +} + +} // namespace warthog::io diff --git a/src/search/problem_instance.cpp b/src/search/problem_instance.cpp index 2239a7f..602f767 100644 --- a/src/search/problem_instance.cpp +++ b/src/search/problem_instance.cpp @@ -1,7 +1,10 @@ #include +namespace warthog::search +{ + std::ostream& -operator<<(std::ostream& str, const warthog::search::problem_instance& pi) +operator<<(std::ostream& str, const problem_instance& pi) { pi.print(str); @@ -9,10 +12,11 @@ operator<<(std::ostream& str, const warthog::search::problem_instance& pi) } std::ostream& -operator<<( - std::ostream& str, const warthog::search::search_problem_instance& pi) +operator<<(std::ostream& str, const search_problem_instance& pi) { pi.print(str); return str; } + +} diff --git a/src/search/search_metrics.cpp b/src/search/search_metrics.cpp index a2a9fb7..64ed2d2 100644 --- a/src/search/search_metrics.cpp +++ b/src/search/search_metrics.cpp @@ -1,5 +1,8 @@ #include +namespace warthog::search +{ + std::ostream& operator<<(std::ostream& str, const warthog::search::search_metrics& met) { @@ -11,3 +14,5 @@ operator<<(std::ostream& str, const warthog::search::search_metrics& met) << " lb=" << met.lb_ << " ub=" << met.ub_; return str; } + +} diff --git a/src/util/file_utils.cpp b/src/util/file_utils.cpp index 1944804..006f472 100644 --- a/src/util/file_utils.cpp +++ b/src/util/file_utils.cpp @@ -9,7 +9,6 @@ #include #include #include -#include namespace warthog::util {