From 2531574be113f6a2169b55779b155202bfcaa3a7 Mon Sep 17 00:00:00 2001 From: Dirk Vanden Boer Date: Thu, 29 Aug 2024 14:31:00 +0200 Subject: [PATCH] Use a faster csv parser to parse the point sources --- bootstrap.py | 6 +- deps/geodynamix | 2 +- .../FindFastCppCsvParser.cmake | 20 --- .../fast-cpp-csv-parser/portfile.cmake | 15 -- .../fast-cpp-csv-parser/vcpkg.json | 7 - deps/vcpkg | 2 +- logic/configurationparser.cpp | 1 + logic/debugtools.cpp | 1 + logic/emissioninventory.cpp | 1 + logic/emissionscollector.cpp | 4 +- logic/gridprocessing.cpp | 1 + logic/include/emap/emissioninventory.h | 38 +++++ logic/include/emap/inputconversion.h | 1 + logic/inputparsers.cpp | 161 +++++++++++++----- logic/modelrun.cpp | 3 +- logic/outputbuilderfactory.cpp | 3 +- logic/spatialpatterninventory.cpp | 1 + .../05_model_parameters/code_conversions.xlsx | Bin 18253 -> 18851 bytes logic/test/data/point_sources.csv | 19 +++ logic/test/data/point_sources_empty_coord.csv | 3 + logic/test/gridprocessingtest.cpp | 1 + logic/test/inputparsertest.cpp | 74 ++++++-- 22 files changed, 253 insertions(+), 111 deletions(-) delete mode 100644 deps/overlay-ports/fast-cpp-csv-parser/FindFastCppCsvParser.cmake delete mode 100644 deps/overlay-ports/fast-cpp-csv-parser/portfile.cmake delete mode 100644 deps/overlay-ports/fast-cpp-csv-parser/vcpkg.json create mode 100644 logic/test/data/point_sources.csv create mode 100644 logic/test/data/point_sources_empty_coord.csv diff --git a/bootstrap.py b/bootstrap.py index 860474c..f692b3a 100755 --- a/bootstrap.py +++ b/bootstrap.py @@ -36,14 +36,10 @@ "c:/DEV/bld" # avoid long path issues by using a short build path ) - overlay_ports = os.path.abspath( - os.path.join(os.path.dirname(__file__), "deps", "overlay-ports") - ) - if args.clean: vcpkg.clean(triplet=triplet) else: - vcpkg.bootstrap(ports_dir=os.path.join(".", "deps"), triplet=triplet, build_root=build_root, install_root=install_root, overlay_ports=overlay_ports, clean_after_build=args.clean_after_build) + vcpkg.bootstrap(ports_dir=os.path.join(".", "deps"), triplet=triplet, build_root=build_root, install_root=install_root, clean_after_build=args.clean_after_build) except KeyboardInterrupt: print("\nInterrupted") sys.exit(-1) diff --git a/deps/geodynamix b/deps/geodynamix index 0ab6194..7366eff 160000 --- a/deps/geodynamix +++ b/deps/geodynamix @@ -1 +1 @@ -Subproject commit 0ab61942c9f0787541d24f7b165f6cf37eb74b85 +Subproject commit 7366eff9d2b032225d36e5380d6881ef7704080e diff --git a/deps/overlay-ports/fast-cpp-csv-parser/FindFastCppCsvParser.cmake b/deps/overlay-ports/fast-cpp-csv-parser/FindFastCppCsvParser.cmake deleted file mode 100644 index 7ba8809..0000000 --- a/deps/overlay-ports/fast-cpp-csv-parser/FindFastCppCsvParser.cmake +++ /dev/null @@ -1,20 +0,0 @@ -include(FindPackageHandleStandardArgs) - -find_path(FastCppCsvParser_INCLUDE_DIR - NAMES csv.h - HINTS ${FastCppCsvParser_ROOT_DIR}/include ${FastCppCsvParser_INCLUDEDIR} -) - -find_package_handle_standard_args(FastCppCsvParser - FOUND_VAR FastCppCsvParser_FOUND - REQUIRED_VARS FastCppCsvParser_INCLUDE_DIR -) - -mark_as_advanced(FastCppCsvParser_ROOT_DIR FastCppCsvParser_INCLUDE_DIR) - -if(FastCppCsvParser_FOUND AND NOT TARGET FastCppCsvParser::csv) - add_library(FastCppCsvParser::csv INTERFACE IMPORTED) - set_target_properties(FastCppCsvParser::csv PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES "${FastCppCsvParser_INCLUDE_DIR}" - ) -endif() diff --git a/deps/overlay-ports/fast-cpp-csv-parser/portfile.cmake b/deps/overlay-ports/fast-cpp-csv-parser/portfile.cmake deleted file mode 100644 index 25d840b..0000000 --- a/deps/overlay-ports/fast-cpp-csv-parser/portfile.cmake +++ /dev/null @@ -1,15 +0,0 @@ -# header-only library - -vcpkg_from_github( - OUT_SOURCE_PATH SOURCE_PATH - REPO ben-strasser/fast-cpp-csv-parser - REF 75600d0b77448e6c410893830df0aec1dbacf8e3 - SHA512 aab418e98eb895dabd6369b186b7a55beddb84b89e358395a9f125829074916eff9086d80f9cd342d1bfd91acacc7103875c970a84164b75fff259cc93729285 - HEAD_REF master -) - -file(INSTALL ${SOURCE_PATH}/csv.h DESTINATION ${CURRENT_PACKAGES_DIR}/include) -file(INSTALL ${CMAKE_CURRENT_LIST_DIR}/FindFastCppCsvParser.cmake DESTINATION ${CURRENT_PACKAGES_DIR}/share/cmake) - -# Handle copyright -configure_file(${SOURCE_PATH}/LICENSE ${CURRENT_PACKAGES_DIR}/share/${PORT}/copyright COPYONLY) diff --git a/deps/overlay-ports/fast-cpp-csv-parser/vcpkg.json b/deps/overlay-ports/fast-cpp-csv-parser/vcpkg.json deleted file mode 100644 index 8e2640a..0000000 --- a/deps/overlay-ports/fast-cpp-csv-parser/vcpkg.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "fast-cpp-csv-parser", - "version-string": "2021-01-03", - "port-version": 1, - "description": "A small, easy-to-use and fast header-only library for reading comma separated value (CSV) files", - "homepage": "https://github.com/ben-strasser/fast-cpp-csv-parser" -} diff --git a/deps/vcpkg b/deps/vcpkg index 53238eb..57adc34 160000 --- a/deps/vcpkg +++ b/deps/vcpkg @@ -1 +1 @@ -Subproject commit 53238eb7cdb557aa018585f4b12e288727fc9bdb +Subproject commit 57adc34a5f058ef12ebcba10a78fec9026da9b11 diff --git a/logic/configurationparser.cpp b/logic/configurationparser.cpp index d0ad5e8..4614f67 100644 --- a/logic/configurationparser.cpp +++ b/logic/configurationparser.cpp @@ -14,6 +14,7 @@ namespace emap { using namespace inf; using namespace std::string_view_literals; +namespace gdal = inf::gdal; static EmissionDestination emission_destination_from_string(std::string_view str) { diff --git a/logic/debugtools.cpp b/logic/debugtools.cpp index 76ec229..dbb6a0d 100644 --- a/logic/debugtools.cpp +++ b/logic/debugtools.cpp @@ -18,6 +18,7 @@ namespace emap { using namespace inf; using namespace std::string_literals; +namespace gdal = inf::gdal; class VectorBuilder { diff --git a/logic/emissioninventory.cpp b/logic/emissioninventory.cpp index 004bc33..0f9cdf2 100644 --- a/logic/emissioninventory.cpp +++ b/logic/emissioninventory.cpp @@ -15,6 +15,7 @@ namespace emap { using namespace inf; using namespace date::literals; +namespace gdal = inf::gdal; static fs::path throw_if_not_exists(const fs::path& path) { diff --git a/logic/emissionscollector.cpp b/logic/emissionscollector.cpp index 7587547..ddf66e5 100644 --- a/logic/emissionscollector.cpp +++ b/logic/emissionscollector.cpp @@ -77,9 +77,7 @@ void EmissionsCollector::add_emissions(const CountryCellCoverage& countryInfo, c } for (auto& entry : pointEmissions) { - if (entry.value().amount() > 0.0) { - _outputBuilder->add_point_output_entry(entry); - } + _outputBuilder->add_point_output_entry(entry); } if (diffuseEmissions.empty() && !pointEmissions.empty()) { diff --git a/logic/gridprocessing.cpp b/logic/gridprocessing.cpp index 3bf8621..2ead359 100644 --- a/logic/gridprocessing.cpp +++ b/logic/gridprocessing.cpp @@ -32,6 +32,7 @@ namespace emap { using namespace inf; using namespace std::string_literals; +namespace gdal = inf::gdal; gdal::VectorDataSet transform_vector(const fs::path& vectorPath, const GeoMetadata& destMeta) { diff --git a/logic/include/emap/emissioninventory.h b/logic/include/emap/emissioninventory.h index 690a13b..5e01f11 100644 --- a/logic/include/emap/emissioninventory.h +++ b/logic/include/emap/emissioninventory.h @@ -1,6 +1,7 @@ #pragma once #include "emap/emissions.h" +#include "infra/math.h" namespace emap { @@ -223,6 +224,26 @@ class EmissionCollection throw inf::RuntimeError("No emission found with id: {}", id); } + const TEmission& emission_with_id_at_coordinate(const EmissionIdentifier& id, Coordinate coord) const + { + auto iter = std::find_if(_emissions.begin(), _emissions.end(), [&id, &coord](const TEmission& em) { + if (em.id() == id) { + if (auto coordOpt = em.coordinate(); coordOpt.has_value()) { + return inf::math::approx_equal(coord.x, coordOpt->x, 1e-4) && + inf::math::approx_equal(coord.y, coordOpt->y, 1e-4); + } + } + + return false; + }); + + if (iter == _emissions.end()) { + throw inf::RuntimeError("No emission found with id: {} at coordinate {}", id, coord); + } + + return *iter; + } + std::optional try_emission_with_id(const EmissionIdentifier& id) const noexcept { auto emissionIter = find_sorted(id); @@ -243,6 +264,23 @@ class EmissionCollection return result; } + std::vector emissions_with_id_at_coordinate(const EmissionIdentifier& id, Coordinate coord) const + { + std::vector result; + std::copy_if(_emissions.begin(), _emissions.end(), std::back_inserter(result), [&id, &coord](const TEmission& em) { + if (em.id() == id) { + if (auto coordOpt = em.coordinate(); coordOpt.has_value()) { + return inf::math::approx_equal(coord.x, coordOpt->x, 1e-4) && + inf::math::approx_equal(coord.y, coordOpt->y, 1e-4); + } + } + + return false; + }); + + return result; + } + size_t empty() const noexcept { return _emissions.empty(); diff --git a/logic/include/emap/inputconversion.h b/logic/include/emap/inputconversion.h index f7b42db..3ee75d7 100644 --- a/logic/include/emap/inputconversion.h +++ b/logic/include/emap/inputconversion.h @@ -3,6 +3,7 @@ #include "infra/exception.h" #include "infra/string.h" +#include #include #include #include diff --git a/logic/inputparsers.cpp b/logic/inputparsers.cpp index 5aaccee..ae6eca5 100644 --- a/logic/inputparsers.cpp +++ b/logic/inputparsers.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -25,18 +26,10 @@ #include #include -#if defined(_MSC_VER) -#pragma warning(push) -#pragma warning(disable : 4267 4244) -#endif -#include -#if defined(_MSC_VER) -#pragma warning(pop) -#endif - namespace emap { using namespace inf; +namespace gdal = inf::gdal; static int32_t required_csv_column(const inf::CsvReader& csv, const std::string& columnName) { @@ -209,64 +202,131 @@ SingleEmissions parse_point_sources(const fs::path& emissionsCsv, const RunConfi size_t lineNr = 2; - std::unordered_map pointSourceEmissions; - try { Log::debug("Parse emissions: {}", emissionsCsv); - /* + std::vector pointSources; + std::unordered_map pointSourceEmissions; + +#if 1 using namespace io; - CSVReader<21, trim_chars<' ', '\t'>, no_quote_escape<';'>, throw_on_overflow> in(str::from_u8(emissionsCsv.u8string())); + CSVReader<25, trim_chars<' ', '\t'>, no_quote_escape<';'>, throw_on_overflow> in(file::u8string(emissionsCsv)); + + in.read_header(ignore_missing_column | ignore_extra_column, + "type", + "scenario", + "year", + "reporting_country", + "nfr_sector", + "gnfr_sector", + "pollutant", + "emission", + "unit", + "x", + "y", + "hoogte_m", + "diameter_m", + "temperatuur_C", + "warmteinhoud_MW", + "debiet_Nm3/u", + "Debiet_Nm3/u", + "dv", + "type_emissie", + "EIL_nummer", + "exploitatie_naam", + "EIL_Emissiepunt_Jaar_Naam", + "Activiteit_type", + "subtype", + "pointsource_index"); + + if (!in.has_column("nfr_sector") && !in.has_column("gnfr_sector")) { + throw RuntimeError("Missing nfr_sector or gnfr_sector column"); + } + auto sectorType = in.has_column("nfr_sector") ? EmissionSector::Type::Nfr : EmissionSector::Type::Gnfr; + bool hasSubTypeCol = in.has_column("subtype"); + bool hasDvCol = in.has_column("dv"); + bool hasPointSourceIndexCol = in.has_column("pointsource_index"); + bool hasCoordinates = in.has_column("x") && in.has_column("y"); + + int32_t year = 0, dv = 0; + double value, height, diameter, temp, warmthContents, flowRate; + char *type, *scenario, *countryStr, *nfrSectorName, *gnfrSectorName, *pollutantName, *unit, *emissionType, *eilNr, *explName, *eilYearName, *activityType; + char *subtype = nullptr, *psIndex = nullptr; + char *x = nullptr, *y = nullptr; + while (in.read_row(type, scenario, year, countryStr, nfrSectorName, gnfrSectorName, pollutantName, value, unit, x, y, height, diameter, temp, warmthContents, flowRate, flowRate, dv, emissionType, eilNr, explName, eilYearName, activityType, subtype, psIndex)) { + auto sectorName = std::string_view(sectorType == EmissionSector::Type::Nfr ? nfrSectorName : gnfrSectorName); + const auto country = countryInv.try_country_from_string(countryStr); - int32_t year; - double value, height, diameter, temp, warmthContents, x, y, flowRate; - char *type, *scenario, *countryStr, *sectorName, *pollutantName, *unit, *excType, *eilNr, *explName, *naceCode, *eilYearName, *activityType, *subtype; - while (in.read_row(type, scenario, year, countryStr, sectorName, pollutant, value, x, y, unit, height, diameter, temp, warmthContents, flowRate, excType, eilNr, explName, naceCode, eilYearName, activityType, subtype)) { - if (sectorInv.is_ignored_sector(sectorType, sectorName) || - pollutantInv.is_ignored_pollutant(pollutantName)) { + if (sectorName.empty() || + sectorInv.is_ignored_sector(sectorType, sectorName, *country) || + pollutantInv.is_ignored_pollutant(pollutantName, *country)) { continue; } - double emissionValue = to_giga_gram(to_double(line.get_string(colEmission), lineNr), line.get_string(colUnit)); + double emissionValue = to_giga_gram(value, unit); + if (emissionValue == 0.0) { + continue; + } auto sector = sectorInv.try_sector_from_string(sectorType, sectorName); - auto country = countryInv.try_country_from_string(line.get_string(colCountry)); auto pollutant = pollutantInv.try_pollutant_from_string(pollutantName); - if (sector.has_value() && country.has_value() && pollutant.has_value()) { - EmissionEntry info( - EmissionIdentifier(*country, *sector, *pollutant), - EmissionValue(emissionValue)); - - info.set_height(line.get_double(colHeight).value_or(0.0)); - info.set_diameter(line.get_double(colDiameter).value_or(0.0)); - info.set_temperature(line.get_double(colTemperature).value_or(-9999.0)); - info.set_warmth_contents(line.get_double(colWarmthContents).value_or(0.0)); - info.set_flow_rate(line.get_double(colFlowRate).value_or(0.0)); - - std::string subType = "none"; - if (colSubType.has_value()) { - subType = line.get_string(*colSubType); + + if (sector.has_value() && pollutant.has_value()) { + PointSourceIdentifier ps; + ps.sector = *sector; + ps.country = *country; + ps.pollutant = *pollutant; + ps.height = height; + ps.diameter = diameter; + ps.temperature = temp; + ps.warmthContents = warmthContents; + ps.flowRate = flowRate; + + if (hasSubTypeCol) { + ps.subType = subtype ? subtype : "none"; + } else if (hasPointSourceIndexCol) { + ps.subType = psIndex ? psIndex : "none"; + } else { + ps.subType = "none"; } - info.set_source_id(fmt::format("{}_{}_{}_{}_{}_{}_{}_{}", info.height(), info.diameter(), info.temperature(), info.warmth_contents(), info.flow_rate(), line.get_string(colEilPoint), line.get_string(colEil), subType)); + ps.eilNumber = eilNr; + ps.eilPoint = eilYearName; - if (colX.has_value() && colY.has_value()) { - auto x = line.get_double(*colX); - auto y = line.get_double(*colY); - if (x.has_value() && y.has_value()) { - info.set_coordinate(Coordinate(*x, *y)); + if (hasCoordinates) { + auto xVal = str::to_double(x); + auto yVal = str::to_double(y); + if (xVal.has_value() && yVal.has_value()) { + ps.coordinate = Coordinate(*xVal, *yVal); } else { - throw RuntimeError("Invalid coordinate in point sources: {}", line.get_string(*colX), line.get_string(*colY)); + throw RuntimeError("Invalid coordinate in point sources: x='{}' y='{}'", x, y); } } - result.add_emission(std::move(info)); + if (hasDvCol) { + ps.dv = dv; + } + + if (combineIdentical) { + pointSourceEmissions[ps] += emissionValue; + } else { + pointSources.push_back(ps.to_emission_entry(emissionValue)); + } + } else { + if (!pollutant.has_value()) { + Log::warn("Unknown pollutant name: {}", pollutantName); + } + + if (!sector.has_value()) { + Log::warn("Unknown sector name: {}", sectorName); + } } + ++lineNr; - }*/ + } +#else - SingleEmissions result(cfg.year()); inf::CsvReader csv(emissionsCsv); auto colCountry = required_csv_column(csv, "reporting_country"); @@ -304,6 +364,9 @@ SingleEmissions parse_point_sources(const fs::path& emissionsCsv, const RunConfi } double emissionValue = to_giga_gram(to_double(line.get_string(colEmission), lineNr), line.get_string(colUnit)); + if (emissionValue == 0.0) { + continue; + } auto sector = sectorInv.try_sector_from_string(sectorType, sectorName); auto pollutant = pollutantInv.try_pollutant_from_string(pollutantName); @@ -339,7 +402,7 @@ SingleEmissions parse_point_sources(const fs::path& emissionsCsv, const RunConfi if (combineIdentical) { pointSourceEmissions[ps] += emissionValue; } else { - result.add_emission(ps.to_emission_entry(emissionValue)); + pointSources.push_back(ps.to_emission_entry(emissionValue)); } } else { if (!pollutant.has_value()) { @@ -354,12 +417,16 @@ SingleEmissions parse_point_sources(const fs::path& emissionsCsv, const RunConfi ++lineNr; } +#endif + if (combineIdentical) { for (const auto& [ps, emission] : pointSourceEmissions) { - result.add_emission(ps.to_emission_entry(emission)); + pointSources.push_back(ps.to_emission_entry(emission)); } } + SingleEmissions result(cfg.year()); + result.set_emissions(std::move(pointSources)); return result; } catch (const std::exception& e) { throw RuntimeError("Error parsing {} line {} ({})", emissionsCsv, lineNr, e.what()); diff --git a/logic/modelrun.cpp b/logic/modelrun.cpp index cbaa9d8..eca7760 100644 --- a/logic/modelrun.cpp +++ b/logic/modelrun.cpp @@ -32,6 +32,7 @@ namespace emap { using namespace inf; +namespace gdal = inf::gdal; struct SpatialPatternProcessInfo { @@ -453,4 +454,4 @@ int run_model(const RunConfiguration& cfg, const ModelProgress::Callback& progre return EXIT_FAILURE; } } -} \ No newline at end of file +} diff --git a/logic/outputbuilderfactory.cpp b/logic/outputbuilderfactory.cpp index 385e15c..ea53d8e 100644 --- a/logic/outputbuilderfactory.cpp +++ b/logic/outputbuilderfactory.cpp @@ -13,6 +13,7 @@ namespace emap { using namespace inf; +namespace gdal = inf::gdal; static std::unordered_map parse_chimere_country_mapping(const fs::path& mappingPath, const CountryInventory& countryInv) { @@ -90,4 +91,4 @@ std::unique_ptr make_output_builder(const RunConfiguration& cfg) throw RuntimeError("No known output builder for the specified grid definition"); } -} \ No newline at end of file +} diff --git a/logic/spatialpatterninventory.cpp b/logic/spatialpatterninventory.cpp index 011241c..8172e7c 100644 --- a/logic/spatialpatterninventory.cpp +++ b/logic/spatialpatterninventory.cpp @@ -20,6 +20,7 @@ namespace emap { using namespace inf; +namespace gdal = inf::gdal; static std::set scan_available_years(const fs::path& spatialPatternPath) { diff --git a/logic/test/data/_input/05_model_parameters/code_conversions.xlsx b/logic/test/data/_input/05_model_parameters/code_conversions.xlsx index 1f8f1c680a2636a16fa3ceb8a190a9889d9172dd..2a928b1017144660449c030efe3d5c069c71747f 100644 GIT binary patch delta 9002 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