From e6fcf465b729443d4e2ecc4356be62cbaf1ce519 Mon Sep 17 00:00:00 2001 From: Siarhei Fedartsou Date: Sat, 26 Nov 2022 22:31:13 +0100 Subject: [PATCH] wip --- CMakeLists.txt | 6 ++ include/updater/csv_file_parser.hpp | 7 +- include/updater/data_source.hpp | 6 +- include/updater/file_parser.hpp | 5 +- include/updater/parquet_file_parser.hpp | 49 +++++++------- include/updater/updater_config.hpp | 54 +++++++++------- src/tools/contract.cpp | 14 ++-- src/tools/customize.cpp | 8 ++- src/updater/data_source.cpp | 61 ++++++++++-------- src/updater/updater.cpp | 6 +- unit_tests/updater/parquet.cpp | 23 +++++-- unit_tests/updater/speed.parquet | Bin 0 -> 5597 bytes unit_tests/updater/speed_without_rate.parquet | Bin 0 -> 3681 bytes unit_tests/updater/speeds_file.parquet | Bin 464 -> 0 bytes 14 files changed, 141 insertions(+), 98 deletions(-) create mode 100644 unit_tests/updater/speed.parquet create mode 100644 unit_tests/updater/speed_without_rate.parquet delete mode 100644 unit_tests/updater/speeds_file.parquet diff --git a/CMakeLists.txt b/CMakeLists.txt index ec780882a..bd70ddabf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -508,6 +508,9 @@ if(ENABLE_CONAN) onetbb:shared=${TBB_SHARED} boost:without_stacktrace=True # Apple Silicon cross-compilation fails without it arrow:parquet=True + arrow:with_snappy=True + arrow:with_brotli=True + arrow:with_zlib=True BUILD missing ) # explicitly say Conan to use x86 dependencies if build for x86 platforms (https://github.com/conan-io/cmake-conan/issues/141) @@ -569,6 +572,9 @@ else() add_dependency_includes(${TBB_INCLUDE_DIR}) set(TBB_LIBRARIES TBB::tbb) + find_package(Parquet REQUIRED) + set(ARROW_LIBRARIES Parquet::parquet_shared) + find_package(EXPAT REQUIRED) add_dependency_includes(${EXPAT_INCLUDE_DIRS}) diff --git a/include/updater/csv_file_parser.hpp b/include/updater/csv_file_parser.hpp index 8ab3f3dfb..2a92bac33 100644 --- a/include/updater/csv_file_parser.hpp +++ b/include/updater/csv_file_parser.hpp @@ -1,9 +1,9 @@ #ifndef OSRM_UPDATER_CSV_FILE_PARSER_HPP #define OSRM_UPDATER_CSV_FILE_PARSER_HPP #include "file_parser.hpp" +#include #include #include -#include #include "updater/source.hpp" @@ -47,7 +47,8 @@ template struct CSVFilesParser : public FilesPars private: // Parse a single CSV file and return result as a vector - std::vector> ParseFile(const std::string &filename, std::size_t file_id) const final + std::vector> ParseFile(const std::string &filename, + std::size_t file_id) const final { namespace qi = boost::spirit::qi; @@ -90,7 +91,7 @@ template struct CSVFilesParser : public FilesPars throw util::exception(message.str() + SOURCE_REF); } } - + const KeyRule key_rule; const ValueRule value_rule; }; diff --git a/include/updater/data_source.hpp b/include/updater/data_source.hpp index 54551a675..4bfe1fa24 100644 --- a/include/updater/data_source.hpp +++ b/include/updater/data_source.hpp @@ -10,8 +10,10 @@ namespace updater { namespace data { -SegmentLookupTable readSegmentValues(const std::vector &paths, SpeedAndTurnPenaltyFormat format); -TurnLookupTable readTurnValues(const std::vector &paths, SpeedAndTurnPenaltyFormat format); +SegmentLookupTable readSegmentValues(const std::vector &paths, + SpeedAndTurnPenaltyFormat format); +TurnLookupTable readTurnValues(const std::vector &paths, + SpeedAndTurnPenaltyFormat format); } // namespace data } // namespace updater } // namespace osrm diff --git a/include/updater/file_parser.hpp b/include/updater/file_parser.hpp index 2ed2449f7..830ed23b6 100644 --- a/include/updater/file_parser.hpp +++ b/include/updater/file_parser.hpp @@ -1,8 +1,8 @@ #ifndef OSRM_UPDATER_FILE_PARSER_HPP #define OSRM_UPDATER_FILE_PARSER_HPP +#include #include #include -#include #include "updater/source.hpp" @@ -88,7 +88,8 @@ template struct FilesParser protected: // Parse a single CSV file and return result as a vector - virtual std::vector> ParseFile(const std::string &filename, std::size_t file_id) const; + virtual std::vector> ParseFile(const std::string &filename, + std::size_t file_id) const; }; } // namespace updater } // namespace osrm diff --git a/include/updater/parquet_file_parser.hpp b/include/updater/parquet_file_parser.hpp index 7e46b3431..308451162 100644 --- a/include/updater/parquet_file_parser.hpp +++ b/include/updater/parquet_file_parser.hpp @@ -1,11 +1,11 @@ #ifndef OSRM_UPDATER_PARQUET_FILE_PARSER_HPP #define OSRM_UPDATER_PARQUET_FILE_PARSER_HPP #include "file_parser.hpp" +#include #include #include #include #include -#include #include "updater/source.hpp" @@ -31,24 +31,26 @@ template struct ParquetFilesParser : public Files { private: // Parse a single Parquet file and return result as a vector - std::vector> ParseFile(const std::string &filename, std::size_t file_id) const final + std::vector> ParseFile(const std::string &filename, + std::size_t file_id) const final { - try { + try + { std::shared_ptr infile; - PARQUET_ASSIGN_OR_THROW( - infile, - arrow::io::ReadableFile::Open(filename)); + PARQUET_ASSIGN_OR_THROW(infile, arrow::io::ReadableFile::Open(filename)); parquet::StreamReader os{parquet::ParquetFileReader::Open(infile)}; std::vector> result; - while ( !os.eof() ) + while (!os.eof()) { result.emplace_back(ReadKeyValue(os, file_id)); } return result; - } catch (const std::exception &e) { + } + catch (const std::exception &e) + { throw util::exception(e.what() + SOURCE_REF); } } @@ -64,38 +66,31 @@ template struct ParquetFilesParser : public Files return {key, value}; } - void Read(parquet::StreamReader &os, Turn& turn) const { - int64_t from, via, to; - os >> from >> via >> to; - turn.from = from; - turn.via = via; - turn.to = to; + void Read(parquet::StreamReader &os, Turn &turn) const + { + os >> turn.from >> turn.via >> turn.to; } - void Read(parquet::StreamReader &os, PenaltySource& penalty_source) const { + void Read(parquet::StreamReader &os, PenaltySource &penalty_source) const + { os >> penalty_source.duration >> penalty_source.weight; } - void Read(parquet::StreamReader &os, Segment& segment) const { - int64_t from; - int64_t to; - os >> from >> to; - - segment.from = from; - segment.to = to; - //std::cerr << from << " " << to<< std::endl; - //os >> segment.from >> segment.to >> parquet::EndRow; + void Read(parquet::StreamReader &os, Segment &segment) const + { + os >> segment.from >> segment.to; } - void Read(parquet::StreamReader &os, SpeedSource& speed_source) const { + void Read(parquet::StreamReader &os, SpeedSource &speed_source) const + { std::optional rate; os >> speed_source.speed >> rate; // TODO: boost::optional - if (rate) { + if (rate) + { speed_source.rate = *rate; } } - }; } // namespace updater } // namespace osrm diff --git a/include/updater/updater_config.hpp b/include/updater/updater_config.hpp index dcc46c810..b32c221cd 100644 --- a/include/updater/updater_config.hpp +++ b/include/updater/updater_config.hpp @@ -42,11 +42,11 @@ namespace osrm namespace updater { - - enum class SpeedAndTurnPenaltyFormat { - CSV, - PARQUET - }; +enum class SpeedAndTurnPenaltyFormat +{ + CSV, + PARQUET +}; struct UpdaterConfig final : storage::IOConfig { @@ -76,45 +76,51 @@ struct UpdaterConfig final : storage::IOConfig double log_edge_updates_factor = 0.0; std::time_t valid_now; -SpeedAndTurnPenaltyFormat speed_and_turn_penalty_format = SpeedAndTurnPenaltyFormat::CSV; + SpeedAndTurnPenaltyFormat speed_and_turn_penalty_format = SpeedAndTurnPenaltyFormat::CSV; std::vector segment_speed_lookup_paths; std::vector turn_penalty_lookup_paths; std::string tz_file_path; }; - -inline std::istream& operator>> (std::istream &in, SpeedAndTurnPenaltyFormat& format) { +inline std::istream &operator>>(std::istream &in, SpeedAndTurnPenaltyFormat &format) +{ std::string token; in >> token; - std::transform(token.begin(), token.end(), token.begin(), [](auto c){ return std::tolower(c); }); + std::transform( + token.begin(), token.end(), token.begin(), [](auto c) { return std::tolower(c); }); - if (token == "csv") { + if (token == "csv") + { format = SpeedAndTurnPenaltyFormat::CSV; - } else if (token == "parquet") { + } + else if (token == "parquet") + { format = SpeedAndTurnPenaltyFormat::PARQUET; - } else { - throw boost::program_options::validation_error{boost::program_options::validation_error::invalid_option_value}; + } + else + { + throw boost::program_options::validation_error{ + boost::program_options::validation_error::invalid_option_value}; } return in; } - -inline std::ostream& operator<< (std::ostream &out, SpeedAndTurnPenaltyFormat format) { - switch (format) { - case SpeedAndTurnPenaltyFormat::CSV: - out << "csv"; - break; - case SpeedAndTurnPenaltyFormat::PARQUET: - out << "parquet"; - break; +inline std::ostream &operator<<(std::ostream &out, SpeedAndTurnPenaltyFormat format) +{ + switch (format) + { + case SpeedAndTurnPenaltyFormat::CSV: + out << "csv"; + break; + case SpeedAndTurnPenaltyFormat::PARQUET: + out << "parquet"; + break; } return out; } - - } // namespace updater } // namespace osrm diff --git a/src/tools/contract.cpp b/src/tools/contract.cpp index 2288ab0f3..e6b7701c0 100644 --- a/src/tools/contract.cpp +++ b/src/tools/contract.cpp @@ -49,11 +49,15 @@ return_code parseArguments(int argc, "core,k", boost::program_options::value(&contractor_config.core_factor)->default_value(1.0), "DEPRECATED: Will always be 1.0. Percentage of the graph (in vertices) to contract " - "[0..1].") ("speed-and-turn-penalty-format", boost::program_options::value(&contractor_config.updater_config.speed_and_turn_penalty_format)->default_value(updater::SpeedAndTurnPenaltyFormat::CSV))("segment-speed-file", - boost::program_options::value>( - &contractor_config.updater_config.segment_speed_lookup_paths) - ->composing(), - "Lookup files containing nodeA, nodeB, speed data to adjust edge weights")( + "[0..1].")("speed-and-turn-penalty-format", + boost::program_options::value( + &contractor_config.updater_config.speed_and_turn_penalty_format) + ->default_value(updater::SpeedAndTurnPenaltyFormat::CSV))( + "segment-speed-file", + boost::program_options::value>( + &contractor_config.updater_config.segment_speed_lookup_paths) + ->composing(), + "Lookup files containing nodeA, nodeB, speed data to adjust edge weights")( "turn-penalty-file", boost::program_options::value>( &contractor_config.updater_config.turn_penalty_lookup_paths) diff --git a/src/tools/customize.cpp b/src/tools/customize.cpp index 4ae8ad6fb..7d14f8834 100644 --- a/src/tools/customize.cpp +++ b/src/tools/customize.cpp @@ -21,7 +21,6 @@ enum class return_code : unsigned exit }; - return_code parseArguments(int argc, char *argv[], std::string &verbosity, @@ -41,8 +40,11 @@ return_code parseArguments(int argc, ("threads,t", boost::program_options::value(&customization_config.requested_num_threads) ->default_value(std::thread::hardware_concurrency()), - "Number of threads to use") - ("speed-and-turn-penalty-format", boost::program_options::value(&customization_config.updater_config.speed_and_turn_penalty_format)->default_value(updater::SpeedAndTurnPenaltyFormat::CSV))( + "Number of threads to use")( + "speed-and-turn-penalty-format", + boost::program_options::value( + &customization_config.updater_config.speed_and_turn_penalty_format) + ->default_value(updater::SpeedAndTurnPenaltyFormat::CSV))( "segment-speed-file", boost::program_options::value>( &customization_config.updater_config.segment_speed_lookup_paths) diff --git a/src/updater/data_source.cpp b/src/updater/data_source.cpp index 456cd8ccb..a74d3de16 100644 --- a/src/updater/data_source.cpp +++ b/src/updater/data_source.cpp @@ -34,37 +34,45 @@ namespace updater namespace data { -namespace { -std::unique_ptr> makeSegmentParser(SpeedAndTurnPenaltyFormat format) { - switch (format) { - case SpeedAndTurnPenaltyFormat::CSV: - { - static const auto value_if_blank = std::numeric_limits::quiet_NaN(); - const qi::real_parser> unsigned_double; - return std::make_unique>(qi::ulong_long >> ',' >> qi::ulong_long, - unsigned_double >> -(',' >> (qi::double_ | qi::attr(value_if_blank)))); - } - case SpeedAndTurnPenaltyFormat::PARQUET: - return std::make_unique>(); +namespace +{ +std::unique_ptr> +makeSegmentParser(SpeedAndTurnPenaltyFormat format) +{ + switch (format) + { + case SpeedAndTurnPenaltyFormat::CSV: + { + static const auto value_if_blank = std::numeric_limits::quiet_NaN(); + const qi::real_parser> unsigned_double; + return std::make_unique>( + qi::ulong_long >> ',' >> qi::ulong_long, + unsigned_double >> -(',' >> (qi::double_ | qi::attr(value_if_blank)))); } -} + case SpeedAndTurnPenaltyFormat::PARQUET: + return std::make_unique>(); + } +} -std::unique_ptr> makeTurnParser(SpeedAndTurnPenaltyFormat format) { - switch (format) { - case SpeedAndTurnPenaltyFormat::CSV: - { - return std::make_unique>(qi::ulong_long >> ',' >> qi::ulong_long >> ',' >> - qi::ulong_long, - qi::double_ >> -(',' >> qi::double_)); - } - case SpeedAndTurnPenaltyFormat::PARQUET: - return std::make_unique>(); +std::unique_ptr> makeTurnParser(SpeedAndTurnPenaltyFormat format) +{ + switch (format) + { + case SpeedAndTurnPenaltyFormat::CSV: + { + return std::make_unique>( + qi::ulong_long >> ',' >> qi::ulong_long >> ',' >> qi::ulong_long, + qi::double_ >> -(',' >> qi::double_)); } -} + case SpeedAndTurnPenaltyFormat::PARQUET: + return std::make_unique>(); + } +} } // namespace -SegmentLookupTable readSegmentValues(const std::vector &paths, SpeedAndTurnPenaltyFormat format) +SegmentLookupTable readSegmentValues(const std::vector &paths, + SpeedAndTurnPenaltyFormat format) { auto parser = makeSegmentParser(format); @@ -83,7 +91,8 @@ SegmentLookupTable readSegmentValues(const std::vector &paths, Spee return result; } -TurnLookupTable readTurnValues(const std::vector &paths, SpeedAndTurnPenaltyFormat format) +TurnLookupTable readTurnValues(const std::vector &paths, + SpeedAndTurnPenaltyFormat format) { auto parser = makeTurnParser(format); return (*parser)(paths); diff --git a/src/updater/updater.cpp b/src/updater/updater.cpp index 6b8ea8919..c3d4ad29c 100644 --- a/src/updater/updater.cpp +++ b/src/updater/updater.cpp @@ -618,7 +618,8 @@ Updater::LoadAndUpdateEdgeExpandedGraph(std::vector &e tbb::concurrent_vector updated_segments; if (update_edge_weights) { - auto segment_speed_lookup = data::readSegmentValues(config.segment_speed_lookup_paths, config.speed_and_turn_penalty_format); + auto segment_speed_lookup = data::readSegmentValues(config.segment_speed_lookup_paths, + config.speed_and_turn_penalty_format); TIMER_START(segment); updated_segments = updateSegmentData(config, @@ -633,7 +634,8 @@ Updater::LoadAndUpdateEdgeExpandedGraph(std::vector &e util::Log() << "Updating segment data took " << TIMER_MSEC(segment) << "ms."; } - auto turn_penalty_lookup = data::readTurnValues(config.turn_penalty_lookup_paths, config.speed_and_turn_penalty_format); + auto turn_penalty_lookup = data::readTurnValues(config.turn_penalty_lookup_paths, + config.speed_and_turn_penalty_format); if (update_turn_penalties) { auto updated_turn_penalties = updateTurnPenalties(config, diff --git a/unit_tests/updater/parquet.cpp b/unit_tests/updater/parquet.cpp index 35a99ef2c..07a93d75c 100644 --- a/unit_tests/updater/parquet.cpp +++ b/unit_tests/updater/parquet.cpp @@ -1,13 +1,28 @@ +#include +#include +#include +#include #include -#include #include +#include +#include using namespace osrm; using namespace osrm::updater; BOOST_AUTO_TEST_CASE(parquet_readSegmentValues) { - boost::filesystem::path test_path(TEST_DATA_DIR "/speeds_file.parquet"); - SegmentLookupTable segment_lookup_table = data::readSegmentValues({test_path.string()}, SpeedAndTurnPenaltyFormat::PARQUET); - BOOST_CHECK_EQUAL(segment_lookup_table.lookup.size(), 2); + { + SegmentLookupTable segment_lookup_table = data::readSegmentValues( + {boost::filesystem::path{TEST_DATA_DIR "/speed.parquet"}.string()}, + SpeedAndTurnPenaltyFormat::PARQUET); + BOOST_CHECK_EQUAL(segment_lookup_table.lookup.size(), 100); + } + + { + SegmentLookupTable segment_lookup_table = data::readSegmentValues( + {boost::filesystem::path{TEST_DATA_DIR "/speed_without_rate.parquet"}.string()}, + SpeedAndTurnPenaltyFormat::PARQUET); + BOOST_CHECK_EQUAL(segment_lookup_table.lookup.size(), 100); + } } \ No newline at end of file diff --git a/unit_tests/updater/speed.parquet b/unit_tests/updater/speed.parquet new file mode 100644 index 0000000000000000000000000000000000000000..cec25b6a5cb85b6dab9e1b6cef24e9d8e5c9c125 GIT binary patch literal 5597 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