From c72e925ac27b4db0faed1a368570d46e8227798e Mon Sep 17 00:00:00 2001 From: Erwin Nindl Date: Sat, 26 Jun 2021 09:15:57 +0200 Subject: [PATCH] Adds CPP implementatinon --- CMakeLists.txt | 14 ++++- src/CMakeLists.txt | 8 ++- src/calc.cpp | 70 ++++++++++++++++++++++++ src/calc.hpp | 47 +++++++++++++++++ src/data.hpp | 31 +++++++++++ src/json_output.cpp | 22 ++++++++ src/json_output.hpp | 24 +++++++++ src/main.cpp | 123 +++++++++++++++++++++++++++++++++---------- src/proto_loader.cpp | 61 +++++++++++++++++++++ src/proto_loader.hpp | 24 +++++++++ src/spatial_grid.hpp | 69 ++++++++++++++++++++++++ 11 files changed, 461 insertions(+), 32 deletions(-) create mode 100644 src/calc.cpp create mode 100644 src/calc.hpp create mode 100644 src/data.hpp create mode 100644 src/json_output.cpp create mode 100644 src/json_output.hpp create mode 100644 src/proto_loader.cpp create mode 100644 src/proto_loader.hpp create mode 100644 src/spatial_grid.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 204d722..9cd9bf8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,9 +1,20 @@ -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.11) project(EsriAssignment) set(CMAKE_CXX_STANDARD 17) +include(FetchContent) + +FetchContent_Declare(json + GIT_REPOSITORY https://github.com/ArthurSonzogni/nlohmann_json_cmake_fetchcontent + GIT_TAG v3.9.1) +FetchContent_GetProperties(json) +if(NOT json_POPULATED) + FetchContent_Populate(json) + add_subdirectory(${json_SOURCE_DIR} ${json_BINARY_DIR} EXCLUDE_FROM_ALL) +endif() + find_package(Protobuf REQUIRED) include_directories(${PROTOBUF_INCLUDE_DIR}) include_directories(${CMAKE_CURRENT_BINARY_DIR}) @@ -11,4 +22,3 @@ include_directories(${CMAKE_CURRENT_BINARY_DIR}) add_subdirectory(protocol_buffers_definitions) include_directories(src) add_subdirectory(src) - diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 66b029f..643c14e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,6 +1,10 @@ set(assignment_SOURCES - main.cpp) + main.cpp + calc.cpp + json_output.cpp + proto_loader.cpp) add_executable(main ${assignment_SOURCES}) -target_link_libraries(main proto ${PROTOBUF_LIBRARY}) +target_link_libraries(main PRIVATE proto ${PROTOBUF_LIBRARY}) +target_link_libraries(main PRIVATE nlohmann_json::nlohmann_json) diff --git a/src/calc.cpp b/src/calc.cpp new file mode 100644 index 0000000..5476316 --- /dev/null +++ b/src/calc.cpp @@ -0,0 +1,70 @@ +#include "calc.hpp" +#include + + +namespace calc { + + +std::pair +position_segment(std::vector const& position, double time) +{ + auto second = std::lower_bound(position.begin(), position.end(), time, + [](Position const& pos, double t) { + return pos.time < t; + }); + if (second == position.begin()) { // corner case + second++; + } + return std::make_pair(*(second-1), *second); +} + + +void interpolate_positions(std::vector& magnetics, std::vector const& truth) +{ + for (auto& mag : magnetics) { + auto const truth_seg = position_segment(truth, mag.time); + for (size_t i = 0; i < mag.position.size(); i++) { + mag.position[i] = + (truth_seg.second.position[i] - truth_seg.first.position[i]) / + (truth_seg.second.time - truth_seg.first.time) * + (mag.time - truth_seg.first.time) + truth_seg.first.position[i]; + } + } +} + + +void crop_temporal_inersection(std::vector& magnetics, std::vector const& truth) +{ + auto const mag_first = std::lower_bound( + magnetics.begin(), magnetics.end(), truth[0].time, + [](Magnetic const& mag, double t) { + return mag.time < t; + }); + magnetics.erase(magnetics.begin(), mag_first); + auto const mag_last = std::upper_bound( + magnetics.begin(), magnetics.end(), truth.back().time, + [](double t, Magnetic const& mag) { + return t < mag.time; + }); + magnetics.erase(mag_last, magnetics.end()); +} + + +std::tuple +spatial_extent(std::vector const& mag) +{ + auto [x_min, x_max] = std::minmax_element( + std::cbegin(mag), std::cend(mag), + [](Magnetic const& a, Magnetic const& b) { + return a.position[0] < b.position[0]; + }); + auto [y_min, y_max] = std::minmax_element( + std::cbegin(mag), std::cend(mag), + [](Magnetic const& a, Magnetic const& b) { + return a.position[1] < b.position[1]; + }); + return std::make_tuple(x_min->position[0], y_min->position[1], x_max->position[0], y_max->position[1]); +} + + +} \ No newline at end of file diff --git a/src/calc.hpp b/src/calc.hpp new file mode 100644 index 0000000..a4e952b --- /dev/null +++ b/src/calc.hpp @@ -0,0 +1,47 @@ +#ifndef CALC_HPP_ +#define CALC_HPP_ + +#include "data.hpp" + +#include +#include +#include + + +namespace calc { + + +template +double norm(iter_t first, iter_t last) +{ + return std::sqrt(std::inner_product(first, last, first, 0.0)); +} + + +std::pair +position_segment(std::vector const& position, double time); + +void interpolate_positions(std::vector& magnetics, std::vector const& truth); +void crop_temporal_inersection(std::vector& magnetics, std::vector const& truth); + +std::tuple +spatial_extent(std::vector const& mag); + +template +void average_grid(grid_t& grid, avg_grid_t& average_grid) +{ + for (size_t x = 0; x < grid.size_x(); x++) { + for (size_t y = 0; y < grid.size_y(); y++) { + auto const& cell = grid[x][y]; + if (cell.size() > 0) { + double const sum = std::accumulate(cell.cbegin(), cell.cend(), 0.0); + size_t const n = cell.size(); + average_grid[x][y] = sum / n; + } + } + } +} + +} // namespace calc + +#endif // CALC_HPP_ diff --git a/src/data.hpp b/src/data.hpp new file mode 100644 index 0000000..760359e --- /dev/null +++ b/src/data.hpp @@ -0,0 +1,31 @@ +#ifndef DATA_HPP_ +#define DATA_HPP_ + +#include +#include + +using Coordinate = std::array; + + +struct Magnetic { + Magnetic(double t, double x, double y, double z) : + time(t), field({x, y, z}) {} + Magnetic(double t, double x, double y) : + time(t), position({x, y}) {} + double time; + std::array field; + Coordinate position; +}; + + +struct Position { + Position(double t, double x, double y) : + time(t), position({x, y}) {} + double time; + Coordinate position; +}; + +using Magnetics = std::vector; +using Positions = std::vector; + +#endif /* DATA_HPP_ */ diff --git a/src/json_output.cpp b/src/json_output.cpp new file mode 100644 index 0000000..3bc6e35 --- /dev/null +++ b/src/json_output.cpp @@ -0,0 +1,22 @@ + +#include "json_output.hpp" + +#include +#include + + +JsonOutput::JsonOutput(char const* file_path) : + filePath_(file_path) +{} + + +bool JsonOutput::write() +{ + std::ofstream outfile(filePath_); + if (!outfile) { + return false; + } + outfile << std::setw(2); + outfile << jsonData_ << std::endl; + return true; +} diff --git a/src/json_output.hpp b/src/json_output.hpp new file mode 100644 index 0000000..e73d5a7 --- /dev/null +++ b/src/json_output.hpp @@ -0,0 +1,24 @@ +#ifndef JSONOUTPUT_HPP_ +#define JSONOUTPUT_HPP_ + +#include + + +class JsonOutput { +public: + explicit JsonOutput(char const* file_path); + ~JsonOutput() = default; + + template + void add(char const* key, T const& obj) + { + jsonData_[key] = obj; + } + bool write(); + +private: + char const* filePath_; + ::nlohmann::json jsonData_; +}; + +#endif // JSONOUTPUT_HPP_ diff --git a/src/main.cpp b/src/main.cpp index 71e1af2..64f52e9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,40 +1,107 @@ -#include +#include "calc.hpp" +#include "json_output.hpp" +#include "proto_loader.hpp" +#include "spatial_grid.hpp" + #include - -#include "protocol_buffers_definitions/recordings.pb.h" +#include +#include +#include +#include -using namespace ::indoors::proto; +static void print_usage() +{ + std::cout << "ESRI C++ code assignment, E. Nindl 2021\n\n"; + std::cout << "Usage:\n"; + std::cout << " executable \n"; +} + + +std::tuple +total_spatial_extent(std::vector const& magnetics_recs) { + double x_min = std::numeric_limits::max(); + double y_min = std::numeric_limits::max(); + double x_max = std::numeric_limits::min(); + double y_max = std::numeric_limits::min(); + for (auto const& magnetics : magnetics_recs) { + auto const [x_min_i, y_min_i, x_max_i, y_max_i] = ::calc::spatial_extent(magnetics); + x_min = std::min(x_min, x_min_i); + y_min = std::min(y_min, y_min_i); + x_max = std::max(x_max, x_max_i); + y_max = std::max(y_max, y_max_i); + } + return std::make_tuple(x_min, y_min, x_max, y_max); +} + + +static bool write_output(std::string const& output_file, SpatialGrid& grid) +{ + JsonOutput json_output(output_file.c_str()); + json_output.add("start_x", grid.start_x()); + json_output.add("start_y", grid.start_y()); + json_output.add("spacing", grid.spacing); + json_output.add("data", grid.data()); + return json_output.write(); +} + int main(int argc, char* argv[]) { - // Verify that the version of the library that we linked against is - // compatible with the version of the headers we compiled against. - GOOGLE_PROTOBUF_VERIFY_VERSION; - - std::cout << "ESRI C++ code assignment\n"; - - Recording recording; - Position positions; - - std::fstream input(argv[1], std::ios::in | std::ios::binary); - if (!recording.ParseFromIstream(&input)) { - std::cerr << "Failed to parse input file.\n"; - return -1; + if (argc < 3) { + print_usage(); + return 1; } - std::cout.precision(17); - for (int i=0; i < recording.magnetics_size(); i++) { - auto const& s = recording.magnetics(i); - std::cout << s.t() << ": [" << s.x() << ", " << s.y() << ", " << s.z() << "]\n"; - } - for (int i=0; i < recording.positions_size(); i++) { - auto const& s = recording.positions(i); - std::cout << s.t() << ": [" << s.x() << ", " << s.y() << "]\n"; + std::string const output_file{argv[argc - 1]}; + std::vector input_files; + for (uint32_t i = 1; i < (argc - 1); i++) { + input_files.push_back(argv[i]); } - // Optional: Delete all global objects allocated by libprotobuf. - google::protobuf::ShutdownProtobufLibrary(); + std::vector magnetics_recs; + std::vector groundtruth_recs; + + try { + for (auto const& input_file : input_files) { + ProtoLoader loader(input_file.c_str()); + magnetics_recs.push_back(loader.magnetics()); + groundtruth_recs.push_back(loader.groundtruth()); + } + } catch (std::exception& e) { + std::cerr << "Error loading input: " << e.what() << "\n"; + return 1; + } + + for (size_t rec_id = 0; rec_id < magnetics_recs.size(); rec_id++) { + Magnetics& magnetics = magnetics_recs[rec_id]; + Positions& groundtruth = groundtruth_recs[rec_id]; + ::calc::crop_temporal_inersection(magnetics, groundtruth); + if (magnetics.size() == 0) { + std::cerr << "Error in groundtruth data\n"; + return 1; + } + ::calc::interpolate_positions(magnetics, groundtruth); + } + + auto const [x_min, y_min, x_max, y_max] = total_spatial_extent(magnetics_recs); + SpatialGrid> grid(x_min, y_min, x_max, y_max); + + for (size_t rec_id = 0; rec_id < magnetics_recs.size(); rec_id++) { + Magnetics& magnetics = magnetics_recs[rec_id]; + Positions& groundtruth = groundtruth_recs[rec_id]; + for (auto& mag : magnetics) { + auto& cell = grid.at(mag.position); + double const magnitude = ::calc::norm(mag.field.cbegin(), mag.field.cend()); + cell.push_back(magnitude); + } + } + + SpatialGrid average_grid(x_min, y_min, x_max, y_max); + calc::average_grid(grid, average_grid); + if (!write_output(output_file, average_grid)) { + std::cout << "Error writing output file: " << output_file << "\n"; + return 1; + } return 0; } - diff --git a/src/proto_loader.cpp b/src/proto_loader.cpp new file mode 100644 index 0000000..25af3e5 --- /dev/null +++ b/src/proto_loader.cpp @@ -0,0 +1,61 @@ +#include "proto_loader.hpp" +#include "protocol_buffers_definitions/recordings.pb.h" + +#include +#include + + + +ProtoLoader::ProtoLoader(char const* f) : + filePath_(f) +{ + // Verify that the version of the library that we linked against is + // compatible with the version of the headers we compiled against. + GOOGLE_PROTOBUF_VERIFY_VERSION; + + std::fstream input(filePath_, std::ios::in | std::ios::binary); + if (!recording_.ParseFromIstream(&input)) { + throw std::runtime_error("Failed to parse input file"); + } +} + + +ProtoLoader::~ProtoLoader() +{ + // Optional: Delete all global objects allocated by libprotobuf. + ::google::protobuf::ShutdownProtobufLibrary(); +} + + +std::vector ProtoLoader::magnetics() const +{ + double t_prev = 0; + std::vector magnetics; + for (int i = 0; i < recording_.magnetics_size(); i++) { + auto const& s = recording_.magnetics(i); + double const t = s.t(); + if (t_prev > t) { + throw std::runtime_error("Magnetics data not sorted by time"); + } + magnetics.emplace_back(t, s.x(), s.y(), s.z()); + t_prev = t; + } + return magnetics; +} + + +std::vector ProtoLoader::groundtruth() const +{ + double t_prev = 0; + std::vector groundtruth; + for (int i=0; i < recording_.positions_size(); i++) { + auto const& s = recording_.positions(i); + double const t = s.t(); + if ((t - t_prev) < dt_min) { + throw std::runtime_error("Groundtruth data invalid"); + } + groundtruth.emplace_back(t, s.x(), s.y()); + t_prev = t; + } + return groundtruth; +} diff --git a/src/proto_loader.hpp b/src/proto_loader.hpp new file mode 100644 index 0000000..dcd4474 --- /dev/null +++ b/src/proto_loader.hpp @@ -0,0 +1,24 @@ +#ifndef PROTOLOADER_HPP_ +#define PROTOLOADER_HPP_ + +#include "data.hpp" +#include "protocol_buffers_definitions/recordings.pb.h" +#include + + +class ProtoLoader { +public: + explicit ProtoLoader(char const* file_path); + ~ProtoLoader(); + + std::vector magnetics() const; + std::vector groundtruth() const; + + static constexpr double dt_min{1e-3}; + +private: + char const* filePath_; + ::indoors::proto::Recording recording_; +}; + +#endif /* PROTOLOADER_HPP_ */ diff --git a/src/spatial_grid.hpp b/src/spatial_grid.hpp new file mode 100644 index 0000000..fa6897e --- /dev/null +++ b/src/spatial_grid.hpp @@ -0,0 +1,69 @@ +#ifndef SPATIALGRID_HPP_ +#define SPATIALGRID_HPP_ + +#include +#include +#include +#include + + +template +class SpatialGrid { +public: + SpatialGrid() = delete; + explicit SpatialGrid(double x_start, double y_start, double x_end, double y_end) : + offset_({static_cast(x_start / spacing), static_cast(y_start / spacing)}), + size_x_(static_cast(std::ceil(x_end / spacing)) - offset_[0]), + size_y_(static_cast(std::ceil(y_end / spacing)) - offset_[1]), + grid_(size_x_, std::vector(size_y_)) + {} + ~SpatialGrid() = default; + + value_t& at(double x, double y) + { + size_t const id_x{static_cast(x / spacing) - offset_[0]}; + size_t const id_y{static_cast(y / spacing) - offset_[1]}; + return grid_[id_x][id_y]; + } + value_t& at(std::array pos) + { + return at(pos[0], pos[1]); + } + + std::vector& operator[](size_t idx) + { + return grid_[idx]; + } + size_t size_x() const + { + return grid_.size(); + } + size_t size_y() const + { + return grid_[0].size(); + } + size_t start_x() const noexcept + { + return offset_[0] * spacing; + } + size_t start_y() const noexcept + { + return offset_[1] * spacing; + } + + std::vector>& data() { + return grid_; + } + + static constexpr size_t spacing{5}; + +private: + size_t const offset_[2]; + size_t const size_x_; + size_t const size_y_; + + std::vector> grid_; +}; + + +#endif /* SPATIALGRID_HPP_ */