diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index eeece11..003459d 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -111,7 +111,7 @@ jobs: - name: run tests run: | pip install ddt requests - python -v tests/full_corpus/test_full_corpus.py + python -v tests/full_corpus/test.py test_wheels_macos_14: needs: [build_wheels] @@ -125,6 +125,7 @@ jobs: echo "~/.pyenv/shims" >> $GITHUB_PATH - name: install dependencies run: | + brew cleanup --prune=all && rm -rf "$(brew --cache)" brew update brew install pyenv pyenv install ${{ matrix.python-version }} @@ -143,7 +144,7 @@ jobs: run: | pyenv local ${{ matrix.python-version }} python3 -m pip install --break-system-packages ddt requests - python3 tests/full_corpus/test_full_corpus.py + python3 tests/full_corpus/test.py upload_pypi: diff --git a/.github/workflows/tests-with-coverage.yml b/.github/workflows/tests-with-coverage.yml index fb15cd2..3148ae6 100644 --- a/.github/workflows/tests-with-coverage.yml +++ b/.github/workflows/tests-with-coverage.yml @@ -1,6 +1,8 @@ name: Tests Linux with coverage -on: [push] +on: + push: + pull_request: jobs: build: @@ -10,11 +12,16 @@ jobs: - uses: actions/checkout@v6 with: submodules: true - - name: Install dependencies + - name: Install system dependencies run: | sudo apt update - sudo apt install -y python3-pip python3-dev lcov g++ catch2 - pip install --upgrade meson ninja numpy meson-python>=0.14.0 build wheel ddt requests + sudo apt install -y lcov g++ catch2 + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version: '3.13' + - name: Install Python dependencies + run: python3.13 -m pip install meson ninja numpy meson-python>=0.14.0 build wheel ddt requests - name: Configure with meson run: meson -Db_coverage=true -Dwith_tests=true . build - name: Build (meson) diff --git a/.gitignore b/.gitignore index 2c364d0..c6b4376 100644 --- a/.gitignore +++ b/.gitignore @@ -5,10 +5,12 @@ subprojects/* CMakeLists.txt.user .cache/* .cmake/* +.idea/* *.user *.user.* *.ipynb_check* *__pycache__* +*.patch version.txt docs/_build/* docs/generated/* diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 258583e..096bdc0 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -11,6 +11,7 @@ build: apt_packages: - g++ - ninja-build + - cmake tools: python: "3.12" # You can also specify other tool versions: diff --git a/benchmarks/chrono/main.cpp b/benchmarks/chrono/main.cpp new file mode 100644 index 0000000..533fc79 --- /dev/null +++ b/benchmarks/chrono/main.cpp @@ -0,0 +1,168 @@ +#include +#include +#include + +inline constexpr std::size_t mega(std::size_t n) +{ + return n * 1024 * 1024; +} + +template +no_init_vector generate_sorted_time_vectors(T start, T end, std::size_t count) +{ + T step = (end - start) / count; + no_init_vector result(count); + for (auto& v : result) + { + v = T(start); + start += step; + } + return result; +} + +template +inline auto& value_ref(T& t) +{ + if constexpr (std::is_same_v) + { + return t.nseconds; + } + else if constexpr (std::is_same_v) + { + return t.mseconds; + } + else if constexpr (std::is_same_v) + { + return t.seconds; + } +} + + +template +no_init_vector generate_sorted_time_vectors(T start, T end, std::size_t count) +{ + T step((value_ref(end) - value_ref(start)) / count); + no_init_vector result(count); + for (auto& v : result) + { + v = start; + value_ref(start) += value_ref(step); + } + return result; +} + + +template +static void BM_leap_second(benchmark::State& state, leap_func_t leap_func) +{ + constexpr std::size_t start = 63'072'000'000'000'000; + constexpr std::size_t end = 2'081'948'754'000'000'000; + + const auto time_vect = generate_sorted_time_vectors(start, end, state.range(0)); + for (auto _ : state) + { + int64_t leap = 0; + for (const auto& time : time_vect) + { + leap ^= leap_func(time); + } + benchmark::DoNotOptimize(leap); + } + state.counters["Epochs"] = std::size(time_vect); + state.counters["epochs_per_second"] + = benchmark::Counter(std::size(time_vect), benchmark::Counter::kIsIterationInvariantRate); +} +BENCHMARK_CAPTURE(BM_leap_second, branchless, cdf::chrono::_impl::leap_second_branchless) + ->RangeMultiplier(4) + ->Range(100, mega(64)) + ->Complexity(); + +BENCHMARK_CAPTURE( + BM_leap_second, baseline, static_cast(cdf::chrono::_impl::leap_second)) + ->RangeMultiplier(4) + ->Range(100, mega(64)) + ->Complexity(); + +template +static void BM_to_ns_from_1970(benchmark::State& state, func_t func, time_t start, time_t end) +{ + const auto time_vect = generate_sorted_time_vectors(start, end, state.range(0)); + no_init_vector output(time_vect.size()); + for (auto _ : state) + { + benchmark::ClobberMemory(); + func(time_vect, output.data()); + benchmark::DoNotOptimize(output); + } + state.counters["Epochs"] = std::size(time_vect); + state.counters["epochs_per_second"] + = benchmark::Counter(std::size(time_vect), benchmark::Counter::kIsIterationInvariantRate); +} + +BENCHMARK_CAPTURE(BM_to_ns_from_1970, tt2000_scalar, + static_cast&, int64_t* const)>( + cdf::chrono::_impl::scalar_to_ns_from_1970), + cdf::tt2000_t{63'072'000'000'000'000}, + cdf::tt2000_t{2'081'948'754'000'000'000} + ) + ->RangeMultiplier(4) + ->Range(10, mega(1024)) + ->Complexity() + ->UseRealTime(); + +BENCHMARK_CAPTURE(BM_to_ns_from_1970, tt2000_vectorized, + static_cast&, int64_t* const)>( + vectorized_to_ns_from_1970), + cdf::tt2000_t{63'072'000'000'000'000}, + cdf::tt2000_t{2'081'948'754'000'000'000} + ) + ->RangeMultiplier(4) + ->Range(10, mega(1024)) + ->Complexity() + ->UseRealTime(); + +BENCHMARK_CAPTURE(BM_to_ns_from_1970, tt2000_entry_point, + static_cast& , int64_t* const)>( + cdf::to_ns_from_1970), + cdf::tt2000_t{63'072'000'000'000'000}, + cdf::tt2000_t{2'081'948'754'000'000'000} + ) + ->RangeMultiplier(4) + ->Range(10, mega(1024)) + ->Complexity() + ->UseRealTime(); + +BENCHMARK_CAPTURE(BM_to_ns_from_1970, epoch_scalar, + static_cast&, int64_t* const)>( + cdf::chrono::_impl::scalar_to_ns_from_1970), + cdf::epoch{62167215600000.0}, + cdf::epoch{63745052400000.0} + ) + ->RangeMultiplier(4) + ->Range(10, mega(1024)) + ->Complexity() + ->UseRealTime(); + +BENCHMARK_CAPTURE(BM_to_ns_from_1970, epoch_vectorized, + static_cast&, int64_t* const)>( + vectorized_to_ns_from_1970), + cdf::epoch{62167215600000.0}, + cdf::epoch{63745052400000.0} + ) + ->RangeMultiplier(4) + ->Range(10, mega(1024)) + ->Complexity() + ->UseRealTime(); + +BENCHMARK_CAPTURE(BM_to_ns_from_1970, epoch_entry_point, + static_cast&, int64_t* const)>( + cdf::to_ns_from_1970), + cdf::epoch{62167215600000.0}, + cdf::epoch{63745052400000.0} + ) + ->RangeMultiplier(4) + ->Range(10, mega(1024)) + ->Complexity() + ->UseRealTime(); + +BENCHMARK_MAIN(); diff --git a/benchmarks/meson.build b/benchmarks/meson.build new file mode 100644 index 0000000..8405d43 --- /dev/null +++ b/benchmarks/meson.build @@ -0,0 +1,8 @@ +google_benchmarks_dep = dependency('benchmark', required : true) +foreach bench:['file_reader', 'chrono'] + exe = executable('benchmark-'+bench, bench+'/main.cpp', + dependencies:[google_benchmarks_dep, cdfpp_dep], + install: false + ) + benchmark(bench, exe) +endforeach diff --git a/include/cdfpp/cdf-debug.hpp b/include/cdfpp/cdf-debug.hpp index 72c7cdb..bb79410 100644 --- a/include/cdfpp/cdf-debug.hpp +++ b/include/cdfpp/cdf-debug.hpp @@ -23,6 +23,12 @@ /*-- Author : Alexis Jeandet -- Mail : alexis.jeandet@member.fsf.org ----------------------------------------------------------------------------*/ +#include + +#include +#include +#include + #pragma once #ifdef CDFPP_ENABLE_ASSERT #include @@ -44,3 +50,15 @@ #define CDFPP_DIAGNOSTIC_DISABLE_DEPRECATED #define CDFPP_DIAGNOSTIC_POP #endif + +[[nodiscard]] constexpr auto exception_message(const auto& msg, + const std::source_location& location = std::source_location::current()) +{ + return fmt::format( + R"( +{} + at {}:{} in function {} + )", + msg, location.file_name(), location.line(), location.function_name()); +} + diff --git a/include/cdfpp/cdf-enums.hpp b/include/cdfpp/cdf-enums.hpp index f630914..24260b2 100644 --- a/include/cdfpp/cdf-enums.hpp +++ b/include/cdfpp/cdf-enums.hpp @@ -24,10 +24,14 @@ -- Mail : alexis.jeandet@member.fsf.org ----------------------------------------------------------------------------*/ #pragma once +#include +#include #include #include #include +#include "cdf-helpers.hpp" + namespace cdf { @@ -169,24 +173,54 @@ enum class CDF_Types : uint32_t CDF_UCHAR = 52 }; +consteval bool is_cdf_time_type(CDF_Types type) +{ + return helpers::is_in( + type, CDF_Types::CDF_EPOCH, CDF_Types::CDF_EPOCH16, CDF_Types::CDF_TIME_TT2000); +} + +consteval bool is_cdf_string_type(CDF_Types type) +{ + return helpers::is_in(type, CDF_Types::CDF_CHAR, CDF_Types::CDF_UCHAR); +} + +consteval bool is_cdf_integer_type(CDF_Types type) +{ + return helpers::is_in(type, CDF_Types::CDF_INT1, CDF_Types::CDF_INT2, CDF_Types::CDF_INT4, + CDF_Types::CDF_INT8, CDF_Types::CDF_UINT1, CDF_Types::CDF_UINT2, CDF_Types::CDF_UINT4, + CDF_Types::CDF_BYTE); +} + +consteval bool is_cdf_floating_point_type(CDF_Types type) +{ + return helpers::is_in(type, CDF_Types::CDF_REAL4, CDF_Types::CDF_REAL8, CDF_Types::CDF_FLOAT, + CDF_Types::CDF_DOUBLE); +} + // number of nanoseconds since 01-Jan-2000 12:00:00.000.000.000 (J2000) struct tt2000_t { - int64_t value; + int64_t nseconds; }; -bool operator==(const tt2000_t& lhs, const tt2000_t& rhs) + +static inline tt2000_t operator""_tt2k(unsigned long long __val) +{ + return tt2000_t { static_cast(__val) }; +} + +inline bool operator==(const tt2000_t& lhs, const tt2000_t& rhs) { - return lhs.value == rhs.value; + return lhs.nseconds == rhs.nseconds; } // number of milliseconds since 01-Jan-0000 00:00:00.000 struct epoch { - double value; + double mseconds; }; -bool operator==(const epoch& lhs, const epoch& rhs) +inline bool operator==(const epoch& lhs, const epoch& rhs) { - return lhs.value == rhs.value; + return lhs.mseconds == rhs.mseconds; } // number of picoseconds since 01-Jan-0000 00:00:00.000.000.000.000 @@ -195,11 +229,37 @@ struct epoch16 double seconds; double picoseconds; }; -bool operator==(const epoch16& lhs, const epoch16& rhs) +inline bool operator==(const epoch16& lhs, const epoch16& rhs) { return lhs.seconds == rhs.seconds && lhs.picoseconds == rhs.picoseconds; } +template +concept cdf_time_t = std::is_same_v || std::is_same_v + || std::is_same_v; + +template +concept cdf_time_t_span_t = std::is_same_v> + || std::is_same_v> || std::is_same_v> + || std::is_same_v> + || std::is_same_v> + || std::is_same_v>; + +template +concept time_point_t = requires(T t) { + typename T::clock; + typename T::duration; + { t.time_since_epoch() } -> std::convertible_to; +}; + +template +concept time_point_collection_t = requires(T t) { + typename T::value_type; + { std::size(t) } -> std::convertible_to; + { t.data() } -> std::convertible_to; + requires time_point_t; +}; + template constexpr auto from_cdf_type() { @@ -239,7 +299,7 @@ constexpr auto from_cdf_type() return epoch16 {}; } -std::size_t cdf_type_size(CDF_Types type) +[[nodiscard]] inline std::size_t cdf_type_size(CDF_Types type) { switch (type) { @@ -350,4 +410,67 @@ constexpr CDF_Types to_cdf_type() template using from_cdf_type_t = decltype(from_cdf_type()); + + +[[nodiscard]] inline auto cdf_type_dispatch(CDF_Types cdf_type, auto&& f, auto&&... args) +{ + switch (cdf_type) + { + case CDF_Types::CDF_UCHAR: + return std::forward(f).template operator()( + std::forward(args)...); + case CDF_Types::CDF_CHAR: + return std::forward(f).template operator()( + std::forward(args)...); + case CDF_Types::CDF_BYTE: + return std::forward(f).template operator()( + std::forward(args)...); + case CDF_Types::CDF_INT1: + return std::forward(f).template operator()( + std::forward(args)...); + case CDF_Types::CDF_UINT1: + return std::forward(f).template operator()( + std::forward(args)...); + case CDF_Types::CDF_INT2: + return std::forward(f).template operator()( + std::forward(args)...); + case CDF_Types::CDF_UINT2: + return std::forward(f).template operator()( + std::forward(args)...); + case CDF_Types::CDF_INT4: + return std::forward(f).template operator()( + std::forward(args)...); + case CDF_Types::CDF_UINT4: + return std::forward(f).template operator()( + std::forward(args)...); + case CDF_Types::CDF_INT8: + return std::forward(f).template operator()( + std::forward(args)...); + case CDF_Types::CDF_FLOAT: + return std::forward(f).template operator()( + std::forward(args)...); + case CDF_Types::CDF_REAL4: + return std::forward(f).template operator()( + std::forward(args)...); + case CDF_Types::CDF_DOUBLE: + return std::forward(f).template operator()( + std::forward(args)...); + case CDF_Types::CDF_REAL8: + return std::forward(f).template operator()( + std::forward(args)...); + case CDF_Types::CDF_EPOCH: + return std::forward(f).template operator()( + std::forward(args)...); + case CDF_Types::CDF_EPOCH16: + return std::forward(f).template operator()( + std::forward(args)...); + case CDF_Types::CDF_TIME_TT2000: + return std::forward(f).template operator()( + std::forward(args)...); + default: + throw std::runtime_error { std::string { "Unsupported CDF type " } + + std::to_string(static_cast(cdf_type)) }; + break; + } +} } diff --git a/include/cdfpp/cdf-helpers.hpp b/include/cdfpp/cdf-helpers.hpp index 93f4eb8..1c5fc6e 100644 --- a/include/cdfpp/cdf-helpers.hpp +++ b/include/cdfpp/cdf-helpers.hpp @@ -47,5 +47,29 @@ namespace helpers { return Visitor(lambdas...); } + + consteval bool is_in(const auto& value, const auto&... values) + { + return ((value == values) || ...); + } + + constexpr bool rt_is_in(const auto& value, const auto&... values) + { + return ((value == values) || ...); + } + + template + struct is_any_of : std::integral_constant || ...)>{}; + + template + using is_any_of_t = typename is_any_of::type; + + template + inline constexpr bool is_any_of_v = is_any_of::value; + + constexpr bool contains(const std::string& str, const auto& substr) + { + return str.find(substr) != std::string::npos; + } } } diff --git a/include/cdfpp/cdf-io/loading/buffers.hpp b/include/cdfpp/cdf-io/loading/buffers.hpp index 56900d3..5fac6af 100644 --- a/include/cdfpp/cdf-io/loading/buffers.hpp +++ b/include/cdfpp/cdf-io/loading/buffers.hpp @@ -51,26 +51,26 @@ namespace cdf::io::buffers { template -inline constexpr auto get_data_ptr(buffer_t& buffer) -> decltype(buffer.data()) +constexpr auto get_data_ptr(buffer_t& buffer) -> decltype(buffer.data()) { return buffer.data(); } template -inline constexpr auto get_data_ptr( +constexpr auto get_data_ptr( buffer_t& buffer, typename std::enable_if>::type* = 0) { return buffer; } template -inline constexpr auto get_data_ptr(buffer_t& buffer) -> decltype(get_data_ptr(buffer.buffer)) +constexpr auto get_data_ptr(buffer_t& buffer) -> decltype(get_data_ptr(buffer.buffer)) { return get_data_ptr(buffer.buffer); } template -inline constexpr auto get_data_ptr(buffer_t& buffer) -> decltype(buffer.view(0UL)) +constexpr auto get_data_ptr(buffer_t& buffer) -> decltype(buffer.view(0UL)) { return buffer.view(0UL); } diff --git a/include/cdfpp/cdf-io/saving/records-saving.hpp b/include/cdfpp/cdf-io/saving/records-saving.hpp index 3896da9..5a7359d 100644 --- a/include/cdfpp/cdf-io/saving/records-saving.hpp +++ b/include/cdfpp/cdf-io/saving/records-saving.hpp @@ -34,9 +34,6 @@ #include "cdfpp/cdf-helpers.hpp" #include "cdfpp/variable.hpp" #include -#include -#include -#include #include #include @@ -164,7 +161,7 @@ template } template -[[nodiscard]] constexpr inline std::size_t fields_size(const record_t& s, T&& field) +[[nodiscard]] constexpr std::size_t fields_size(const record_t& s, T&& field) { using Field_t = std::remove_cv_t>; constexpr std::size_t count = count_members; @@ -176,7 +173,7 @@ template } template -[[nodiscard]] constexpr inline std::size_t fields_size(const record_t& s, T&& field, Ts&&... fields) +[[nodiscard]] constexpr std::size_t fields_size(const record_t& s, T&& field, Ts&&... fields) { return fields_size(s, std::forward(field)) + fields_size(s, std::forward(fields)...); } diff --git a/include/cdfpp/cdf-repr.hpp b/include/cdfpp/cdf-repr.hpp index 98d50c9..e7d19be 100644 --- a/include/cdfpp/cdf-repr.hpp +++ b/include/cdfpp/cdf-repr.hpp @@ -24,15 +24,14 @@ -- Mail : alexis.jeandet@member.fsf.org ----------------------------------------------------------------------------*/ #pragma once -#include "cdfpp_config.h" #include #include #include #include #include #include +#include #include -#include #include using namespace cdf; @@ -78,17 +77,17 @@ inline stream_t& operator<<(stream_t& os, const decltype(cdf::to_time_point(tt20 template inline stream_t& operator<<(stream_t& os, const epoch& time) { - if (time.value == -1e31) + if (time.mseconds == -1e31) { os << "9999-12-31T23:59:59.999"; return os; } - if (time.value == 0) + if (time.mseconds == 0) { os << "0000-01-01T00:00:00.000"; return os; } - os << cdf::to_time_point(time); + os << fmt::format("{:%Y-%m-%dT%H:%M:%S}", cdf::to_time_point(time)); return os; } @@ -105,29 +104,29 @@ inline stream_t& operator<<(stream_t& os, const epoch16& time) os << "0000-01-01T00:00:00.000000000000"; return os; } - os << cdf::to_time_point(time); + os << fmt::format("{:%Y-%m-%dT%H:%M:%S}", cdf::to_time_point(time)); return os; } template inline stream_t& operator<<(stream_t& os, const tt2000_t& time) { - if (time.value == static_cast(0x8000000000000000)) + if (time.nseconds == static_cast(0x8000000000000000)) { os << "9999-12-31T23:59:59.999999999"; return os; } - if (time.value == static_cast(0x8000000000000001)) + if (time.nseconds == static_cast(0x8000000000000001)) { os << "0000-01-01T00:00:00.000000000"; return os; } - if (time.value == static_cast(0x8000000000000003)) + if (time.nseconds == static_cast(0x8000000000000003)) { os << "9999-12-31T23:59:59.999999999"; return os; } - os << cdf::to_time_point(time); + os << fmt::format("{:%Y-%m-%dT%H:%M:%S}", cdf::to_time_point(time)); return os; } @@ -176,64 +175,19 @@ inline stream_t& stream_string_like(stream_t& os, const input_t& input) template stream_t& operator<<(stream_t& os, const cdf::data_t& data) { - using namespace cdf; - switch (data.type()) - { - case CDF_Types::CDF_BYTE: - stream_collection(os, data.get(), ", "); - break; - case CDF_Types::CDF_INT1: - stream_collection(os, data.get(), ", "); - break; - case CDF_Types::CDF_UINT1: - stream_collection(os, data.get(), ", "); - break; - case CDF_Types::CDF_INT2: - stream_collection(os, data.get(), ", "); - break; - case CDF_Types::CDF_INT4: - stream_collection(os, data.get(), ", "); - break; - case CDF_Types::CDF_INT8: - stream_collection(os, data.get(), ", "); - break; - case CDF_Types::CDF_UINT2: - stream_collection(os, data.get(), ", "); - break; - case CDF_Types::CDF_UINT4: - stream_collection(os, data.get(), ", "); - break; - case CDF_Types::CDF_DOUBLE: - stream_collection(os, data.get(), ", "); - break; - case CDF_Types::CDF_REAL8: - stream_collection(os, data.get(), ", "); - break; - case CDF_Types::CDF_FLOAT: - stream_collection(os, data.get(), ", "); - break; - case CDF_Types::CDF_REAL4: - stream_collection(os, data.get(), ", "); - break; - case CDF_Types::CDF_EPOCH: - stream_collection(os, data.get(), ", "); - break; - case CDF_Types::CDF_EPOCH16: - stream_collection(os, data.get(), ", "); - break; - case CDF_Types::CDF_TIME_TT2000: - stream_collection(os, data.get(), ", "); - break; - case cdf::CDF_Types::CDF_UCHAR: - stream_string_like(os, data.get()); - break; - case cdf::CDF_Types::CDF_CHAR: - stream_string_like(os, data.get()); - break; - default: - break; - } - + cdf_type_dispatch(data.type(), + [](stream_t& os, const cdf::data_t& data) + { + if constexpr (is_cdf_string_type(T)) + { + stream_string_like(os, data.get()); + } + else + { + stream_collection(os, data.get(), ", "); + } + }, + os, data); return os; } diff --git a/include/cdfpp/chrono/cdf-chrono-impl.hpp b/include/cdfpp/chrono/cdf-chrono-impl.hpp new file mode 100644 index 0000000..27e81bc --- /dev/null +++ b/include/cdfpp/chrono/cdf-chrono-impl.hpp @@ -0,0 +1,185 @@ +/*------------------------------------------------------------------------------ +-- The MIT License (MIT) +-- +-- Copyright © 2026, Laboratory of Plasma Physics- CNRS +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy +-- of this software and associated documentation files (the “Software”), to deal +-- in the Software without restriction, including without limitation the rights +-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +-- INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +-- PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +-- HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +-- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +-- SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +-------------------------------------------------------------------------------*/ +/*-- Author : Alexis Jeandet +-- Mail : alexis.jeandet@member.fsf.org +----------------------------------------------------------------------------*/ +#pragma once + +#include "cdf-chrono-constants.hpp" +#include "cdf-leap-seconds.h" +#include "cdfpp/cdf-enums.hpp" +#include + +namespace cdf::chrono::_impl +{ +using namespace std::chrono; +using namespace cdf::chrono; + + +inline int64_t leap_second_branchless(int64_t ns_from_1970) +{ + const auto& table = leap_seconds::leap_seconds_tt2000; + int64_t offset = 0; + for (size_t i = 0; i < table.size(); ++i) + { + offset = (ns_from_1970 >= table[i].first) ? table[i].second : offset; + } + return offset; +} + +inline int64_t leap_second(int64_t ns_from_1970) +{ + if (ns_from_1970 > leap_seconds::leap_seconds_tt2000.front().first) + { + if (ns_from_1970 < leap_seconds::leap_seconds_tt2000.back().first) + { + auto lc = std::cbegin(leap_seconds::leap_seconds_tt2000); + while (ns_from_1970 >= (lc + 1)->first) + { + lc++; + } + return lc->second; + } + else + { + return leap_seconds::leap_seconds_tt2000.back().second; + } + } + return 0; +} + +inline int64_t leap_second(const tt2000_t& ep) +{ + if (ep.nseconds > leap_seconds::leap_seconds_tt2000_reverse.front().first) + { + if (ep.nseconds < leap_seconds::leap_seconds_tt2000_reverse.back().first) + { + auto lc = std::cbegin(leap_seconds::leap_seconds_tt2000_reverse); + while (ep.nseconds >= (lc + 1)->first) + { + lc++; + } + return lc->second; + } + else + { + return leap_seconds::leap_seconds_tt2000_reverse.back().second; + } + } + return 0; +} + +inline auto _leap_second(const tt2000_t& ep, std::size_t leap_index_hint) +{ + if (ep.nseconds >= leap_seconds::leap_seconds_tt2000_reverse[leap_index_hint].first) + { + while (leap_index_hint + 1 < leap_seconds::leap_seconds_tt2000_reverse.size() + && ep.nseconds >= leap_seconds::leap_seconds_tt2000_reverse[leap_index_hint + 1].first) + { + ++leap_index_hint; + } + return std::tuple { leap_seconds::leap_seconds_tt2000_reverse[leap_index_hint].second, + leap_index_hint }; + } + else + { + while (leap_index_hint > 0 + && ep.nseconds < leap_seconds::leap_seconds_tt2000_reverse[leap_index_hint].first) + { + --leap_index_hint; + } + return std::tuple { leap_seconds::leap_seconds_tt2000_reverse[leap_index_hint].second, + leap_index_hint }; + } +} + + +inline void _unsorted_to_ns_from_1970( + const tt2000_t* const input, const std::size_t count, int64_t* const output) +{ + if (count) + { + std::size_t last_index = 0; + for (std::size_t i = 0; i < count; ++i) + { + auto [ls, idx] = _leap_second(input[i], last_index); + output[i] = input[i].nseconds - ls + constants::tt2000_offset; + last_index = idx; + } + } +} + +inline void _optimistic_to_ns_from_1970_after_2017( + const tt2000_t* const input, const std::size_t count, int64_t* const output) +{ + const auto last_leap_sec = leap_seconds::leap_seconds_tt2000_reverse.back().first; + const auto offset + = constants::tt2000_offset - leap_seconds::leap_seconds_tt2000_reverse.back().second; + bool all_after_2017 = true; + for (std::size_t i = 0; i < count; ++i) + { + output[i] = input[i].nseconds + offset; + all_after_2017 &= (input[i].nseconds >= last_leap_sec); + } + if (!all_after_2017) + { + _impl::_unsorted_to_ns_from_1970(input, count, output); + } +} + +inline void scalar_to_ns_from_1970( + const std::span& input, int64_t* const output) +{ + if (input.size() == 0) + { + return; + } + if (input[0].nseconds >= leap_seconds::leap_seconds_tt2000_reverse.back().first) + { + return _impl::_optimistic_to_ns_from_1970_after_2017(input.data(), input.size(), output); + } + return _impl::_unsorted_to_ns_from_1970(input.data(), input.size(), output); +} + +inline void scalar_to_ns_from_1970( + const std::span& input, int64_t* const output) +{ + for (std::size_t i = 0; i < input.size(); ++i) + { + output[i] = (input[i].mseconds - constants::epoch_offset_miliseconds) * 1'000'000; + } +} + +inline void scalar_to_ns_from_1970( + const std::span& input, int64_t* const output) +{ + + for (std::size_t i = 0; i < input.size(); ++i) + { + output[i] + = (input[i].seconds - constants::epoch_offset_seconds) * 1'000'000'000 + + (input[i].picoseconds / 1'000); + } +} + +} diff --git a/include/cdfpp/chrono/cdf-chrono.hpp b/include/cdfpp/chrono/cdf-chrono.hpp index a1f32a5..a5b862e 100644 --- a/include/cdfpp/chrono/cdf-chrono.hpp +++ b/include/cdfpp/chrono/cdf-chrono.hpp @@ -24,85 +24,124 @@ -- Mail : alexis.jeandet@member.fsf.org ----------------------------------------------------------------------------*/ #pragma once + #include "cdf-chrono-constants.hpp" -#include "cdf-leap-seconds.h" +#include "cdf-chrono-impl.hpp" #include "cdfpp/cdf-debug.hpp" #include "cdfpp/cdf-enums.hpp" #include "cdfpp/no_init_vector.hpp" +#include + #include #include #include #include +#include +#include + +#ifndef CDFPP_NO_SIMD +#include "cdfpp/vectorized/cdf-chrono.hpp" +#endif + + +using namespace std::chrono; namespace cdf { -using namespace std::chrono; + using namespace cdf::chrono; -inline int64_t leap_second(int64_t ns_from_1970) +namespace chrono::_impl { - if (ns_from_1970 > leap_seconds::leap_seconds_tt2000.front().first) + static inline std::size_t ideal_threads_count() + { + static const auto threads_count = std::thread::hardware_concurrency() >= 32 ? 8U : 2U; + return threads_count; + } + + /* the takeaway is that threading is worth it on platforms with many cores where we infer + * big memory bandwidht because of many memry channels. + * On a typical 4 core laptop with 8 threads, threading is not worth it because we already + * top reach ~70% of memory bandwidth with a single thread. + */ + template + static inline void _thread_if_needed( + const auto& input, auto* const output, const auto& function) { - if (ns_from_1970 < leap_seconds::leap_seconds_tt2000.back().first) + static const auto threads_count = ideal_threads_count(); + const auto count = std::size(input); + if ((count >= (min_chunk_size * threads_count))) { - auto lc = std::cbegin(leap_seconds::leap_seconds_tt2000); - while (ns_from_1970 >= (lc + 1)->first) + const auto chunk_size = [count]() + { + auto cs = (count / threads_count); + while ((cs * threads_count) < count) + { + cs += min_chunk_size; + } + return cs; + }(); + std::vector threads; + threads.reserve(threads_count); + std::size_t start = 0; + std::size_t sz = chunk_size; + for (std::size_t i = 0; i < threads_count; ++i) + { + threads.emplace_back([input, start, sz, output, &function]() + { function(input.subspan(start, sz), output + start); }); + start += sz; + sz = ((start + chunk_size) > count) ? count - start : chunk_size; + } + for (auto& t : threads) { - lc++; + t.join(); } - return lc->second; } else { - return leap_seconds::leap_seconds_tt2000.back().second; + function(input, output); } } - return 0; + } -inline int64_t leap_second(const tt2000_t& ep) +static inline void to_ns_from_1970(const cdf_time_t_span_t auto& input, int64_t* output) { - if (ep.value > leap_seconds::leap_seconds_tt2000_reverse.front().first) - { - if (ep.value < leap_seconds::leap_seconds_tt2000_reverse.back().first) + chrono::_impl::_thread_if_needed<1 * 1024 * 1024>(input, output, + [](const cdf_time_t_span_t auto& input, int64_t* output) { - auto lc = std::cbegin(leap_seconds::leap_seconds_tt2000_reverse); - while (ep.value >= (lc + 1)->first) +#ifndef CDFPP_NO_SIMD + if (input.size() >= 8) { - lc++; + vectorized_to_ns_from_1970(input, output); } - return lc->second; - } - else - { - return leap_seconds::leap_seconds_tt2000_reverse.back().second; - } - } - return 0; + else + { + _impl::scalar_to_ns_from_1970(input, output); + } +#else + _impl::scalar_to_ns_from_1970(input, output); +#endif + }); } -template -epoch to_epoch(const std::chrono::time_point& tp) +epoch to_epoch(const time_point_t auto& tp) { using namespace std::chrono; return epoch { duration_cast(tp.time_since_epoch()).count() + constants::epoch_offset_miliseconds }; } -template -no_init_vector to_epoch(const no_init_vector>& tps) +no_init_vector to_epoch(const auto& tps) { - using namespace std::chrono; no_init_vector result(std::size(tps)); std::transform(std::cbegin(tps), std::cend(tps), std::begin(result), - [](const std::chrono::time_point& v) { return to_epoch(v); }); + static_cast(to_epoch)); return result; } -template -epoch16 to_epoch16(const std::chrono::time_point& tp) +epoch16 to_epoch16(const time_point_t auto& tp) { - using namespace std::chrono; auto se = static_cast(duration_cast(tp.time_since_epoch()).count()); auto s = se + constants::epoch_offset_seconds; auto ps = (static_cast(duration_cast(tp.time_since_epoch()).count()) @@ -111,38 +150,33 @@ epoch16 to_epoch16(const std::chrono::time_point& tp) return epoch16 { s, ps }; } -template -no_init_vector to_epoch16( - const no_init_vector>& tps) + +no_init_vector to_epoch16(const time_point_collection_t auto& tps) { - using namespace std::chrono; no_init_vector result(std::size(tps)); std::transform(std::cbegin(tps), std::cend(tps), std::begin(result), - [](const std::chrono::time_point& v) { return to_epoch16(v); }); + static_cast(to_epoch16)); return result; } -template -tt2000_t to_tt2000(const std::chrono::time_point& tp) +tt2000_t to_tt2000(const time_point_t auto& tp) { using namespace std::chrono; auto nsec = duration_cast(tp.time_since_epoch()).count(); - return tt2000_t { nsec - constants::tt2000_offset + leap_second(nsec) }; + return tt2000_t { nsec - constants::tt2000_offset + _impl::leap_second(nsec) }; } -template -no_init_vector to_tt2000( - const no_init_vector>& tps) +no_init_vector to_tt2000(const time_point_collection_t auto& tps) { using namespace std::chrono; no_init_vector result(std::size(tps)); std::transform(std::cbegin(tps), std::cend(tps), std::begin(result), - [](const std::chrono::time_point& v) { return to_tt2000(v); }); + static_cast(to_tt2000)); return result; } -template -T to_cdf_time(const std::chrono::time_point& tp) +template +T to_cdf_time(const time_point_t auto& tp) { if constexpr (std::is_same_v) return to_tt2000(tp); @@ -150,11 +184,33 @@ T to_cdf_time(const std::chrono::time_point& tp) return to_epoch(tp); else if constexpr (std::is_same_v) return to_epoch16(tp); + else + throw std::runtime_error("Unsupported cdf time type"); +} + +template +T to_cdf_time(const cdf_time_t auto& in) +{ + using input_t = std::decay_t; + if constexpr (std::is_same_v) + return in; + else + return to_cdf_time(to_time_point(in)); +} + +static inline void from_ns_from_1970(const std::span& input, cdf_time_t auto* output) +{ + std::transform(std::cbegin(input), std::cend(input), output, + [](const int64_t ns) + { + return to_cdf_time>( + std::chrono::system_clock::time_point {} + std::chrono::nanoseconds(ns)); + }); } inline auto to_time_point(const epoch& ep) { - double ms = ep.value - constants::epoch_offset_miliseconds, ns; + double ms = ep.mseconds - constants::epoch_offset_miliseconds, ns; ns = std::modf(ms, &ms) * 1000000.; return std::chrono::time_point {} + milliseconds(int64_t(ms)) + nanoseconds(int64_t(ns)); @@ -168,13 +224,11 @@ inline auto to_time_point(const epoch16& ep) + nanoseconds(static_cast(ns)); } - inline auto to_time_point(const tt2000_t& ep) { using namespace std::chrono; return time_point {} - + nanoseconds(ep.value - leap_second(ep) + constants::tt2000_offset); + + nanoseconds(ep.nseconds - _impl::leap_second(ep) + constants::tt2000_offset); } - } diff --git a/include/cdfpp/chrono/cdf-leap-seconds.h b/include/cdfpp/chrono/cdf-leap-seconds.h index 6a1f0c2..bddb6d2 100644 --- a/include/cdfpp/chrono/cdf-leap-seconds.h +++ b/include/cdfpp/chrono/cdf-leap-seconds.h @@ -91,6 +91,8 @@ constexpr std::array leap_seconds_tt2000 = { // ('1', 'Jan', '2017') std::pair { 1483228800000000000, 37000000000 }, }; + + constexpr std::array leap_seconds_tt2000_reverse = { // ('1', 'Jan', '1972') std::pair { -883655957816000000, 10000000000 }, diff --git a/include/cdfpp/no_init_vector.hpp b/include/cdfpp/no_init_vector.hpp index 6d7f234..2518fa2 100644 --- a/include/cdfpp/no_init_vector.hpp +++ b/include/cdfpp/no_init_vector.hpp @@ -82,6 +82,8 @@ class default_init_allocator : public A #ifdef MADV_HUGEPAGE ::madvise(mem, pCount * sizeof(T), MADV_HUGEPAGE); #endif + // tell the kernel to start populating the pages + ::madvise(mem, pCount * sizeof(T), MADV_WILLNEED); } else { @@ -91,11 +93,19 @@ class default_init_allocator : public A return reinterpret_cast(mem); } +#if __cplusplus < 202002L template T* allocate(std::size_t pCount, const T* ptr = 0, typename std::enable_if::type* = 0) { return A::allocate(pCount, ptr); } +#endif + + template + T* allocate(std::size_t pCount, typename std::enable_if::type* = 0) + { + return A::allocate(pCount); + } template void deallocate(T* ptr, std::size_t, typename std::enable_if::type* = 0) noexcept( diff --git a/include/cdfpp/variable.hpp b/include/cdfpp/variable.hpp index 5ead481..74c9ac1 100644 --- a/include/cdfpp/variable.hpp +++ b/include/cdfpp/variable.hpp @@ -26,6 +26,7 @@ #pragma once #include "attribute.hpp" #include "cdf-data.hpp" +#include "cdf-debug.hpp" #include "cdf-enums.hpp" #include "cdf-io/majority-swap.hpp" #include "cdf-map.hpp" @@ -35,8 +36,13 @@ #include #include #include +#include #include +#include +#include +#include + template inline stream_t& operator<<( stream_t& os, const cdf_map& attributes) @@ -69,6 +75,12 @@ template return 0UL; } +/* + * Before version 1.0 it would make sense to consider exposing a view to data instead of + * a vector. That would allow zero copy from and to any user defined data structure + * (when layout is compatible). + * For example, we could build a view on top of an existing numpy array without copy. + */ struct Variable { using var_data_t = data_t; @@ -164,6 +176,7 @@ struct Variable check_shape(); } + void set_data(const data_t& data, const shape_t& shape) { p_data = data; @@ -171,6 +184,7 @@ struct Variable check_shape(); } + void set_data(data_t&& data, shape_t&& shape) { p_data = std::move(data); @@ -178,6 +192,13 @@ struct Variable check_shape(); } + void set_data(std::pair&& data) + { + p_data = std::move(data.first); + p_shape = std::move(data.second); + check_shape(); + } + [[nodiscard]] std::size_t bytes() const noexcept { if (std::size(p_shape)) @@ -272,7 +293,14 @@ struct Variable and not(is_nrv() and _data().size() == 0UL and (_data().type() == CDF_Types::CDF_CHAR or _data().type() == CDF_Types::CDF_UCHAR))) - throw std::invalid_argument { "Variable: given shape and data size doens't match" }; + throw std::invalid_argument { exception_message(fmt::format(R"( +Variable: given shape and data size doesn't match: +Variable name: "{}" + Shape: {} , size {} +Data: + size: {} +)", + p_name, p_shape, flat_size(p_shape), _data().size())) }; } std::string p_name; diff --git a/include/cdfpp/vectorized/cdf-chrono-impl.hpp b/include/cdfpp/vectorized/cdf-chrono-impl.hpp new file mode 100644 index 0000000..fe89270 --- /dev/null +++ b/include/cdfpp/vectorized/cdf-chrono-impl.hpp @@ -0,0 +1,420 @@ +/*------------------------------------------------------------------------------ +-- The MIT License (MIT) +-- +-- Copyright © 2025, Laboratory of Plasma Physics- CNRS +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy +-- of this software and associated documentation files (the “Software”), to deal +-- in the Software without restriction, including without limitation the rights +-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +-- INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +-- PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +-- HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +-- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +-- SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +-------------------------------------------------------------------------------*/ +/*-- Author : Alexis Jeandet +-- Mail : alexis.jeandet@member.fsf.org +----------------------------------------------------------------------------*/ +#pragma once +#include "cdfpp/cdf-enums.hpp" +#include "cdfpp/cdf-helpers.hpp" +#include "cdfpp/chrono/cdf-chrono-constants.hpp" +#include "cdfpp/chrono/cdf-chrono-impl.hpp" +#include "cdfpp/chrono/cdf-leap-seconds.h" +#include +#include + + +namespace cdf::chrono::vectorized +{ +using namespace cdf::chrono; + +template +void stream_store(const auto& batch_data, auto* const output) +{ + if constexpr (std::is_same_v) + { + return batch_data.store_unaligned(output); + } + if constexpr (std::is_base_of_v) + { +#if defined(__AVX512F__) + _mm512_stream_si512(reinterpret_cast<__m512i*>(output), batch_data); +#endif + } + else if constexpr (std::is_base_of_v) + { +#if defined(__AVX2__) + _mm256_stream_si256(reinterpret_cast<__m256i*>(output), batch_data); +#endif + } + else + xsimd::store_aligned(output, batch_data); +} + +template +void store(const auto& batch_data, auto* output) +{ + if constexpr (std::is_same_v) + batch_data.store_unaligned(output); + else + batch_data.store_aligned(output); +} + +template +void sfence() +{ + if constexpr (std::is_same_v || std::is_same_v) + { +#if defined(__AVX__) || defined(__AVX512F__) + _mm_sfence(); +#endif + } +} + + +template +static inline std::tuple,int64_t*> _realign(Arch, const std::span& input, + int64_t* const output, const auto& scalar_function) +{ + const auto count = std::size(input); + const auto input_offset = (reinterpret_cast(input.data()) / sizeof(tt2000_t)) + % (Arch::alignment() / sizeof(tt2000_t)); + const auto output_offset = (reinterpret_cast(output) / sizeof(tt2000_t)) + % (Arch::alignment() / sizeof(int64_t)); + if ((input_offset == output_offset) && (input_offset < count)) + { + const auto elements_to_align = (Arch::alignment() / sizeof(tt2000_t)) - input_offset; + scalar_function(input.subspan(0, elements_to_align), output); + return {input.subspan(elements_to_align), output + elements_to_align}; + } + return {input, output}; +} + +template +constexpr auto _make_indexes(std::index_sequence) +{ + return xsimd::batch((2 * I + offset)...); +} + +template ::size>> +constexpr auto make_even_indexes() +{ + return _make_indexes(Is {}); +} + +template ::size>> +constexpr auto make_odd_indexes() +{ + return _make_indexes(Is {}); +} + + +struct _to_ns_from_1970_epoch16_t +{ + template + static inline void to_ns_from_1970( + Arch, const std::span& input, int64_t* const output) + { + using batchout_type = xsimd::batch; + using batchin_type = xsimd::batch; + constexpr std::size_t simd_size = batchout_type::size; + const auto count = std::size(input); + const auto even_indexes = make_even_indexes(); + const auto odd_indexes = make_odd_indexes(); + + const auto offset = xsimd::broadcast(constants::epoch_offset_seconds); + const auto ps_in_ns = xsimd::broadcast(1'000); + const auto ns_in_s = xsimd::broadcast(1'000'000'000); + std::size_t i = 0; + for (; i + simd_size <= count; i += simd_size) + { + auto seconds + = batchin_type::gather(reinterpret_cast(&input[i]), even_indexes); + auto picos + = batchin_type::gather(reinterpret_cast(&input[i]), odd_indexes); + xsimd::batch_cast(((seconds - offset) * ns_in_s + (picos / ps_in_ns))) + .store(output + i, output_align_mode {}); + } + if (i < count) + { + _impl::scalar_to_ns_from_1970(input.subspan(i), output + i); + } + } + + template + void operator()( + Arch, const std::span& input, int64_t* const output); +}; + +template +void _to_ns_from_1970_epoch16_t::operator()( + Arch, const std::span& input, int64_t* const output) +{ + if constexpr (cdf::helpers::is_any_of_v) + { + return _impl::scalar_to_ns_from_1970(input, output); + } + if (xsimd::is_aligned(input.data()) && xsimd::is_aligned(output)) + { + to_ns_from_1970( + Arch {}, input, output); + } + else if (xsimd::is_aligned(input.data())) + { + to_ns_from_1970( + Arch {}, input, output); + } + else if (xsimd::is_aligned(output)) + { + to_ns_from_1970( + Arch {}, input, output); + } + else + { + to_ns_from_1970( + Arch {}, input, output); + } +} + +struct _to_ns_from_1970_epoch_t +{ + + template + static inline void to_ns_from_1970( + Arch, const std::span& input, int64_t* const output) + { + using batchout_type = xsimd::batch; + using batchin_type = xsimd::batch; + const auto count = std::size(input); + constexpr std::size_t simd_size = batchout_type::size; + const auto offset = xsimd::broadcast(constants::epoch_offset_miliseconds); + const auto ns_in_ms = xsimd::broadcast(1'000'000); + std::size_t i = 0; + for (; i + simd_size <= count; i += simd_size) + { + auto epoch_batch = batchin_type::load(&input[i].mseconds, input_align_mode {}); + store( + xsimd::batch_cast((epoch_batch - offset) * ns_in_ms), output + i); + } + if (i < count) + { + _impl::scalar_to_ns_from_1970(input.subspan(i), output + i); + } + } + + template + void operator()(Arch, const std::span& input, int64_t* const output); +}; + +template +void _to_ns_from_1970_epoch_t::operator()( + Arch, const std::span& input, int64_t* const output) +{ + if constexpr (cdf::helpers::is_any_of_v) + { + return _impl::scalar_to_ns_from_1970(input, output); + } + if (xsimd::is_aligned(input.data()) && xsimd::is_aligned(output)) + { + to_ns_from_1970( + Arch {}, input, output); + } + else if (xsimd::is_aligned(input.data())) + { + to_ns_from_1970( + Arch {}, input, output); + } + else if (xsimd::is_aligned(output)) + { + to_ns_from_1970( + Arch {}, input, output); + } + else + { + to_ns_from_1970( + Arch {}, input, output); + } +} + +struct _to_ns_from_1970_tt2000_t +{ + + template + static inline void _optimistic_after_2017( + Arch, const std::span& input, int64_t* const output) + { + using batch_type = xsimd::batch; + const auto count = std::size(input); + constexpr std::size_t simd_size = batch_type::size; + std::size_t i = 0; + const auto offset = xsimd::broadcast( + constants::tt2000_offset - leap_seconds::leap_seconds_tt2000_reverse.back().second); + const auto last_leap_sec = xsimd::broadcast( + leap_seconds::leap_seconds_tt2000_reverse.back().first); + auto was_after_2017 = xsimd::batch_bool(true); + for (; i + simd_size <= count; i += simd_size) + { + auto tt2000_batch = batch_type::load(&input[i].nseconds, input_align_mode {}); + store(tt2000_batch + offset, &output[i]); + was_after_2017 = was_after_2017 & (tt2000_batch >= last_leap_sec); + } + // sfence(); + if (!xsimd::all(was_after_2017)) + { + return _unsorted( + Arch {}, input, output); + } + if (i < count) + { + _impl::scalar_to_ns_from_1970(input.subspan(i), &output[i]); + } + } + + template + static inline void _unsorted( + Arch, const std::span& input, int64_t* const output) + { + using batch_type = xsimd::batch; + constexpr std::size_t simd_size = batch_type::size; + std::size_t i = 0; + const auto count = std::size(input); + constexpr auto max_leap_offset = leap_seconds::leap_seconds_tt2000_reverse.back().second; + const auto last_leap_sec = xsimd::broadcast( + leap_seconds::leap_seconds_tt2000_reverse.back().first); + const auto one_sec = xsimd::broadcast(1000000000LL); + + + for (; i + simd_size <= count; i += simd_size) + { + auto tt2000_batch = batch_type::load(&input[i].nseconds, input_align_mode {}); + auto offset + = xsimd::broadcast(constants::tt2000_offset - max_leap_offset); + int leap_index = std::size(leap_seconds::leap_seconds_tt2000_reverse) - 2; + auto needs_correction = (tt2000_batch < last_leap_sec); + if (!xsimd::any(needs_correction)) + { + store(tt2000_batch + offset, &output[i]); + continue; + } + while (xsimd::any(needs_correction) && (leap_index != -1)) + { + offset = xsimd::select(needs_correction, offset + one_sec, offset); + needs_correction + = (tt2000_batch < xsimd::broadcast( + leap_seconds::leap_seconds_tt2000_reverse[leap_index].first)); + leap_index--; + } + store(tt2000_batch + offset, &output[i]); + } + // sfence(); + if (i < count) + { + _impl::scalar_to_ns_from_1970(input.subspan(i), &output[i]); + } + } + + template + static inline void to_ns_from_1970( + Arch, const std::span& input, int64_t* const output) + { + /* We asume that the input is almost always sorted and recent (after 2017) + * so we first check if the first value is after the last leap second + * if yes, we can use a faster algorithm (just a constant offset). + * This optimistic still keeps track of the fact that some values may be before + * the last leap second, in which case we fallback to the unsorted algorithm. + */ + if (input[0].nseconds >= leap_seconds::leap_seconds_tt2000_reverse.back().first) + { + return _optimistic_after_2017( + Arch {}, input, output); + } + return _unsorted(Arch {}, input, output); + } + + template + void operator()( + Arch, const std::span& input, int64_t* const output); +}; + +template +void _to_ns_from_1970_tt2000_t::operator()( + Arch, const std::span& input, int64_t* const output) +{ + // fallback to scalar implementation where it's not worth vectorizing + // in this particular case, we manipulate 64-bit integers and to avoid + // branching in the vectorized code, we process more leap seconds than + // the scalar code, we need at least 512 bits SIMD registers to make it + // worth it. + if constexpr (cdf::helpers::is_any_of_v) + { + return _impl::scalar_to_ns_from_1970(input, output); + } + { + if (xsimd::is_aligned(input.data()) && xsimd::is_aligned(output)) + { + to_ns_from_1970( + Arch {}, input, output); + } + else if (xsimd::is_aligned(input.data())) + { + to_ns_from_1970( + Arch {}, input, output); + } + else if (xsimd::is_aligned(output)) + { + to_ns_from_1970( + Arch {}, input, output); + } + else + { + auto [ new_in, new_out ] = _realign(Arch {}, input, output, + static_cast&, int64_t* const)>( + _impl::scalar_to_ns_from_1970)); + if (new_out != output) + { + to_ns_from_1970( + Arch {}, new_in, new_out); + } + else + { + to_ns_from_1970( + Arch {}, new_in, new_out); + } + } + } +} + +#ifdef CDFPP_ENABLE_SSE2_ARCH +extern template void _to_ns_from_1970_tt2000_t::operator()( + xsimd::sse2, const std::span& input, int64_t* const output); +extern template void _to_ns_from_1970_epoch_t::operator()( + xsimd::sse2, const std::span& input, int64_t* const output); +extern template void _to_ns_from_1970_epoch16_t::operator()( + xsimd::sse2, const std::span& input, int64_t* const output); +#endif +#ifdef CDFPP_ENABLE_AVX2_ARCH +extern template void _to_ns_from_1970_tt2000_t::operator()( + xsimd::avx2, const std::span& input, int64_t* const output); +extern template void _to_ns_from_1970_epoch_t::operator()( + xsimd::avx2, const std::span& input, int64_t* const output); +extern template void _to_ns_from_1970_epoch16_t::operator()( + xsimd::avx2, const std::span& input, int64_t* const output); +#endif +#ifdef CDFPP_ENABLE_AVX512BW_ARCH +extern template void _to_ns_from_1970_tt2000_t::operator()( + xsimd::avx512bw, const std::span& input, int64_t* const output); +extern template void _to_ns_from_1970_epoch_t::operator()( + xsimd::avx512bw, const std::span& input, int64_t* const output); +extern template void _to_ns_from_1970_epoch16_t::operator()( + xsimd::avx512bw, const std::span& input, int64_t* const output); +#endif +} diff --git a/include/cdfpp/vectorized/cdf-chrono.hpp b/include/cdfpp/vectorized/cdf-chrono.hpp new file mode 100644 index 0000000..6b46b0f --- /dev/null +++ b/include/cdfpp/vectorized/cdf-chrono.hpp @@ -0,0 +1,36 @@ +/*------------------------------------------------------------------------------ +-- The MIT License (MIT) +-- +-- Copyright © 2025, Laboratory of Plasma Physics- CNRS +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy +-- of this software and associated documentation files (the “Software”), to deal +-- in the Software without restriction, including without limitation the rights +-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +-- INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +-- PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +-- HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +-- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +-- SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +-------------------------------------------------------------------------------*/ +/*-- Author : Alexis Jeandet +-- Mail : alexis.jeandet@member.fsf.org +----------------------------------------------------------------------------*/ +#pragma once +#include "cdfpp/cdf-enums.hpp" + +extern void vectorized_to_ns_from_1970( + const std::span& input, int64_t* const output); + +extern void vectorized_to_ns_from_1970( + const std::span& input, int64_t* const output); + +extern void vectorized_to_ns_from_1970( + const std::span& input, int64_t* const output); diff --git a/meson.build b/meson.build index ae837aa..757afcf 100644 --- a/meson.build +++ b/meson.build @@ -6,7 +6,7 @@ project( capture:true, env:{'SRC_ROOT':meson.project_source_root()} ).stdout().strip() , - default_options : ['warning_level=3', 'cpp_std=c++17', 'default_library=static', 'buildtype=release'], + default_options : ['warning_level=3', 'cpp_std=c++20', 'default_library=static', 'buildtype=release'], license : 'GPL3' ) @@ -42,12 +42,17 @@ configure_file(output : 'cdfpp_config.h', cpp = meson.get_compiler('cpp') if('clang'==cpp.get_id()) add_global_arguments('-fsized-deallocation', language : 'cpp') + add_global_arguments('-fvisibility=hidden', language : 'cpp') +endif +if('gcc'==cpp.get_id()) + add_global_arguments('-fvisibility=hidden', language : 'cpp') endif pybind11_dep = dependency('pybind11') hedley_dep = dependency('hedley') fmt_dep = dependency('fmt') +xsimd_dep = dependency('xsimd') if build_machine.system() == 'windows' link_args = ['-static-libstdc++','-static-libgcc','-static'] @@ -119,6 +124,8 @@ cdfpp_headers = files( 'include/cdfpp/cdf-io/saving/link_records.hpp' ) +subdir('simd') + if get_option('with_experimental_zstd') add_project_arguments('-DCDFPP_USE_ZSTD', language : ['cpp']) zstd_dep = dependency('libzstd') @@ -132,7 +139,7 @@ endif pycdfpp_headers = files( 'pycdfpp/chrono.hpp', - 'pycdfpp/buffers.hpp', + 'pycdfpp/collections.hpp', 'pycdfpp/cdf.hpp', 'pycdfpp/attribute.hpp', 'pycdfpp/variable.hpp', @@ -151,7 +158,8 @@ cdfpp_extra_files = files( 'tests/python_saving/test.py', 'tests/python_variable_set_values/test.py', 'tests/python_skeletons/test.py', - 'tests/full_corpus/test_full_corpus.py', + 'tests/python_chrono/test.py', + 'tests/full_corpus/test.py', '.github/workflows/CI.yml', '.github/workflows/tests-with-coverage.yml', '.github/dependabot.yml' @@ -175,7 +183,7 @@ cdfpp_dep_inc = include_directories('include', '.') cdfpp_dep = declare_dependency(include_directories: cdfpp_dep_inc, - dependencies: [zlib_dep, hedley_dep, fmt_dep, zstd_dep], + dependencies: [zlib_dep, hedley_dep, fmt_dep, zstd_dep] + [simd_deps], link_args : link_args, compile_args : compile_args) @@ -251,87 +259,33 @@ if get_option('with_experimental_wasm') endif if get_option('with_tests') - - configure_file(output : 'tests_config.hpp', - configuration : { - 'DATA_PATH' : '"' + meson.current_source_dir() / 'tests/resources' + '"' - } - ) - - catch_dep = dependency('catch2-with-main', version:'>3.0.0', required : true) - - - foreach test:['endianness','simple_open', 'majority', 'chrono', 'nomap', 'records_loading', 'records_saving', - 'rle_compression', 'libdeflate_compression', 'zlib_compression', 'simple_save', 'zstd_compression'] - exe = executable('test-'+test,'tests/'+test+'/main.cpp', - dependencies:[catch_dep, cdfpp_dep], - install: false - ) - test(test, exe) - endforeach - - test('python_loading_test', python3, - args:[meson.current_source_dir()+'/tests/python_loading/test.py'], - env:['PYTHONPATH='+meson.current_build_dir()], - workdir:meson.current_build_dir()) - - test('python_saving_test', python3, - args:[meson.current_source_dir()+'/tests/python_saving/test.py'], - env:['PYTHONPATH='+meson.current_build_dir()], - workdir:meson.current_build_dir()) - - test('python_skeletons_test', python3, - args:[meson.current_source_dir()+'/tests/python_skeletons/test.py'], - env:['PYTHONPATH='+meson.current_build_dir()], - workdir:meson.current_build_dir()) - - test('python_variable_set_values_test', python3, - args:[meson.current_source_dir()+'/tests/python_variable_set_values/test.py'], - env:['PYTHONPATH='+meson.current_build_dir()], - workdir:meson.current_build_dir()) - - test('full_corpus_test', python3, - args:[meson.current_source_dir()+'/tests/full_corpus/test_full_corpus.py'], - env:['PYTHONPATH='+meson.current_build_dir()], - timeout: 300, - workdir:meson.current_build_dir()) - - python_wrapper_cpp = executable('python_wrapper_cpp','tests/python_wrapper_cpp/main.cpp', - dependencies:[pybind11_dep, python3.dependency(embed:true)], - install: false - ) - - manual_load = executable('manual_load','tests/manual_load/main.cpp', - dependencies:[cdfpp_dep], - install: false - ) - - manual_load = executable('manual_save','tests/manual_save/main.cpp', - dependencies:[cdfpp_dep], - install: false - ) - - - foreach example:['basic_cpp'] - exe = executable('example-'+example,'examples/'+example+'/main.cpp', - dependencies:[cdfpp_dep], - cpp_args: ['-DDATA_PATH="@0@/tests/resources"'.format(meson.current_source_dir())], - install: false - ) - endforeach + subdir('tests') endif if get_option('with_benchmarks') - google_benchmarks_dep = dependency('benchmark', required : true) - foreach bench:['file_reader'] - exe = executable('benchmark-'+bench,'benchmarks/'+bench+'/main.cpp', - dependencies:[google_benchmarks_dep, cdfpp_dep], - install: false - ) - benchmark(bench, exe) - endforeach + subdir('benchmarks') endif -summary({'C': meson.get_compiler('c').cmd_array(), - 'C++': meson.get_compiler('cpp').cmd_array() +summary({ + 'C': meson.get_compiler('c').cmd_array(), + 'C++': meson.get_compiler('cpp').cmd_array(), + 'Architecture': target_machine.cpu_family(), }, section: 'Compilers') + +summary({ + 'Hedley': hedley_dep.version(), + 'fmt': fmt_dep.version(), + 'xsimd': xsimd_dep.version() + }, section: 'Dependencies') + +summary({ + 'CDFpp version': meson.project_version(), + 'Build type': get_option('buildtype'), + 'Libdeflate support': get_option('use_libdeflate'), + 'NoMap support': get_option('use_nomap'), + 'Python wrapper': not get_option('disable_python_wrapper'), + 'Experimental Zstd support': get_option('with_experimental_zstd'), + 'Experimental Wasm support': get_option('with_experimental_wasm'), + 'Tests': get_option('with_tests'), + 'Benchmarks': get_option('with_benchmarks') + }, section: 'Configuration Options') diff --git a/notebooks/TimeConvBenchmarksAnalysis.ipynb b/notebooks/TimeConvBenchmarksAnalysis.ipynb new file mode 100644 index 0000000..e075d3d --- /dev/null +++ b/notebooks/TimeConvBenchmarksAnalysis.ipynb @@ -0,0 +1,181 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "f89a5104-3cd3-4624-a153-99eb1536d2b4", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "from math import log10\n", + "%matplotlib inline\n", + "\n", + "import pandas as pds\n", + "import json" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "0f2ea41e-4da7-46ac-a6e6-08317069db76", + "metadata": {}, + "outputs": [], + "source": [ + "results = json.load(open(\"chrono_bench.json\"))" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "027c99fb-28ea-408b-86e2-a14943359ec6", + "metadata": {}, + "outputs": [], + "source": [ + "benchmakrs=pds.DataFrame(results[\"benchmarks\"])\n", + "scalar=benchmakrs[benchmakrs.name.str.contains(\"scalar\")]\n", + "avx=benchmakrs[benchmakrs.name.str.contains(\"vectorized\")]\n", + "mixed=benchmakrs[benchmakrs.name.str.contains(\"entry_point\")]" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "d339b32c-c1ab-4a27-8ca3-05bbc161db5b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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ERERUQCYRBEREREREQWASRQREREREVEAmEQREREREREFgEkUERERERFRAJhEERERERERBYBJFBERERERUQCYRBERkSrNnz8fiYmJIf85jzzyCP7617+G/OeEUm5uLgRBQEVFxWn3+e9//4sJEyaENS4iIrViEkVEFMFuvvlmXHHFFWH/ucFIgK699lrs3r07aDE15NixY3j11Vfxr3/9q8HvHz9+HAaDATabDS6XCyaTCQcPHqyzT1ZWFgRBqHebPXt2SGMP1PTp07Fx40b89ttvcodCRKR4OrkDICIiOhfR0dGIjo4O6c949913MWzYMGRmZjb4/d9//x29e/eGyWTC2rVrkZSUhHbt2tXb78knn8Rtt91WZ1tcXFzI4j4XBoMB119/PV577TWMGDFC7nCIiBSNlSgiIvK74IILMHPmTDzwwANISkpCeno6Hn/88Tr7CIKA//znP7j44osRHR2NDh064IsvvvB/v6GhY/n5+RAEAfv370dubi6mTZuGyspKf1Xm1J9RY9OmTRg1ahTi4uIQHx+P/v37Y8OGDUAD1azTVXxqHDp0CNdccw0SExORlJSEiRMnYv/+/Wc8HgsXLjzjELc1a9Zg+PDhAIBVq1b5758qLi4O6enpdW4mk6nO8fr++++Rk5MDo9GIIUOGYOvWrXVe48svv0SPHj0QFRWFrKwsvPjii3W+73A48OCDDyIjIwNRUVHo1KkT3nvvvTr75OXlYcCAAYiJicGwYcOwa9euOt+fMGECFi9ejOrq6jMeFyKiSMckioiI6vjggw/8lZU5c+bgySefxC+//FJnn0ceeQRXXXUVNm3ahBtuuAGTJ0/Gjh07GvX6w4YNwyuvvIL4+HgcPXoUR48exX333dfgvjfccAPatm2L9evXIy8vDw899BD0en2D+65fv97/ekVFRRgyZIi/ouJyuTB+/HjExcXht99+w+rVqxEbG4uLLroITqezwdcrKyvD9u3bMWDAgDrbDx48iMTERCQmJuKll17CW2+9hcTERDz88MP45ptvkJiYiL///e+NOha13X///XjxxRexfv16tGzZEhMmTIDL5QJ8yc8111yDyZMnY8uWLXj88cfxyCOPYP78+f7nT5kyBZ9++ilee+017NixA2+99RZiY2Pr/Ix//etfePHFF7FhwwbodDpMnz69zvcHDBgAt9uNtWvXBhw/EVFEEYmIKGJNnTpVnDhxov/xyJEjxfPOO6/OPgMHDhQffPBB/2MA4t/+9rc6+wwePFi84447RFEUxeXLl4sAxPLycv/3//zzTxGAWFhYKIqiKM6bN09MSEg4a3xxcXHi/PnzG/zemV5j5syZYmZmplhSUiKKoih++OGHYteuXUWv1+vfx+FwiNHR0eJPP/3U4GvUxHzw4ME6210ul1hYWChu2rRJ1Ov14qZNm8Q9e/aIsbGx4ooVK8TCwkLx+PHj/v0zMzNFg8EgmkymOreVK1fWOV4LFy70P+fEiRNidHS0+Nlnn4miKIrXX3+9OG7cuDpx3H///WJ2drYoiqK4a9cuEYD4yy+/NPi71PyMpUuX+rd9//33IgCxurq6zr4tWrQ47TEnIiIJK1FERFRHTk5OncetWrVCSUlJnW1Dhw6t97ixlahAzJo1C7feeivGjh2L2bNnY+/evWd9zttvv4333nsPixcvRsuWLQHfsMA9e/YgLi4OsbGxiI2NRVJSEux2+2lfs2ZIm9ForLNdp9MhKysLO3fuxMCBA5GTk4Njx44hLS0N559/PrKyspCSklLnOffffz/y8/Pr3E6tcNU+pklJSejatav/mO7YsaPeUMHhw4ejoKAAHo8H+fn50Gq1GDly5BmPTe1z26pVKwCod26jo6NRVVV1xtchIop0bCxBRER1nDpcThAEeL3eRj9fo5E+n5OKVpKaYWmBevzxx3H99dfj+++/x48//ojHHnsMCxcuxJVXXtng/suXL8ddd92FTz/9tE7CYLVa0b9/f3z88cf1nlOTaJ2qJhEqLy+vs0+PHj1w4MABuFwueL1exMbGwu12w+12IzY2FpmZmdi2bVu91+rUqdM5HYPGaGyDjdrntma+2Knntqys7LTHhIiIJKxEERFRwP744496j7t37w7USkqOHj3q/35+fn6d/Q0GAzweT6N+VpcuXXDPPffg559/xqRJkzBv3rwG99uzZw+uvvpqPPzww5g0aVKd7/Xr1w8FBQVITU1Fp06d6twSEhIafL2OHTsiPj4e27dvr7P9hx9+QH5+PtLT0/HRRx8hPz8fPXv2xCuvvIL8/Hz88MMPjfq9TlX7mJaXl2P37t3+Y9q9e3esXr26zv6rV69Gly5doNVq0atXL3i9XqxYseKcfnaNvXv3wm63o2/fvk16HSKi5o5JFBERBezzzz/H+++/j927d+Oxxx7DunXrcOeddwIAOnXqhIyMDDz++OMoKCjA999/X6+TXFZWFqxWK5YtW4bS0tIGh49VV1fjzjvvRG5uLg4cOIDVq1dj/fr1/sTi1H0nTJiAvn374q9//SuOHTvmv8HXoCIlJQUTJ07Eb7/9hsLCQuTm5mLmzJkoKipq8HfUaDQYO3YsVq1aVWd7ZmYmYmNjUVxcjIkTJyIjIwPbtm3DVVddhU6dOjXYDt1isdSJ6dixYzCbzXX2efLJJ7Fs2TJs3boVN998M1JSUvxreN17771YtmwZ/v3vf2P37t344IMP8MYbb/gbcmRlZWHq1KmYPn06vvnmG//vt2jRorOey9p+++03dOjQAR07dgzoeUREkYZJFBERBeyJJ57AwoULkZOTgwULFuDTTz9FdnY24Bsy9umnn2Lnzp3IycnBc889h6eeeqrO84cNG4a//e1vuPbaa9GyZUvMmTOn3s/QarU4ceIEpkyZgi5duuCaa67BxRdfjCeeeKLevsXFxdi5cyeWLVuG1q1bo1WrVv4bAMTExGDlypVo164dJk2ahO7du+OWW26B3W5HfHz8aX/PW2+9FQsXLqw35C03NxcDBw6E0WjEunXr0LZtW//Pasijjz5aJ6ZWrVrhgQceqLPP7Nmzcffdd6N///44duwY/ve//8FgMAC+StqiRYuwcOFC9OzZE48++iiefPJJ3Hzzzf7n/+c//8HVV1+Nv//97+jWrRtuu+022Gy208bUkE8//bTeelZERFSfINYetE5ERHQWgiDg66+/9ldJmjNRFDF48GDcc889uO6660LyM3JzczFq1CiUl5fXWfcq3LZt24bRo0dj9+7dpx3iSEREElaiiIiITkMQBLz99ttwu91yhxJyR48exYIFC5hAERE1ArvzERERnUGfPn3Qp08fucMIubFjx8odAhGRanA4HxERERERUQA4nI+IiIiIiCgATKKIiIiIiIgCwCSKiIiIiIgoAEyiiIiIiIiIAsAkioiIiIiIKABMooiIiIiIiALAJIqIiIiIiCgATKKIiIiIiIgCwCSKiIiIiIgoAP8P4xge/zTK5yYAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "vmin=min(benchmakrs.epochs_per_second)\n", + "vmax=max(benchmakrs.epochs_per_second)\n", + "f=plt.figure(figsize=(10,8))\n", + "plt.title(\"tt2000 to Numpy datetime64[ns]\")\n", + "ax=plt.subplot()\n", + "scalar.plot(x=\"Epochs\", y=\"epochs_per_second\", ax=ax, marker=\"+\")\n", + "avx.plot(x=\"Epochs\", y=\"epochs_per_second\", ax=ax, marker=\"o\")\n", + "mixed.plot(x=\"Epochs\", y=\"epochs_per_second\", ax=ax, marker=\"v\")\n", + "\n", + "last_cache_size=1\n", + "for cache in results['context'][\"caches\"]:\n", + " if cache[\"type\"] in ('Data','Unified'):\n", + " size = cache['size']/8\n", + " name = f\"L{cache['level']}\"\n", + " ax.vlines(size, vmin, vmax, label=name, linestyles=\"dashed\")\n", + " mid = 10**((log10(size)+log10(last_cache_size))/2)\n", + " ax.text(mid, vmin, name, fontweight='heavy', fontsize=\"x-large\", horizontalalignment=\"center\")\n", + " last_cache_size = size\n", + "\n", + "ax.legend([\n", + " \"Scalar\",\n", + " \"AVX512\",\n", + " \"AVX512 + 2 threads\"\n", + "])\n", + "\n", + "plt.ylabel(\"Throughput (Epoch/s)\")\n", + "plt.xlabel(\"Input size (#Epoch)\")\n", + "\n", + "plt.loglog()" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "25400893-ecca-43ea-ad7b-66c9dbafb245", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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P7B27VqSk5PJyMjAx8eHqKgo2rZty8CBA7n99tsJCNDvDxERAMM0vXsM0N9bojIzMz23i8Lmz+Dzf5x5u+v+A22vd0ciERERERGPU9baQKMHVAfBURW7nYiIiIiInJbXFlEJCQm0bt2aLl26WB3l3DXqAaH1gdONmGRAaHTJdiIiIiIick7Una86dOcDSPwSZt3+250/H9LfCqsbPygZ5lxERERERE5J3fm8TetBJYVSaL0Tl4fWVwElIiIiIlKBNDpfddJ6UMnw5QeWQ05yyTVQjXqAzW51MhERERGRakNFVHVjs0OTnlanEBERERGpttSdT0REREREpBxURImIiIiIiJSD1xZR1WqIcxERERERcRsNcV5dhjgXEREREZFzoiHORUREREREKoGKKBERERERkXJQESUiIiIiIlIOKqJERERERETKQUWUiIiIiIhIOaiIEhERERERKQevLaI0T5SIiIiIiJwNzROleaJERERERETzRImIiIiIiFQOFVEiIiIiIiLloCJKRERERESkHFREiYiIiIiIlIOKKBERERERkXJQESUiIiIiIlIOKqJERERERETKQUWUiIiIiIhIOfhYHcBqv881nJWVZXUUERERERGx0O81we81wul4fRGVnZ0NQExMjNVRRERERESkCsjOziYsLOy06w3zTGVWNedyuWjRogVr167FMIzTbtelSxdWr15d5nWn2/6vy/98Pysri5iYGA4dOkRoaOg5vKsz+7v3U9GPP9O25d23p1t+pmXav2VfV5b9a9W+/bvcFf3Ysmyr/Xv2jz2X/avPhnPfVp+9Z/94fTZU7mO1fyv3sfps+HumaZKdnU39+vWx2U5/5ZPXt0TZbDb8/Pz+ttIEsNvtpz3Ap1p3uu3/uvxU24WGhlb6yfR376eiH3+mbcu7b0+3vKzLtH/PvK4s+9KqfXu6166Mx5ZlW+3fs3/suexffTac+7b67D37x+uzoXIfq/1buY/VZ8OZnakuQANLlIiPjz+nbU617nTb/3V5WV67Mpzr65bn8Wfatrz79nTLy3McKps37F+r9u25vnZF7tszbaP9e+7blmcfnm65PhvKv17799y302fD2T9W+7dyH6vPhorh9d35qpKsrCzCwsLIzMx0yzce3kb7t/Jo31Yu7d/Kpf1bubR/K4/2beXS/q1cnr5/1RJVhfj7+/PUU0/h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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "vmin=min(benchmakrs.real_time)\n", + "vmax=max(benchmakrs.real_time)\n", + "f=plt.figure(figsize=(10,8))\n", + "plt.title(\"tt2000 to Numpy datetime64[ns]\")\n", + "ax=plt.subplot()\n", + "scalar.plot(x=\"Epochs\", y=\"real_time\", ax=ax, marker=\"+\")\n", + "avx.plot(x=\"Epochs\", y=\"real_time\", ax=ax, marker=\"o\")\n", + "mixed.plot(x=\"Epochs\", y=\"real_time\", ax=ax, marker=\"v\")\n", + "\n", + "last_cache_size=1\n", + "for cache in results['context'][\"caches\"]:\n", + " if cache[\"type\"] in ('Data','Unified'):\n", + " size = cache['size']/8\n", + " name = f\"L{cache['level']}\"\n", + " ax.vlines(size, vmin, vmax, label=name, linestyles=\"dashed\")\n", + " mid = 10**((log10(size)+log10(last_cache_size))/2)\n", + " ax.text(mid, vmin, name, fontweight='heavy', fontsize=\"x-large\", horizontalalignment=\"center\")\n", + " last_cache_size = size\n", + "\n", + "ax.legend([\n", + " \"Scalar\",\n", + " \"AVX512\",\n", + " \"AVX512 + 2 threads\"\n", + "])\n", + "\n", + "plt.ylabel(\"real_time (ns)\")\n", + "plt.xlabel(\"Input size (#Epoch)\")\n", + "\n", + "plt.loglog()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/pycdfpp/__init__.py b/pycdfpp/__init__.py index d1b25ee..ca3b006 100644 --- a/pycdfpp/__init__.py +++ b/pycdfpp/__init__.py @@ -195,67 +195,50 @@ def _values_view_and_type(values: np.ndarray or list, data_type: DataType or Non def _patch_set_values(): - @overload - def _set_values_wrapper(self: Variable, values: np.ndarray or list, data_type: DataType or None = None): - ... - @overload - def _set_values_wrapper(self: Variable, values: Variable): - ... + def _set_values_wrapper(self, values, data_type=None, force=False): + """Sets or resets the values of the variable. - def _set_values_wrapper(self, *args, **kwargs): - """Sets the values of the variable. - - This method can be called in two ways: - 1. With values and optional data type: set_values(values, data_type=None) - 2. With another Variable object: set_values(variable) - Parameters ---------- - values : np.ndarray or list + values : numpy.ndarray or list or tuple or Variable The values to set for the variable. - When a list is passed, the values are converted to a numpy.ndarray with the appropriate data type, - with integers, it will choose the smallest data type that can hold all the values. data_type : DataType or None, optional The data type of the variable. If None, the data type is inferred from the values. (Default is None) - variable : Variable - An existing Variable object to set the values from (for the second calling method). - + When passing integer values as a list or tuple, it will choose the smallest data type that can hold all the values. + When passing a Variable, the data type is taken from the Variable. + force : bool, optional + If True, allows to overwrite existing values even if the shape or data type do not match. + (Default is False) + Returns + ------- + None Raises ------ ValueError - If the variable already exists or if the values are not compatible with the variable's type. + If the shape or data type do not match and force is False. + Examples + -------- + >>> from pycdfpp import CDF, DataType + >>> import numpy as np + >>> cdf = CDF() + >>> cdf.add_variable("var1") + var1: + shape: [ ] + type: CDF_NONE + record vary: True + compression: None + ... + >>> # Setting values with numpy array + >>> cdf["var1"].set_values(np.arange(10, 20, dtype=np.int32)) + >>> cdf["var1"].values + array([10, 11, 12, 13, 14, 15, 16, 17, 18, 19], dtype=int32) """ - - if len(args) == 1 and isinstance(args[0], Variable): - if self.type != DataType.CDF_NONE and self.type != args[0].type: - raise ValueError( - f"Can't set variable of type {self.type} with values of type {args[0].type}") - self._set_values(args[0]) + if isinstance(values, Variable): + return self._set_values(values, force=force) else: - arg_names = ['values', 'data_type'] - for i, arg in enumerate(args): - if i < len(arg_names): - kwargs[arg_names[i]] = arg - else: - raise TypeError(f"Unexpected argument {arg} at position {i+1}. Expected one of {arg_names}.") - values = kwargs.get('values') - data_type = kwargs.get('data_type') - values, data_type = _values_view_and_type(values, data_type, target_type=self.type) - if data_type not in _CDF_TYPES_COMPATIBILITY_TABLE_[self.type]: - raise ValueError( - f"Can't set variable of type {self.type} with values of type {data_type}") - if self.is_nrv: - if self.shape[1:] != values.shape[1:]: - if self.shape[1:] != values.shape: - raise ValueError( - f"Can't set NRV variable of shape {self.shape} with values of shape {values.shape}") - else: - values = values.reshape((1,) + values.shape) - elif self.type != DataType.CDF_NONE and self.shape[1:] != values.shape[1:]: - raise ValueError( - f"Can't set variable of shape {self.shape} with values of shape {values.shape}") - self._set_values(values, data_type) + return self._set_values(values, data_type=data_type, force=force) + # Removed python injected wrappers, the logic is now implemented in C++ Variable.set_values = _set_values_wrapper @@ -347,17 +330,11 @@ def _add_variable_wrapper(self, *args, **kwargs) -> Variable: compression = kwargs.get('compression', CompressionType.no_compression) attributes = kwargs.get('attributes', None) + var = self._add_variable(name, is_nrv=is_nrv, compression=compression) if values is not None: - v, t = _values_view_and_type(values, data_type) - var = self._add_variable( - name=name, values=v, data_type=t, is_nrv=is_nrv, compression=compression) + var.set_values(values, data_type) elif data_type is not None: - v, t = _values_view_and_type([], data_type) - var = self._add_variable( - name=name, values=v, data_type=t, is_nrv=is_nrv, compression=compression) - else: - var = self._add_variable( - name=name, is_nrv=is_nrv, compression=compression) + var.set_values([], data_type) if attributes is not None and var is not None: for name, values in attributes.items(): var.add_attribute(name, values) @@ -688,18 +665,26 @@ def _make_filter(criterion): CDF.filter = filter_cdf def to_datetime64(values): - """ - to_datetime64 + """Convert any compatible given collection of time values to a numpy.datetime64 array. Parameters ---------- - values: Variable or epoch or List[epoch] or numpy.ndarray[epoch] or epoch16 or List[epoch16] or numpy.ndarray[epoch16] or tt2000_t or List[tt2000_t] or numpy.ndarray[tt2000_t] + values: Variable or epoch or List[epoch] or numpy.ndarray[epoch] or epoch16 or List[epoch16] or numpy.array[epoch16] or tt2000_t or List[tt2000_t] or numpy.array[tt2000_t] input value(s) to convert to numpy.datetime64 Returns ------- numpy.ndarray[numpy.datetime64] + + Raises + ------ + TypeError or IndexError + If the input values are not compatible time types. + + Note + ---- + On modern x86_64 systems, it will use the CPU's vectorized instructions to perform the conversion even faster. """ return _pycdfpp.to_datetime64(values) @@ -710,13 +695,18 @@ def to_datetime(values): Parameters ---------- - values: Variable or epoch or List[epoch] or epoch16 or List[epoch16] or tt2000_t or List[tt2000_t] + values: Variable or epoch or List[epoch] or epoch16 or List[epoch16] or tt2000_t or List[tt2000_t] or numpy.array[numpy.datetime64[ns]] input value(s) to convert to datetime.datetime Returns ------- List[datetime.datetime] + + Raises + ------ + TypeError or IndexError + If the input values are not compatible time types. """ return _pycdfpp.to_datetime(values) @@ -727,7 +717,7 @@ def to_tt2000(values): Parameters ---------- - values: datetime.datetime or List[datetime.datetime] + values: datetime.datetime or List[datetime.datetime] or numpy.array[numpy.datetime64[ns]] input value(s) to convert to CDF tt2000 @@ -744,7 +734,7 @@ def to_epoch(values): Parameters ---------- - values: datetime.datetime or List[datetime.datetime] + values: datetime.datetime or List[datetime.datetime] or numpy.array[numpy.datetime64[ns]] input value(s) to convert to CDF epoch @@ -761,7 +751,7 @@ def to_epoch16(values): Parameters ---------- - values: datetime.datetime or List[datetime.datetime] + values: datetime.datetime or List[datetime.datetime] or numpy.array[numpy.datetime64[ns]] input value(s) to convert to CDF epoch16 diff --git a/pycdfpp/buffers.hpp b/pycdfpp/buffers.hpp deleted file mode 100644 index 8480144..0000000 --- a/pycdfpp/buffers.hpp +++ /dev/null @@ -1,296 +0,0 @@ -/*------------------------------------------------------------------------------ --- The MIT License (MIT) --- --- Copyright © 2024, Laboratory of Plasma Physics- CNRS --- --- Permission is hereby granted, free of charge, to any person obtaining a copy --- of this software and associated documentation files (the “Software”), to deal --- in the Software without restriction, including without limitation the rights --- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies --- of the Software, and to permit persons to whom the Software is furnished to do --- so, subject to the following conditions: --- --- The above copyright notice and this permission notice shall be included in all --- copies or substantial portions of the Software. --- --- THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, --- INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A --- PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT --- HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION --- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE --- SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------------------------------*/ -/*-- Author : Alexis Jeandet --- Mail : alexis.jeandet@member.fsf.org -----------------------------------------------------------------------------*/ -#pragma once -#if defined(_MSC_VER) -#pragma warning(push) -#pragma warning(disable : 4127) // warning C4127: Conditional expression is constant -#include -typedef SSIZE_T ssize_t; -#endif - -#include -#include -#include -using namespace cdf; - -#include -#include -#include -#include - -namespace py = pybind11; - -namespace _details -{ - -[[nodiscard]] std::vector shape_ssize_t(const Variable& var) -{ - const auto& shape = var.shape(); - std::vector res(std::size(shape)); - std::transform(std::cbegin(shape), std::cend(shape), std::begin(res), - [](auto v) { return static_cast(v); }); - return res; -} - -[[nodiscard]] std::vector str_shape_ssize_t(const Variable& var) -{ - const auto& shape = var.shape(); - std::vector res(std::size(shape) - 1); - std::transform(std::cbegin(shape), std::cend(shape) - 1, std::begin(res), - [](auto v) { return static_cast(v); }); - return res; -} - -template -[[nodiscard]] std::vector strides(const Variable& var) -{ - const auto& shape = var.shape(); - std::vector res(std::size(shape)); - std::transform(std::crbegin(shape), std::crend(shape), std::begin(res), - [next = sizeof(T)](auto v) mutable - { - auto res = next; - next = static_cast(v * next); - return res; - }); - std::reverse(std::begin(res), std::end(res)); - return res; -} - -template -[[nodiscard]] std::vector str_strides(const Variable& var) -{ - const auto& shape = var.shape(); - std::vector res(std::size(shape) - 1); - std::transform(std::crbegin(shape) + 1, std::crend(shape), std::begin(res), - [next = shape.back()](auto v) mutable - { - auto res = next; - next = static_cast(v * next); - return res; - }); - std::reverse(std::begin(res), std::end(res)); - return res; -} - -template -[[nodiscard]] py::array make_array(Variable& variable, py::object& obj) -{ - // static_assert(data_t != CDF_Types::CDF_CHAR and data_t != CDF_Types::CDF_UCHAR); - from_cdf_type_t* ptr = nullptr; - { - py::gil_scoped_release release; - ptr = variable.get>().data(); - } - return py::array_t>( - shape_ssize_t(variable), strides>(variable), ptr, obj); -} - -template -[[nodiscard]] static inline std::string_view make_string_view(T* data, size_type len) -{ - return std::string_view( - reinterpret_cast(data), static_cast(len)); -} - -template -[[nodiscard]] py::object make_list( - const T* data, decltype(std::declval().shape()) shape, py::object& obj) -{ - if (std::size(shape) > 2) - { - py::list res {}; - std::size_t offset = 0; - auto inner_shape = decltype(shape) { std::begin(shape) + 1, std::end(shape) }; - std::size_t jump = std::accumulate( - std::cbegin(inner_shape), std::cend(inner_shape), 1UL, std::multiplies()); - for (auto i = 0UL; i < shape[0]; i++) - { - res.append(make_list(data + offset, inner_shape, obj)); - offset += jump; - } - return res; - } - if (std::size(shape) == 2) - { - py::list res {}; - std::size_t offset = 0; - for (auto i = 0UL; i < shape[0]; i++) - { - res.append(make_string_view(data + offset, shape[1])); - offset += shape[1]; - } - return res; - } - if (std::size(shape) == 1) - { - return py::str(make_string_view(data, shape[0])); - } - return py::none(); -} - -template -[[nodiscard]] py::object make_list(Variable& variable, py::object& obj) -{ - static_assert(data_t == CDF_Types::CDF_CHAR or data_t == CDF_Types::CDF_UCHAR); - return make_list(variable.get>().data(), variable.shape(), obj); -} - -template -[[nodiscard]] py::buffer_info impl_make_buffer(cdf::Variable& var) -{ - using U = cdf::from_cdf_type_t; - char* ptr = nullptr; - { - py::gil_scoped_release release; - ptr = var.bytes_ptr(); - } - if constexpr ((T == CDF_Types::CDF_CHAR) or (T == CDF_Types::CDF_UCHAR)) - { - return py::buffer_info(ptr, /* Pointer to buffer */ - var.shape().back(), /* Size of one scalar */ - fmt::format("{}s", var.shape().back()), - static_cast(std::size(var.shape()) - 1), /* Number of dimensions */ - str_shape_ssize_t(var), str_strides(var), true); - } - else - { - return py::buffer_info(ptr, /* Pointer to buffer */ - sizeof(U), /* Size of one scalar */ - py::format_descriptor::format(), - static_cast(std::size(var.shape())), /* Number of dimensions */ - shape_ssize_t(var), strides(var), true); - } -} - -template -[[nodiscard]] py::object make_str_array(py::object& obj) -{ - py::module_ np = py::module_::import("numpy"); - if constexpr (encode_strings) - { - return np.attr("char").attr("decode")(py::memoryview(obj)); - } - else - { - return np.attr("array")(py::memoryview(obj)); - } -} - -} - -template -[[nodiscard]] py::object make_values_view(py::object& obj) -{ - Variable& variable = obj.cast(); - switch (variable.type()) - { - case cdf::CDF_Types::CDF_CHAR: - return _details::make_str_array(obj); - case cdf::CDF_Types::CDF_UCHAR: - return _details::make_str_array(obj); - case cdf::CDF_Types::CDF_INT1: - return _details::make_array(variable, obj); - case cdf::CDF_Types::CDF_INT2: - return _details::make_array(variable, obj); - case cdf::CDF_Types::CDF_INT4: - return _details::make_array(variable, obj); - case cdf::CDF_Types::CDF_INT8: - return _details::make_array(variable, obj); - case cdf::CDF_Types::CDF_UINT1: - return _details::make_array(variable, obj); - case cdf::CDF_Types::CDF_BYTE: - return _details::make_array(variable, obj); - case cdf::CDF_Types::CDF_UINT2: - return _details::make_array(variable, obj); - case cdf::CDF_Types::CDF_UINT4: - return _details::make_array(variable, obj); - case cdf::CDF_Types::CDF_FLOAT: - return _details::make_array(variable, obj); - case cdf::CDF_Types::CDF_REAL4: - return _details::make_array(variable, obj); - case cdf::CDF_Types::CDF_DOUBLE: - return _details::make_array(variable, obj); - case cdf::CDF_Types::CDF_REAL8: - return _details::make_array(variable, obj); - case cdf::CDF_Types::CDF_EPOCH: - return _details::make_array(variable, obj); - case cdf::CDF_Types::CDF_EPOCH16: - return _details::make_array(variable, obj); - case cdf::CDF_Types::CDF_TIME_TT2000: - return _details::make_array(variable, obj); - default: - throw std::runtime_error { std::string { "Unsupported CDF type " } - + std::to_string(static_cast(variable.type())) }; - break; - } - return {}; -} - -[[nodiscard]] py::buffer_info make_buffer(cdf::Variable& variable) -{ - - using namespace cdf; - switch (variable.type()) - { - case CDF_Types::CDF_UCHAR: - return _details::impl_make_buffer(variable); - case CDF_Types::CDF_CHAR: - return _details::impl_make_buffer(variable); - case CDF_Types::CDF_INT1: - return _details::impl_make_buffer(variable); - case CDF_Types::CDF_INT2: - return _details::impl_make_buffer(variable); - case CDF_Types::CDF_INT4: - return _details::impl_make_buffer(variable); - case CDF_Types::CDF_INT8: - return _details::impl_make_buffer(variable); - case CDF_Types::CDF_BYTE: - return _details::impl_make_buffer(variable); - case CDF_Types::CDF_UINT1: - return _details::impl_make_buffer(variable); - case CDF_Types::CDF_UINT2: - return _details::impl_make_buffer(variable); - case CDF_Types::CDF_UINT4: - return _details::impl_make_buffer(variable); - case CDF_Types::CDF_FLOAT: - case CDF_Types::CDF_REAL4: - return _details::impl_make_buffer(variable); - case CDF_Types::CDF_DOUBLE: - case CDF_Types::CDF_REAL8: - return _details::impl_make_buffer(variable); - case CDF_Types::CDF_EPOCH: - return _details::impl_make_buffer(variable); - case CDF_Types::CDF_EPOCH16: - return _details::impl_make_buffer(variable); - case CDF_Types::CDF_TIME_TT2000: - return _details::impl_make_buffer(variable); - default: - throw std::runtime_error { std::string { "Unsupported CDF type " } - + std::to_string(static_cast(variable.type())) }; - break; - } -} diff --git a/pycdfpp/chrono.hpp b/pycdfpp/chrono.hpp index b19a509..9ff6dbb 100644 --- a/pycdfpp/chrono.hpp +++ b/pycdfpp/chrono.hpp @@ -24,12 +24,16 @@ -- Mail : alexis.jeandet@member.fsf.org ----------------------------------------------------------------------------*/ #pragma once - #include "repr.hpp" +#include "collections.hpp" +#include "data_types.hpp" + #include #include #include +#include + using namespace cdf; #include @@ -38,36 +42,107 @@ using namespace cdf; #include #include +#include + namespace py = pybind11; -template -[[nodiscard]] inline auto transform(time_t* input, std::size_t count, const function_t& f) +namespace _details { - auto result = py::array_t(count); - py::buffer_info res_buff = result.request(true); - int64_t* res_ptr = static_cast(res_buff.ptr); - std::transform(input, input + count, res_ptr, f); - return result; + + + +template +concept time_point_type = requires(T a) { a.time_since_epoch(); }; + + +[[nodiscard]] inline PyObject* to_datetime(const time_point_type auto& timepoint) +{ + auto dp = floor(timepoint); + year_month_day ymd { dp }; + hh_mm_ss time { floor(timepoint - dp) }; + return PyDateTime_FromDateAndTime(static_cast(ymd.year()), + static_cast(ymd.month()), static_cast(ymd.day()), + static_cast(time.hours().count()), static_cast(time.minutes().count()), + static_cast(time.seconds().count()), + static_cast(time.subseconds().count())); // microseconds } -template -[[nodiscard]] inline auto transform(const py::array_t& input, const function_t& f) + +[[nodiscard]] inline auto to_tp(int64_t ns) { - py::buffer_info in_buff = input.request(); - time_t* in_ptr = static_cast(in_buff.ptr); - return transform(in_ptr, in_buff.shape[0], f); + return std::chrono::system_clock::time_point {} + std::chrono::nanoseconds(ns); } -template -[[nodiscard]] inline auto transform(const std::vector& input, const function_t& f) +[[nodiscard]] inline auto to_tp(const PyDateTime_DateTime* dt) { - auto result = py::array_t(std::size(input)); - py::buffer_info res_buff = result.request(true); - int64_t* res_ptr = static_cast(res_buff.ptr); - std::transform(std::cbegin(input), std::cend(input), res_ptr, f); + int year = PyDateTime_GET_YEAR(dt); + int month = PyDateTime_GET_MONTH(dt); + int day = PyDateTime_GET_DAY(dt); + int hour = PyDateTime_DATE_GET_HOUR(dt); + int minute = PyDateTime_DATE_GET_MINUTE(dt); + int second = PyDateTime_DATE_GET_SECOND(dt); + int micro = PyDateTime_DATE_GET_MICROSECOND(dt); + + std::tm t = {}; + t.tm_year = year - 1900; + t.tm_mon = month - 1; + t.tm_mday = day; + t.tm_hour = hour; + t.tm_min = minute; + t.tm_sec = second; + t.tm_isdst = -1; // Unknown DST + + // Warning: mktime uses local time. If your Python datetime is UTC, + // use timegm (Linux) or _mkgmtime (Windows). +#ifdef _WIN32 + std::time_t tt = _mkgmtime(&t); +#else + std::time_t tt = timegm(&t); +#endif + + auto duration = std::chrono::seconds(tt) + std::chrono::microseconds(micro); + return std::chrono::system_clock::time_point( + std::chrono::duration_cast(duration)); +} + +template +[[nodiscard]] inline time_t from_datetime(const PyObject* dt) +{ + return cdf::to_cdf_time(to_tp(reinterpret_cast(dt))); +} + +[[nodiscard]] inline PyObject* to_datetime(int64_t ns) +{ + return to_datetime(to_tp(ns)); +} + +[[nodiscard]] inline PyObject* to_datetime(const cdf_time_t auto time) +{ + return to_datetime(cdf::to_time_point(time)); +} + +template +[[nodiscard]] inline py::list to_datetime(const std::span input) +{ + using namespace _details::ranges; + py::list result; + auto out = py_stealing_raw_sink { _details::underlying_pyobject(result) }; + for (auto v : input) + { + *out = to_datetime(v); + } return result; } +} // namespace _details + +template +inline void to_datetime64(const std::span& input, int64_t* const output) +{ + py::gil_scoped_release release; + cdf::to_ns_from_1970(input, output); +} + template [[nodiscard]] constexpr auto time_t_to_dtype() { @@ -85,135 +160,359 @@ template } template -[[nodiscard]] inline py::object array_to_datetime64(const py::array_t& input) +[[nodiscard]] inline py::object to_datetime64(const py::array_t& input) { constexpr auto dtype = time_t_to_dtype(); if (input.ndim() > 0) { - auto result = transform(input, - [](const time_t& v) { return cdf::to_time_point(v).time_since_epoch().count(); }); - return py::cast(&result).attr("astype")(dtype); + auto result = _details::ranges::transform(input, + static_cast&, int64_t* const)>(to_datetime64)); + return result.attr("view")(dtype); } - return py::none(); + return py::array_t().attr("view")(dtype); } -template -[[nodiscard]] inline py::object scalar_to_datetime64(const time_t& input) +[[nodiscard]] inline py::object to_datetime64(const cdf_time_t auto& input) { + using time_t = std::decay_t; constexpr auto dtype = time_t_to_dtype(); - auto v = new int64_t; - *v = cdf::to_time_point(input).time_since_epoch().count(); - return py::array(py::dtype(dtype), {}, {}, v); + int64_t v = cdf::to_time_point(input).time_since_epoch().count(); + return py::array(py::dtype(dtype), {}, {}, &v); } -template -[[nodiscard]] inline py::object vector_to_datetime64(const std::vector& input) +[[nodiscard]] inline int64_t to_datetime64(const PyDateTime_DateTime* dt) { - constexpr auto dtype = time_t_to_dtype(); + using namespace std::chrono; + + int y = PyDateTime_GET_YEAR(dt); + int m = PyDateTime_GET_MONTH(dt); + int d = PyDateTime_GET_DAY(dt); + int hh = PyDateTime_DATE_GET_HOUR(dt); + int mm = PyDateTime_DATE_GET_MINUTE(dt); + int ss = PyDateTime_DATE_GET_SECOND(dt); + int us = PyDateTime_DATE_GET_MICROSECOND(dt); + + auto date = year_month_day { year { y }, month { static_cast(m) }, + day { static_cast(d) } }; + sys_days tp_days = date; + + auto tp = tp_days + hours { hh } + minutes { mm } + seconds { ss } + microseconds { us }; + + return duration_cast(tp.time_since_epoch()).count(); +} + +[[nodiscard]] inline int64_t to_datetime64(const PyObject* o) +{ + return to_datetime64(reinterpret_cast(o)); +} + +[[nodiscard]] inline py::object to_datetime64(const py_list_or_py_tuple auto& input) +{ + /* + * The code here is directly using Python C-API to avoid the overhead of pybind11 in order + * to reduce as much as possible the overhead of iterating a list/tuple of random PyObject*. + * We avoid using py::cast or py::object for the items since they would add reference counting. + */ + static auto* tt2000_type_ptr = reinterpret_cast(py::type::of().ptr()); + static auto* epoch_type_ptr = reinterpret_cast(py::type::of().ptr()); + static auto* epoch16_type_ptr = reinterpret_cast(py::type::of().ptr()); + + return _details::ranges::transform>(input, + [](const PyObject* obj) -> int64_t + { + if (PyDateTime_Check(obj)) + { + return to_datetime64(obj); + } + else + { + auto item_type = Py_TYPE(obj); + if (item_type == tt2000_type_ptr) + { + return cdf::to_time_point(*_details::cast(obj)) + .time_since_epoch() + .count(); + } + else if (item_type == epoch_type_ptr) + { + return cdf::to_time_point(*_details::cast(obj)) + .time_since_epoch() + .count(); + } + else if (item_type == epoch16_type_ptr) + { + return cdf::to_time_point(*_details::cast(obj)) + .time_since_epoch() + .count(); + } + else + { + throw std::invalid_argument( + "Only supports datetime.datetime, tt2000_t, epoch and epoch16 types"); + } + } + }) + .attr("view")("datetime64[ns]"); +} - auto result = transform( - input, [](const time_t& v) { return cdf::to_time_point(v).time_since_epoch().count(); }); - return py::cast(&result).attr("astype")(dtype); +[[nodiscard]] inline py::object to_datetime(const cdf_time_t auto& input) +{ + return py::reinterpret_steal(_details::to_datetime(cdf::to_time_point(input))); +} + + +[[nodiscard]] inline py::list to_datetime(const py_list_or_py_tuple auto& input) +{ + static auto* tt2000_type_ptr = reinterpret_cast(py::type::of().ptr()); + static auto* epoch_type_ptr = reinterpret_cast(py::type::of().ptr()); + static auto* epoch16_type_ptr = reinterpret_cast(py::type::of().ptr()); + + return _details::ranges::transform(input, + [](const PyObject* const item) -> PyObject* + { + if (PyDateTime_Check(item)) + { + return PyDateTime_FromDateAndTime(PyDateTime_GET_YEAR(item), + PyDateTime_GET_MONTH(item), PyDateTime_GET_DAY(item), + PyDateTime_DATE_GET_HOUR(item), PyDateTime_DATE_GET_MINUTE(item), + PyDateTime_DATE_GET_SECOND(item), PyDateTime_DATE_GET_MICROSECOND(item)); + } + else + { + auto item_type = Py_TYPE(item); + if (item_type == tt2000_type_ptr) + { + return _details::to_datetime(to_time_point(*_details::cast(item))); + } + else if (item_type == epoch_type_ptr) + { + return _details::to_datetime(to_time_point(*_details::cast(item))); + } + else if (item_type == epoch16_type_ptr) + { + return _details::to_datetime(to_time_point(*_details::cast(item))); + } + } + return nullptr; + }); } template -[[nodiscard]] inline std::vector vector_to_datetime( - const std::vector& input) +[[nodiscard]] inline py::list to_datetime(const py::array_t& input) { - std::vector result(std::size(input)); - std::transform(std::cbegin(input), std::cend(input), std::begin(result), - static_cast(to_time_point)); - return result; + return _details::ranges::transform( + input, static_cast(_details::to_datetime)); } -[[nodiscard]] inline py::object var_to_datetime64(const Variable& input) +[[nodiscard]] inline py::list to_datetime(const py::array& input) { + if (!is_dt64ns(input)) + { + throw std::invalid_argument("Only supports datetime64[ns] arrays"); + } + return _details::ranges::transform( + input, static_cast(_details::to_datetime)); +} + +[[nodiscard]] inline py::object to_datetime64(const Variable& input) +{ + auto result = _details::fast_allocate_array(input.shape()); + auto out_ptr = static_cast(result.request(true).ptr); + const auto size = static_cast(result.size()); switch (input.type()) { case cdf::CDF_Types::CDF_EPOCH: { - auto result = transform(input.get().data(), input.shape()[0], - [](const epoch& v) { return cdf::to_time_point(v).time_since_epoch().count(); }); - return py::cast(&result).attr("astype")("datetime64[ns]"); + to_datetime64( + std::span { input.get().data(), size }, out_ptr); } break; case cdf::CDF_Types::CDF_EPOCH16: { - auto result = transform(input.get().data(), - input.shape()[0], - [](const epoch16& v) { return cdf::to_time_point(v).time_since_epoch().count(); }); - return py::cast(&result).attr("astype")("datetime64[ns]"); + to_datetime64( + std::span { input.get().data(), size }, out_ptr); } break; case cdf::CDF_Types::CDF_TIME_TT2000: { - auto result = transform(input.get().data(), - input.shape()[0], - [](const tt2000_t& v) { return cdf::to_time_point(v).time_since_epoch().count(); }); - return py::cast(&result).attr("astype")("datetime64[ns]"); + to_datetime64( + std::span { input.get().data(), size }, out_ptr); } break; default: - throw std::out_of_range("Only supports cdf time types"); + throw std::invalid_argument("Only supports cdf time types"); break; } - return {}; + return result.attr("view")("datetime64[ns]"); } -[[nodiscard]] inline std::vector var_to_datetime( - const Variable& input) +[[nodiscard]] py::list to_datetime(const Variable& input) { switch (input.type()) { case cdf::CDF_Types::CDF_EPOCH: { - using namespace std::chrono; - std::vector result(input.len()); - std::transform(std::cbegin(input.get()), std::cend(input.get()), - std::begin(result), - static_cast(to_time_point)); - return result; + return _details::ranges::transform( + input, static_cast(_details::to_datetime)); } break; case cdf::CDF_Types::CDF_EPOCH16: { - std::vector result(input.len()); - std::transform(std::cbegin(input.get()), std::cend(input.get()), - std::begin(result), - static_cast( - to_time_point)); - return result; + return _details::ranges::transform( + input, static_cast(_details::to_datetime)); } break; case cdf::CDF_Types::CDF_TIME_TT2000: { - std::vector result(input.len()); - std::transform(std::cbegin(input.get()), std::cend(input.get()), - std::begin(result), - static_cast( - to_time_point)); - return result; + return _details::ranges::transform( + input, static_cast(_details::to_datetime)); } break; default: throw std::out_of_range("Only supports cdf time types"); break; } - return {}; + return py::list {}; } -template -void def_time_types_wrapper(T& mod) +template +[[nodiscard]] inline time_t to_cdf_time_t(const PyObject* dt) +{ + return cdf::to_cdf_time(_details::to_tp(reinterpret_cast(dt))); +} + +[[nodiscard]] inline py::list to_tt2000(const py_list_or_py_tuple auto& input) +{ + static auto* tt2000_type_ptr = reinterpret_cast(py::type::of().ptr()); + static auto* epoch_type_ptr = reinterpret_cast(py::type::of().ptr()); + static auto* epoch16_type_ptr = reinterpret_cast(py::type::of().ptr()); + + return _details::ranges::transform(input, + [](const PyObject* const item) -> PyObject* + { + if (PyDateTime_Check(item)) + { + return py::cast(_details::from_datetime(item)).release().ptr(); + } + else + { + auto item_type = Py_TYPE(item); + if (item_type == tt2000_type_ptr) + { + return py::cast(*_details::cast(item)).release().ptr(); + } + else if (item_type == epoch_type_ptr) + { + return py::cast( + to_cdf_time(to_time_point(*_details::cast(item)))) + .release() + .ptr(); + } + else if (item_type == epoch16_type_ptr) + { + return py::cast( + to_cdf_time(to_time_point(*_details::cast(item)))) + .release() + .ptr(); + } + } + return nullptr; + }); +} + + +[[nodiscard]] inline auto to_epoch(const time_point_collection_t auto& tps) +{ + auto result = cdf::to_epoch(tps); + return py::array_t(std::size(result), result.data()); +} + +[[nodiscard]] inline auto to_epoch16( + const no_init_vector& tps) +{ + auto result = cdf::to_epoch16(tps); + return py::array_t(std::size(result), result.data()); +} + + +[[nodiscard]] inline bool _to_cdf_time_t( + const cdf_time_t_span_t auto& input, cdf_time_t auto* output) +{ + for (const auto in: input) + { + *output= cdf::to_cdf_time>(in); + ++output; + } + return true; +} + +template +[[nodiscard]] inline bool to_cdf_time_t(const py::array& input, time_t* const output) +{ + const static auto tt2000_dtype = py::dtype::of(); + const static auto epoch_dtype = py::dtype::of(); + const static auto epoch16_dtype = py::dtype::of(); + if (is_dt64(input)) + { + if (is_dt64ns(input) || input.dtype().is(py::dtype::of()) || input.dtype().is(py::dtype::of())) + { + auto view = _details::ranges::make_span(py::array(input.attr("view")("int64"))); + py::gil_scoped_release release; + cdf::from_ns_from_1970(view, output); + return true; + } + if (is_dt64ms(input)) + { + return to_cdf_time_t( + py::array(input.attr("astype")("datetime64[ns]")), output); + } + } + if (input.dtype().is(tt2000_dtype)) + { + auto view = _details::ranges::make_span(input); + py::gil_scoped_release release; + return _to_cdf_time_t(view, output); + } + if (input.dtype().is(epoch_dtype)) + { + auto view = _details::ranges::make_span(input); + py::gil_scoped_release release; + return _to_cdf_time_t(view, output); + } + if (input.dtype().is(epoch16_dtype)) + { + auto view = _details::ranges::make_span(input); + py::gil_scoped_release release; + return _to_cdf_time_t(view, output); + } + return false; +} + +template +inline py::object to_cdf_time_t(const py::array& input) +{ + + auto res = _details::fast_allocate_array(input); + time_t* out = reinterpret_cast(res.request().ptr); + if (to_cdf_time_t(input, out)) + { + return res; + } + return py::none(); +} + + +void def_time_types_wrapper(auto& mod) { py::class_(mod, "tt2000_t") .def(py::init()) - .def_readwrite("value", &tt2000_t::value) + .def_readwrite("nseconds", &tt2000_t::nseconds) .def(py::self == py::self) .def("__repr__", __repr__); py::class_(mod, "epoch") .def(py::init()) - .def_readwrite("value", &epoch::value) + .def_readwrite("mseconds", &epoch::mseconds) .def(py::self == py::self) .def("__repr__", __repr__); py::class_(mod, "epoch16") @@ -223,62 +522,105 @@ void def_time_types_wrapper(T& mod) .def_readwrite("picoseconds", &epoch16::picoseconds) .def("__repr__", __repr__); - PYBIND11_NUMPY_DTYPE(tt2000_t, value); - PYBIND11_NUMPY_DTYPE(epoch, value); + PYBIND11_NUMPY_DTYPE(tt2000_t, nseconds); + PYBIND11_NUMPY_DTYPE(epoch, mseconds); PYBIND11_NUMPY_DTYPE(epoch16, seconds, picoseconds); } -template -auto def_time_conversion_functions(T& mod) +auto def_to_dt64_conversion_functions(auto& mod) +{ + mod.def("to_datetime64", + static_cast&)>(to_datetime64), + py::arg { "values" }.noconvert()); + mod.def("to_datetime64", + static_cast&)>(to_datetime64), + py::arg { "values" }.noconvert()); + mod.def("to_datetime64", + static_cast&)>(to_datetime64), + py::arg { "values" }.noconvert()); + + mod.def("to_datetime64", static_cast(to_datetime64), + py::arg { "value" }); + mod.def("to_datetime64", static_cast(to_datetime64), + py::arg { "value" }); + mod.def("to_datetime64", static_cast(to_datetime64), + py::arg { "value" }); + + mod.def("to_datetime64", static_cast(to_datetime64), + py::arg { "values" }.noconvert()); + mod.def("to_datetime64", static_cast(to_datetime64), + py::arg { "values" }.noconvert()); + + mod.def("to_datetime64", static_cast(to_datetime64), + py::arg { "variable" }); +} + +auto def_to_datetime_conversion_functions(auto& mod) { + + mod.def("to_datetime", static_cast&)>(to_datetime), + py::arg { "values" }.noconvert()); + mod.def("to_datetime", + static_cast&)>(to_datetime), + py::arg { "values" }.noconvert()); + mod.def("to_datetime", + static_cast&)>(to_datetime), + py::arg { "values" }.noconvert()); + + mod.def("to_datetime", static_cast(to_datetime), + py::arg { "values" }.noconvert()); + mod.def("to_datetime", static_cast(to_datetime), + py::arg { "values" }.noconvert()); + + + mod.def("to_datetime", static_cast(to_datetime)); + + mod.def("to_datetime", static_cast(to_datetime), + py::arg { "value" }); + mod.def("to_datetime", static_cast(to_datetime), + py::arg { "value" }); + mod.def("to_datetime", static_cast(to_datetime), + py::arg { "value" }); + + mod.def("to_datetime", static_cast(to_datetime)); +} + +auto def_time_conversion_functions(auto& mod) +{ + PyDateTime_IMPORT; // forward { - mod.def("to_datetime64", array_to_datetime64, py::arg { "values" }.noconvert()); - mod.def("to_datetime64", array_to_datetime64, py::arg { "values" }.noconvert()); - mod.def("to_datetime64", array_to_datetime64, py::arg { "values" }.noconvert()); - - mod.def("to_datetime64", scalar_to_datetime64, py::arg { "value" }); - mod.def("to_datetime64", scalar_to_datetime64, py::arg { "value" }); - mod.def("to_datetime64", scalar_to_datetime64, py::arg { "value" }); - - mod.def("to_datetime64", vector_to_datetime64, py::arg { "values" }.noconvert()); - mod.def("to_datetime64", vector_to_datetime64, py::arg { "values" }.noconvert()); - mod.def("to_datetime64", vector_to_datetime64, py::arg { "values" }.noconvert()); - - mod.def("to_datetime64", var_to_datetime64, py::arg { "variable" }); - - mod.def("to_datetime", vector_to_datetime); - mod.def("to_datetime", vector_to_datetime); - mod.def("to_datetime", vector_to_datetime); - mod.def("to_datetime", - static_cast(to_time_point)); - mod.def("to_datetime", - static_cast(to_time_point)); - mod.def("to_datetime", - static_cast(to_time_point)); - - mod.def("to_datetime", var_to_datetime); + def_to_dt64_conversion_functions(mod); + def_to_datetime_conversion_functions(mod); } // backward { + mod.def("to_tt2000", static_cast(to_tt2000), + py::arg { "values" }.noconvert()); + mod.def("to_tt2000", static_cast(to_tt2000), + py::arg { "values" }.noconvert()); + mod.def("to_tt2000", [](decltype(std::chrono::system_clock::now()) tp) { return cdf::to_tt2000(tp); }); - mod.def("to_tt2000", - [](const no_init_vector& tps) - { return cdf::to_tt2000(tps); }); + mod.def("to_tt2000", + [](const py::array& input) -> py::object { return to_cdf_time_t(input); }); mod.def("to_epoch", [](decltype(std::chrono::system_clock::now()) tp) { return cdf::to_epoch(tp); }); mod.def("to_epoch", [](const no_init_vector& tps) - { return cdf::to_epoch(tps); }); + { return to_epoch(tps); }); + mod.def("to_epoch", + [](const py::array& input) -> py::object { return to_cdf_time_t(input); }); mod.def("to_epoch16", [](decltype(std::chrono::system_clock::now()) tp) { return cdf::to_epoch16(tp); }); mod.def("to_epoch16", [](const no_init_vector& tps) - { return cdf::to_epoch16(tps); }); + { return to_epoch16(tps); }); + mod.def("to_epoch16", + [](const py::array& input) -> py::object { return to_cdf_time_t(input); }); } } diff --git a/pycdfpp/collections.hpp b/pycdfpp/collections.hpp new file mode 100644 index 0000000..50c671b --- /dev/null +++ b/pycdfpp/collections.hpp @@ -0,0 +1,764 @@ +/*------------------------------------------------------------------------------ +-- The MIT License (MIT) +-- +-- Copyright © 2024, Laboratory of Plasma Physics- CNRS +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy +-- of this software and associated documentation files (the “Software”), to deal +-- in the Software without restriction, including without limitation the rights +-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +-- INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +-- PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +-- HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +-- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +-- SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +-------------------------------------------------------------------------------*/ +/*-- Author : Alexis Jeandet +-- Mail : alexis.jeandet@member.fsf.org +----------------------------------------------------------------------------*/ +#pragma once +#if defined(_MSC_VER) +#pragma warning(push) +#pragma warning(disable : 4127) // warning C4127: Conditional expression is constant +#include +typedef SSIZE_T ssize_t; +#endif + +#include + +#include +#include +#include +#include +using namespace cdf; + +#include +#include +#include +#include + + +namespace py = pybind11; + +template +concept py_list_or_py_tuple + = std::is_same_v, py::list> || std::is_same_v, py::tuple>; + +template +concept py_list = std::is_same_v, py::list>; + + +template +concept np_array + = std::is_same_v, py::array> || std::is_base_of_v>; + +template +concept Vector + = std::is_same_v, no_init_vector::value_type>> + || std::is_same_v, std::vector::value_type>>; + + + +namespace _details +{ + +[[nodiscard]] std::size_t string_length(const PyObject* obj) +{ + Py_ssize_t len = 0; + PyUnicode_AsUTF8AndSize(const_cast(obj), &len); + return static_cast(len); +} + +[[nodiscard]] inline PyObject* underlying_pyobject(const py::object& obj) +{ + return obj.ptr(); +} + +template +[[nodiscard]] inline const T* cast(const PyObject* const o) +{ + return reinterpret_cast( + reinterpret_cast(o)->simple_value_holder[0]); +} + +template +[[nodiscard]] inline std::size_t flat_size(const T& shape) +{ + return std::accumulate( + std::cbegin(shape), std::cend(shape), 1UL, std::multiplies()); +} + +[[nodiscard]] inline std::size_t flat_size(const py::buffer_info& info) +{ + return std::accumulate( + std::cbegin(info.shape), std::cend(info.shape), 1UL, std::multiplies()); +} + +template +[[nodiscard]] inline auto fast_allocate_array(const Shape& shape, Owner&& owner = std::nullptr_t {}) +{ + using value_t = std::remove_const_t; + using alloc_t = default_init_allocator; + + auto ptr = alloc_t().allocate(flat_size(shape)); + + if constexpr (std::is_same_v>) + { + return py::array_t(shape, ptr, + py::capsule(ptr, [](void* p) { alloc_t().deallocate(static_cast(p), 0); })); + } + else + { + return py::array_t(shape, ptr, std::forward(owner)); + } +} + +template +[[nodiscard]] inline auto fast_allocate_array(const py::buffer_info& info) +{ + return fast_allocate_array(info.shape); +} + +template +[[nodiscard]] inline auto fast_allocate_array(const py::array& ref) +{ + return fast_allocate_array(std::span(ref.shape(), static_cast(ref.ndim()))); +} + +[[nodiscard]] std::vector shape_ssize_t(const Variable& var) +{ + const auto& shape = var.shape(); + std::vector res(std::size(shape)); + std::transform(std::cbegin(shape), std::cend(shape), std::begin(res), + [](auto v) { return static_cast(v); }); + return res; +} + +[[nodiscard]] std::vector str_shape_ssize_t(const Variable& var) +{ + const auto& shape = var.shape(); + std::vector res(std::size(shape) - 1); + std::transform(std::cbegin(shape), std::cend(shape) - 1, std::begin(res), + [](auto v) { return static_cast(v); }); + return res; +} + +template +[[nodiscard]] std::vector strides(const Variable& var) +{ + const auto& shape = var.shape(); + std::vector res(std::size(shape)); + std::transform(std::crbegin(shape), std::crend(shape), std::begin(res), + [next = sizeof(T)](auto v) mutable + { + auto res = next; + next = static_cast(v * next); + return res; + }); + std::reverse(std::begin(res), std::end(res)); + return res; +} + +[[nodiscard]] constexpr bool are_compatible_shapes(const Variable& dest, const auto& source) +{ + const auto dest_size = std::size(dest.shape()); + const auto source_size = std::size(source); + // means dest is uninitialized so it can take any shape + if (dest_size == 0) + { + return true; + } + if (dest.is_nrv() == false) + { + if (dest_size != source_size) + { + return false; + } + // First dimension can be different (record variance) + if (dest_size >=2) + { + for (auto i = 1UL; i < dest_size; i++) + { + if (dest.shape()[i] != source[i]) + { + return false; + } + } + } + return true; + } + else + { + // NRV variable + if ((dest_size -1 != source_size) && (dest_size != source_size)) + { + return false; + } + const auto source_offset = dest_size - source_size; + if (dest_size >=2) + { + for (auto i = 1UL; i < source_size; i++) + { + if (dest.shape()[i] != source[i - source_offset]) + { + return false; + } + } + } + return true; + } + return false; +} + +template +[[nodiscard]] std::vector str_strides(const Variable& var) +{ + const auto& shape = var.shape(); + std::vector res(std::size(shape) - 1); + std::transform(std::crbegin(shape) + 1, std::crend(shape), std::begin(res), + [next = shape.back()](auto v) mutable + { + auto res = next; + next = static_cast(v * next); + return res; + }); + std::reverse(std::begin(res), std::end(res)); + return res; +} + +template +[[nodiscard]] py::array make_array(Variable& variable, py::object& obj) +{ + // static_assert(data_t != CDF_Types::CDF_CHAR and data_t != CDF_Types::CDF_UCHAR); + from_cdf_type_t* ptr = nullptr; + { + py::gil_scoped_release release; + ptr = variable.get>().data(); + } + return py::array_t>( + shape_ssize_t(variable), strides>(variable), ptr, obj); +} + +template +[[nodiscard]] static inline std::string_view make_string_view(T* data, size_type len) +{ + return std::string_view( + reinterpret_cast(data), static_cast(len)); +} + +template +[[nodiscard]] py::object make_list( + const T* data, decltype(std::declval().shape()) shape, py::object& obj) +{ + if (std::size(shape) > 2) + { + py::list res {}; + std::size_t offset = 0; + auto inner_shape = decltype(shape) { std::begin(shape) + 1, std::end(shape) }; + std::size_t jump = std::accumulate( + std::cbegin(inner_shape), std::cend(inner_shape), 1UL, std::multiplies()); + for (auto i = 0UL; i < shape[0]; i++) + { + res.append(make_list(data + offset, inner_shape, obj)); + offset += jump; + } + return res; + } + if (std::size(shape) == 2) + { + py::list res {}; + std::size_t offset = 0; + for (auto i = 0UL; i < shape[0]; i++) + { + res.append(make_string_view(data + offset, shape[1])); + offset += shape[1]; + } + return res; + } + if (std::size(shape) == 1) + { + return py::str(make_string_view(data, shape[0])); + } + return py::none(); +} + +template +[[nodiscard]] py::object make_list(Variable& variable, py::object& obj) +{ + static_assert(data_t == CDF_Types::CDF_CHAR or data_t == CDF_Types::CDF_UCHAR); + return make_list(variable.get>().data(), variable.shape(), obj); +} + +template +[[nodiscard]] py::buffer_info impl_make_buffer(cdf::Variable& var) +{ + using U = cdf::from_cdf_type_t; + char* ptr = nullptr; + { + py::gil_scoped_release release; + ptr = var.bytes_ptr(); + } + if constexpr ((T == CDF_Types::CDF_CHAR) or (T == CDF_Types::CDF_UCHAR)) + { + return py::buffer_info(ptr, /* Pointer to buffer */ + var.shape().back(), /* Size of one scalar */ + fmt::format("{}s", var.shape().back()), + static_cast(std::size(var.shape()) - 1), /* Number of dimensions */ + str_shape_ssize_t(var), str_strides(var), true); + } + else + { + return py::buffer_info(ptr, /* Pointer to buffer */ + sizeof(U), /* Size of one scalar */ + py::format_descriptor::format(), + static_cast(std::size(var.shape())), /* Number of dimensions */ + shape_ssize_t(var), strides(var), true); + } +} + +template +[[nodiscard]] py::object make_str_array(py::object& obj) +{ + py::module_ np = py::module_::import("numpy"); + if constexpr (encode_strings) + { + return np.attr("char").attr("decode")(py::memoryview(obj)); + } + else + { + return np.attr("array")(py::memoryview(obj)); + } +} + +template +[[nodiscard]] inline PyObject* get_item(const PyObject* const p, std::size_t pos) +{ + if constexpr (std::is_same_v) + { + return PyList_GET_ITEM(p, static_cast(pos)); + } + else if constexpr (std::is_same_v) + { + return PyTuple_GET_ITEM(p, static_cast(pos)); + } +} + + +namespace ranges +{ + + template + [[nodiscard]] + auto make_span(const np_array auto& arr) + { + py::buffer_info info = arr.request(); + return std::span( + static_cast(info.ptr), static_cast(info.size)); + } + + struct py_list_set_iterator + { + using iterator_category = std::output_iterator_tag; + using value_type = void; + using difference_type = std::ptrdiff_t; + + PyObject* list; + std::size_t index; + + inline py_list_set_iterator& operator=(PyObject* r) + { + if (r) + { + PyList_SET_ITEM(list, index, r); + } + return *this; + } + + inline py_list_set_iterator& operator*() { return *this; } + inline py_list_set_iterator& operator++() + { + ++index; + return *this; + } + inline py_list_set_iterator operator++(int) + { + auto temp = *this; + ++index; + return temp; + } + }; + + struct py_stealing_raw_sink + { + PyObject* list_ptr; + + struct proxy + { + PyObject* list; + void operator=(PyObject* r) + { + if (r) + { + PyList_Append(list, r); + Py_DECREF(r); + } + } + + void operator=(py::object r) + { + if (r) + { + PyList_Append(list, r.ptr()); + } + } + }; + + proxy operator*() { return { list_ptr }; } + py_stealing_raw_sink& operator++() { return *this; } + py_stealing_raw_sink& operator++(int) { return *this; } + }; + + template + struct py_list_or_tuple_view : std::ranges::view_interface> + { + struct _iterator + { + private: + PyObject* _collection; + std::size_t _index; + + public: + using value_type = const PyObject*; + using difference_type = std::ptrdiff_t; + using iterator_concept = std::random_access_iterator_tag; + + _iterator(PyObject* collection, std::size_t index) + : _collection(collection), _index(index) + { + } + + const PyObject* operator*() const { return get_item(_collection, _index); } + + _iterator& operator++() + { + ++_index; + return *this; + } + + _iterator& operator--() + { + --_index; + return *this; + } + + _iterator operator+(difference_type n) const { return { _collection, _index + n }; } + + _iterator operator-(difference_type n) const { return { _collection, _index - n }; } + + difference_type operator-(const _iterator& other) const + { + return _index - other._index; + } + + auto operator<=>(const _iterator& other) const = default; + + bool operator!=(const _iterator& other) const + { + return _index != other._index || _collection != other._collection; + } + }; + + py_list_or_tuple_view(const T& c) : _collection(underlying_pyobject(c)), _size(py::len(c)) + { + } + + auto begin() const { return _iterator { _collection, _start_offset }; } + auto end() const { return _iterator { _collection, _size - _end_offset }; } + + std::size_t size() const { return _size - _start_offset - _end_offset; } + + py_list_or_tuple_view subview(std::size_t start, std::size_t end) const + { + py_list_or_tuple_view subview = *this; + subview._start_offset += start; + subview._end_offset += (size() - end); + return subview; + } + + py_list_or_tuple_view subview(std::size_t start) const + { + py_list_or_tuple_view subview = *this; + subview._start_offset += start; + return subview; + } + + private: + std::size_t _start_offset = 0; + std::size_t _end_offset = 0; + PyObject* _collection; + std::size_t _size; + }; + + + template > + [[nodiscard]] inline shape_t _shape(const py_list_or_py_tuple auto& input, shape_t&& shape = {}) + { + auto view = py_list_or_tuple_view { input }; + shape.push_back(py::len(input)); + if (py::len(input) > 0) + { + const PyObject* first = *view.begin(); + if (PyList_Check(first) || PyTuple_Check(first)) + { + return _shape(py::reinterpret_borrow(const_cast(first)), + std::move(shape)); + } + else if (PyUnicode_Check(first)) + { + shape.push_back(static_cast(string_length(first))); + } + } + return shape; + } + + + template + [[nodiscard]] + inline T* _transform_inner( + const py_list_or_py_tuple auto& input, const auto& f, T* res_ptr, const auto& shape_span) + { + if (py::len(input) != shape_span[0]) + { + throw std::out_of_range("Inconsistent shapes in nested lists/tuples"); + } + auto view = py_list_or_tuple_view { input }; + for (const PyObject* obj : view) + { + if (PyList_Check(obj)) + { + if (shape_span.size() < 2) + { + throw std::out_of_range("Inconsistent shapes in nested lists/tuples"); + } + res_ptr = _transform_inner( + py::reinterpret_borrow(const_cast(obj)), f, res_ptr, + shape_span.subspan(1)); + } + else + { + *res_ptr = f(obj); + ++res_ptr; + } + } + return res_ptr; + } + + template > + [[nodiscard]] inline std::pair transform( + const py_list_or_py_tuple auto& input, const shape_t& shape, const auto& f) + { + using value_type = typename output_t::value_type; + auto result = output_t(_details::flat_size(shape)); + auto r = _transform_inner(input, f, result.data(), std::span(shape)); + assert(r == result.data() + result.size()); + return { result, shape }; + } + + template > + [[nodiscard]] inline std::pair transform( + const py_list_or_py_tuple auto& input, const auto& f) + { + auto shape = _shape(input); + return transform(input, shape, f); + } + + template + [[nodiscard]] + inline T* _string_transform_inner(const py_list_or_py_tuple auto& input, const auto& f, + T* res_ptr, const auto& shape_span, const std::size_t str_len) + { + if (py::len(input) != shape_span[0]) + { + throw std::out_of_range("Inconsistent shapes in nested lists/tuples"); + } + auto view = py_list_or_tuple_view { input }; + for (const PyObject* obj : view) + { + if (PyList_Check(obj)) + { + if (shape_span.size() < 2) + { + throw std::out_of_range("Inconsistent shapes in nested lists/tuples"); + } + res_ptr = _string_transform_inner( + py::reinterpret_borrow(const_cast(obj)), f, res_ptr, + shape_span.subspan(1), str_len); + } + else + { + f(obj, std::span(res_ptr, str_len)); + res_ptr += str_len; + } + } + return res_ptr; + } + + template > + [[nodiscard]] inline std::pair string_transform( + const py_list_or_py_tuple auto& input, const shape_t& shape, const auto& f) + { + using value_type = typename output_t::value_type; + auto result = output_t(_details::flat_size(shape)); + auto r = _string_transform_inner( + input, f, result.data(), std::span(shape.data(), shape.size() - 1), shape.back()); + assert(r == result.data() + result.size()); + return { result, shape }; + } + + template > + [[nodiscard]] inline std::pair string_transform( + const py_list_or_py_tuple auto& input, const auto& f) + { + auto shape = _shape(input); + return string_transform(input, shape, f); + } + + template + [[nodiscard]] inline py::object transform(const py_list_or_py_tuple auto& input, const auto& f) + { + using value_type = typename output_t::value_type; + auto shape = _shape(input); + auto result = fast_allocate_array(shape); + py::buffer_info res_buff = result.request(true); + value_type* res_ptr = static_cast(res_buff.ptr); + res_ptr = _transform_inner(input, f, res_ptr, std::span(shape)); + return result; + } + + template + [[nodiscard]] inline py::object transform(const py::array& input, const auto& f) + { + py::buffer_info in_buff = input.request(); + auto result = _details::fast_allocate_array(in_buff); + in_value_t* in_ptr = static_cast(in_buff.ptr); + f(std::span(in_ptr, result.size()), static_cast(result.request(true).ptr)); + return result; + } + + template + [[nodiscard]] inline py::object transform( + const auto* input, const auto& shape_span, const auto& f) + { + py::list result; + auto out = py_stealing_raw_sink { _details::underlying_pyobject(result) }; + const auto flat_sz = _details::flat_size(shape_span); + for (std::size_t i = 0; i < static_cast(shape_span[0]); ++i) + { + if (shape_span.size() > 1) + { + *out = transform(input + i * flat_sz, shape_span.subspan(1), f); + } + else + { + if (auto r = f(input[i])) + { + *out = r; + } + else + { + throw std::out_of_range("Conversion failed"); + } + } + } + return result; + } + + template + [[nodiscard]] inline py::object transform(const Variable& v, const auto& f) + { + return transform(v.get().data(), std::span(v.shape()), f); + } + + template + [[nodiscard]] inline py::object transform(const py::array& input, const auto& f) + { + auto view = [&input]() constexpr + { + if constexpr (helpers::is_any_of_v) + { + return py::array(input.attr("view")("int64")); + } + else + { + return input; + } + }(); + auto buffer = view.request(); + value_t* in_ptr = reinterpret_cast(buffer.ptr); + return transform( + in_ptr, std::span(input.shape(), static_cast(input.ndim())), f); + } + + template + [[nodiscard]] inline py::object transform(const py_list_or_py_tuple auto& input, const auto& f) + { + using namespace _details::ranges; + py::list result; + auto view = py_list_or_tuple_view(input); + auto out = py_stealing_raw_sink { _details::underlying_pyobject(result) }; + for (const PyObject* obj : view) + { + if (auto r = f(obj)) + { + *out = r; + } + else if (PyList_Check(obj)) + { + *out = transform( + py::reinterpret_borrow(const_cast(obj)), f); + } + else if (PyTuple_Check(obj)) + { + *out = transform( + py::reinterpret_borrow(const_cast(obj)), f); + } + else + { + throw std::out_of_range( + "Only supports datetime.datetime, tt2000_t, epoch and epoch16 types"); + } + } + return result; + } + +} + +} + +template +[[nodiscard]] py::object make_values_view(py::object& obj) +{ + Variable& variable = obj.cast(); + return cdf_type_dispatch( + variable.type(), [&variable](py::object& o) -> py::object { + if constexpr (T == CDF_Types::CDF_CHAR || T == CDF_Types::CDF_UCHAR) + { + return _details::make_str_array(o); + } + return _details::make_array(variable, o); + }, obj); +} + +[[nodiscard]] py::buffer_info make_buffer(cdf::Variable& variable) +{ + return cdf_type_dispatch( + variable.type(), [](cdf::Variable& var) + { return _details::impl_make_buffer(var); }, variable); +} diff --git a/pycdfpp/data_types.hpp b/pycdfpp/data_types.hpp new file mode 100644 index 0000000..ee75fb9 --- /dev/null +++ b/pycdfpp/data_types.hpp @@ -0,0 +1,502 @@ +/*------------------------------------------------------------------------------ +-- The MIT License (MIT) +-- +-- Copyright © 2026, Laboratory of Plasma Physics- CNRS +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy +-- of this software and associated documentation files (the “Software”), to deal +-- in the Software without restriction, including without limitation the rights +-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +-- INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +-- PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +-- HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +-- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +-- SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +-------------------------------------------------------------------------------*/ +/*-- Author : Alexis Jeandet +-- Mail : alexis.jeandet@member.fsf.org +----------------------------------------------------------------------------*/ +#pragma once + +#include "collections.hpp" + +#include +#include +#include +#include + +#include +#include +#include +using namespace cdf; + + +[[nodiscard]] inline bool is_dt64ns(const py::array& arr) +{ + if (arr.dtype().kind() != 'M') + return false; + return helpers::contains(std::string(arr.dtype().attr("str").cast()), "[ns]"); +} + +[[nodiscard]] inline bool is_dt64ms(const py::array& arr) +{ + if (arr.dtype().kind() != 'M') + return false; + return helpers::contains(std::string(arr.dtype().attr("str").cast()), "[ms]"); +} + +[[nodiscard]] inline bool is_dt64(const py::array& arr) +{ + return arr.dtype().kind() == 'M'; +} + + +enum class BestTypeId : uint16_t +{ + None = 0, + + // Category Masks (High Bits) + // xxxx 01xx xxxx = Unsigned + // xxxx 10xx xxxx = Signed + // xx11 xxxx xxxx = Float + // xxxx 0001 xxxx = String + // xxxx 0010 xxxx = DateTime + + // Numeric Values (Ranked by power of 2 so OR-ing picks the largest width) + NumericMask = 0x3C0, // 11 1100 0000 + UInt8 = 0x41, // 0100 0001 + UInt16 = 0x42, // 0100 0010 + UInt32 = 0x44, // 0100 0100 + UInt64 = 0x48, // 0100 1000 + + Int8 = 0x81, // 1000 0001 + Int16 = 0x82, // 1000 0010 + Int32 = 0x84, // 1000 0100 + Int64 = 0x88, // 1000 1000 + + Float = 0x300, // 1100 0000 (Force Float to win over Int/UInt) + + String = 0x10, // 0001 0000 + DateTime = 0x20, // 0010 0000 + + Collection = 0x8000 // 1000 0000 0000 0000 +}; + +[[nodiscard]] constexpr BestTypeId operator|(BestTypeId lhs, BestTypeId rhs) +{ + using T = std::underlying_type_t; + return static_cast(static_cast(lhs) | static_cast(rhs)); +} + +[[nodiscard]] constexpr BestTypeId operator&(BestTypeId lhs, BestTypeId rhs) +{ + using T = std::underlying_type_t; + return static_cast(static_cast(lhs) & static_cast(rhs)); +} + +[[nodiscard]] constexpr BestTypeId& operator|=(BestTypeId& lhs, BestTypeId rhs) +{ + lhs = lhs | rhs; + return lhs; +} + +[[nodiscard]] constexpr bool contains(BestTypeId t, BestTypeId flag) +{ + return (t & flag) == flag; +} + +[[nodiscard]] constexpr bool has_string(BestTypeId t) +{ + return contains(t, BestTypeId::String); +} + +[[nodiscard]] constexpr bool has_numerical_value(BestTypeId t) +{ + return (t & BestTypeId::NumericMask) != BestTypeId::None; +} + +[[nodiscard]] constexpr bool has_datetime(BestTypeId t) +{ + return contains(t, BestTypeId::DateTime); +} + +[[nodiscard]] constexpr bool has_collection(BestTypeId t) +{ + return contains(t, BestTypeId::Collection); +} + +[[nodiscard]] constexpr bool is_invalid_mix(BestTypeId t) +{ + return (has_string(t) + has_numerical_value(t) + has_datetime(t) + has_collection(t)) > 1; +} + +[[nodiscard]] constexpr BestTypeId _merge_types(BestTypeId a, BestTypeId b) +{ + auto r = a | b; + if (is_invalid_mix(r)) + { + throw std::out_of_range("Incompatible types in nested lists/tuples"); + } + return r; +} + +struct BestType +{ + BestTypeId inferred_type = BestTypeId::None; + int64_t max_int_value = 0; + int64_t min_int_value = 0; + std::size_t string_length = 0; + + constexpr BestType& operator|=(const BestType& other) + { + auto _inferred_type = _merge_types(inferred_type, other.inferred_type); + this->max_int_value = std::max(this->max_int_value, other.max_int_value); + this->min_int_value = std::min(this->min_int_value, other.min_int_value); + this->string_length = std::max(other.string_length, this->string_length); + this->inferred_type = _inferred_type; + return *this; + } +}; + +template +[[nodiscard]] constexpr bool fits(auto min, auto max) +{ + return (min >= static_cast(std::numeric_limits::min())) + && (max <= static_cast(std::numeric_limits::max())); +} + +struct analyze_result +{ + Variable::shape_t shape; + BestType inferred_type = {}; + CDF_Types inferred_cdf_type = CDF_Types::CDF_NONE; + bool is_empty = false; +}; + +struct analyze_context +{ + std::optional inner_collection_size = std::nullopt; + BestType inferred_type = {}; +}; + +[[nodiscard]] inline BestType _min_cdf_int(PyObject* value) +{ + // CDF format only supports up to signed 64 bits integers and unsigned 32 bits integers + int64_t val = PyLong_AsLongLong(const_cast(value)); + return { BestTypeId::Int64, val, val, 0 }; +} + +[[nodiscard]] inline BestType _best_match_type(PyObject* obj) +{ + assert(obj != nullptr); + if (PyLong_Check(obj)) + { + return _min_cdf_int(obj); + } + else if (PyFloat_Check(obj)) + { + return { BestTypeId::Float, 0, 0, 0 }; + } + else if (PyUnicode_Check(obj)) + { + return { BestTypeId::String, 0, 0, _details::string_length(obj) }; + } + else if (PyDateTime_Check(obj)) + { + return { BestTypeId::DateTime, 0, 0, 0 }; + } + throw std::runtime_error(fmt::format("Unsupported data type encountered")); + return { BestTypeId::None, 0, 0, 0 }; +} + +[[nodiscard]] constexpr BestTypeId to_best_type_id(const CDF_Types& t) +{ + switch (t) + { + case CDF_Types::CDF_UCHAR: + case CDF_Types::CDF_CHAR: + return BestTypeId::String; + case CDF_Types::CDF_TIME_TT2000: + return BestTypeId::DateTime; + case CDF_Types::CDF_DOUBLE: + case CDF_Types::CDF_FLOAT: + return BestTypeId::Float; + case CDF_Types::CDF_INT1: + return BestTypeId::Int8; + case CDF_Types::CDF_INT2: + return BestTypeId::Int16; + case CDF_Types::CDF_INT4: + return BestTypeId::Int32; + case CDF_Types::CDF_INT8: + return BestTypeId::Int64; + case CDF_Types::CDF_UINT1: + return BestTypeId::UInt8; + case CDF_Types::CDF_UINT2: + return BestTypeId::UInt16; + case CDF_Types::CDF_UINT4: + return BestTypeId::UInt32; + default: + return BestTypeId::None; + } +} + +[[nodiscard]] constexpr bool are_compatible_types(const CDF_Types& dest, const CDF_Types& source) +{ + switch (dest) + { + case CDF_Types::CDF_UCHAR: + case CDF_Types::CDF_CHAR: + return (source == CDF_Types::CDF_UCHAR) || (source == CDF_Types::CDF_CHAR); + case CDF_Types::CDF_TIME_TT2000: + case CDF_Types::CDF_EPOCH: + case CDF_Types::CDF_EPOCH16: + return (source == CDF_Types::CDF_TIME_TT2000) || (source == CDF_Types::CDF_EPOCH) + || (source == CDF_Types::CDF_EPOCH16); + case CDF_Types::CDF_DOUBLE: + case CDF_Types::CDF_REAL8: + return helpers::rt_is_in(source, CDF_Types::CDF_DOUBLE, CDF_Types::CDF_REAL8, + CDF_Types::CDF_FLOAT, CDF_Types::CDF_INT1, CDF_Types::CDF_INT2, CDF_Types::CDF_INT4, + CDF_Types::CDF_UINT1, CDF_Types::CDF_UINT2, CDF_Types::CDF_UINT4); + case CDF_Types::CDF_FLOAT: + case CDF_Types::CDF_REAL4: + return helpers::rt_is_in(source, CDF_Types::CDF_FLOAT, CDF_Types::CDF_REAL4, + CDF_Types::CDF_INT1, CDF_Types::CDF_INT2, CDF_Types::CDF_UINT1, + CDF_Types::CDF_UINT2); + case CDF_Types::CDF_INT8: + return helpers::rt_is_in(source, CDF_Types::CDF_INT8, CDF_Types::CDF_INT4, + CDF_Types::CDF_INT2, CDF_Types::CDF_INT1, CDF_Types::CDF_UINT4, + CDF_Types::CDF_UINT2, CDF_Types::CDF_UINT1); + case CDF_Types::CDF_INT4: + return helpers::rt_is_in(source, CDF_Types::CDF_INT4, CDF_Types::CDF_INT2, + CDF_Types::CDF_INT1, CDF_Types::CDF_UINT2, CDF_Types::CDF_UINT1); + case CDF_Types::CDF_INT2: + return helpers::rt_is_in( + source, CDF_Types::CDF_INT2, CDF_Types::CDF_INT1, CDF_Types::CDF_UINT1); + case CDF_Types::CDF_INT1: + case CDF_Types::CDF_BYTE: + return source == CDF_Types::CDF_INT1; + case CDF_Types::CDF_UINT4: + return helpers::rt_is_in( + source, CDF_Types::CDF_UINT4, CDF_Types::CDF_UINT2, CDF_Types::CDF_UINT1); + case CDF_Types::CDF_UINT2: + return helpers::rt_is_in(source, CDF_Types::CDF_UINT2, CDF_Types::CDF_UINT1); + case CDF_Types::CDF_UINT1: + return source == CDF_Types::CDF_UINT1; + case CDF_Types::CDF_NONE: + return true; + default: + break; + } + return false; +} + + +[[nodiscard]] constexpr CDF_Types to_cdf_type(const BestType& t) +{ + if (has_string(t.inferred_type)) + { + return CDF_Types::CDF_UCHAR; + } + else if (has_datetime(t.inferred_type)) + { + return CDF_Types::CDF_TIME_TT2000; + } + else if (has_numerical_value(t.inferred_type)) + { + if (contains(t.inferred_type, BestTypeId::Float)) + { + return CDF_Types::CDF_DOUBLE; + } + else + { + if (t.min_int_value < 0) + { + if (fits(t.min_int_value, t.max_int_value)) + { + return CDF_Types::CDF_INT1; + } + else if (fits(t.min_int_value, t.max_int_value)) + { + return CDF_Types::CDF_INT2; + } + else if (fits(t.min_int_value, t.max_int_value)) + { + return CDF_Types::CDF_INT4; + } + else + { + return CDF_Types::CDF_INT8; + } + } + else + { + if (fits(t.min_int_value, t.max_int_value)) + { + return CDF_Types::CDF_UINT1; + } + else if (fits(t.min_int_value, t.max_int_value)) + { + return CDF_Types::CDF_UINT2; + } + else if (fits(t.min_int_value, t.max_int_value)) + { + return CDF_Types::CDF_UINT4; + } + else + { + // Fallback to signed 64 bits integer since CDF has no uint64 type + return CDF_Types::CDF_INT8; + } + } + } + } + return CDF_Types::CDF_NONE; +} + +inline void _analyze_collection_impl( + const py_list_or_py_tuple auto& input, analyze_result& result, std::size_t depth = 0) +{ + auto view = _details::ranges::py_list_or_tuple_view { input }; + if (py::len(input) > 0) + { + if (depth >= result.shape.size()) + { + result.shape.push_back(std::size(view)); + } + analyze_context ctx; + for (const PyObject* obj : view) + { + if (PyList_Check(obj)) + { + ctx.inferred_type |= BestType { BestTypeId::Collection, 0, 0, 0 }; + std::size_t curr_inner_size + = static_cast(PyList_Size(const_cast(obj))); + if (ctx.inner_collection_size && (curr_inner_size != *ctx.inner_collection_size)) + { + throw std::out_of_range("Inconsistent shapes in nested lists/tuples"); + } + else + { + ctx.inner_collection_size = curr_inner_size; + } + _analyze_collection_impl( + py::reinterpret_borrow(const_cast(obj)), result, + depth + 1); + } + else if (PyTuple_Check(obj)) + { + ctx.inferred_type |= BestType { BestTypeId::Collection, 0, 0, 0 }; + std::size_t curr_inner_size + = static_cast(PyTuple_Size(const_cast(obj))); + if (ctx.inner_collection_size && (curr_inner_size != *ctx.inner_collection_size)) + { + throw std::out_of_range("Inconsistent shapes in nested lists/tuples"); + } + else + { + ctx.inner_collection_size = curr_inner_size; + } + _analyze_collection_impl( + py::reinterpret_borrow(const_cast(obj)), result, + depth + 1); + } + else + { + ctx.inferred_type |= _best_match_type(const_cast(obj)); + } + } + if (!has_collection(ctx.inferred_type.inferred_type)) + result.inferred_type |= ctx.inferred_type; + } +} + +[[nodiscard]] inline analyze_result analyze_collection(const py_list_or_py_tuple auto& input) +{ + auto result = analyze_result {}; + _analyze_collection_impl(input, result); + result.is_empty = py::len(input) == 0; + if (has_string(result.inferred_type.inferred_type)) + result.shape.push_back(result.inferred_type.string_length); + result.inferred_cdf_type = to_cdf_type(result.inferred_type); + return result; +} + +[[nodiscard]] inline CDF_Types to_cdf_type(const py::array& arr) +{ + if (is_dt64(arr)) + { + return CDF_Types::CDF_TIME_TT2000; + } + switch (arr.dtype().kind()) + { + case 'i': + { + switch (arr.dtype().itemsize()) + { + case 1: + return CDF_Types::CDF_INT1; + case 2: + return CDF_Types::CDF_INT2; + case 4: + return CDF_Types::CDF_INT4; + case 8: + return CDF_Types::CDF_INT8; + default: + break; + } + break; + } + case 'u': + { + switch (arr.dtype().itemsize()) + { + case 1: + return CDF_Types::CDF_UINT1; + case 2: + return CDF_Types::CDF_UINT2; + case 4: + return CDF_Types::CDF_UINT4; + case 8: + // Fallback to signed 64 bits integer since CDF has no uint64 type + return CDF_Types::CDF_INT8; + default: + break; + } + break; + } + case 'f': + { + switch (arr.dtype().itemsize()) + { + case 4: + return CDF_Types::CDF_FLOAT; + case 8: + return CDF_Types::CDF_DOUBLE; + default: + break; + } + break; + } + case 'U': + case 'S': + return CDF_Types::CDF_UCHAR; + default: + break; + } + return CDF_Types::CDF_NONE; +} + +[[nodiscard]] inline analyze_result analyze_collection(const py::array& input) +{ + auto result = analyze_result {}; + result.shape = Variable::shape_t { input.shape(), input.shape() + input.ndim() }; + result.inferred_cdf_type = to_cdf_type(input); + return result; +} diff --git a/pycdfpp/enums.hpp b/pycdfpp/enums.hpp index 28e1152..bf0d770 100644 --- a/pycdfpp/enums.hpp +++ b/pycdfpp/enums.hpp @@ -27,6 +27,7 @@ #include using namespace cdf; +#include #include namespace py = pybind11; @@ -35,11 +36,13 @@ namespace py = pybind11; template void def_enums_wrappers(T& mod) { - py::enum_(mod, "Majority") + py::native_enum(mod, "Majority", "enum.Enum") .value("row", cdf_majority::row) - .value("column", cdf_majority::column); + .value("column", cdf_majority::column) + .export_values() + .finalize(); - py::enum_(mod, "CompressionType") + py::native_enum(mod, "CompressionType", "enum.Enum") .value("no_compression", cdf_compression_type::no_compression) .value("gzip_compression", cdf_compression_type::gzip_compression) .value("rle_compression", cdf_compression_type::rle_compression) @@ -48,9 +51,10 @@ void def_enums_wrappers(T& mod) #ifdef CDFPP_USE_ZSTD .value("zstd_compression", cdf_compression_type::zstd_compression) #endif - ; + .export_values() + .finalize(); - py::enum_(mod, "DataType") + py::native_enum(mod, "DataType", "enum.Enum") .value("CDF_BYTE", CDF_Types::CDF_BYTE) .value("CDF_CHAR", CDF_Types::CDF_CHAR) .value("CDF_INT1", CDF_Types::CDF_INT1) @@ -68,5 +72,7 @@ void def_enums_wrappers(T& mod) .value("CDF_UINT4", CDF_Types::CDF_UINT4) .value("CDF_DOUBLE", CDF_Types::CDF_DOUBLE) .value("CDF_EPOCH16", CDF_Types::CDF_EPOCH16) - .value("CDF_TIME_TT2000", CDF_Types::CDF_TIME_TT2000); + .value("CDF_TIME_TT2000", CDF_Types::CDF_TIME_TT2000) + .export_values() + .finalize(); } diff --git a/pycdfpp/meson.build b/pycdfpp/meson.build index 7781839..2e6496c 100644 --- a/pycdfpp/meson.build +++ b/pycdfpp/meson.build @@ -11,7 +11,6 @@ else pycdfpp_cpp_args = [] endif - _pycdfpp = python3.extension_module('_pycdfpp', 'pycdfpp.cpp', dependencies: [cdfpp_dep, fmt_dep, pybind11_dep,python3.dependency()], cpp_args: pycdfpp_cpp_args, diff --git a/pycdfpp/variable.hpp b/pycdfpp/variable.hpp index 12af5aa..a159dd2 100644 --- a/pycdfpp/variable.hpp +++ b/pycdfpp/variable.hpp @@ -25,7 +25,9 @@ ----------------------------------------------------------------------------*/ #pragma once #include "attribute.hpp" -#include "buffers.hpp" +#include "chrono.hpp" +#include "collections.hpp" +#include "data_types.hpp" #include "repr.hpp" #include @@ -45,6 +47,7 @@ using namespace cdf; #include #include #include +#include namespace docstrings { @@ -87,6 +90,71 @@ set_values namespace py = pybind11; +template +auto to_numerical(const PyObject* o) +{ + if constexpr (std::is_same_v) + { + if (PyFloat_Check(const_cast(o))) + { + return static_cast(PyFloat_AS_DOUBLE(const_cast(o))); + } + else if (PyLong_Check(const_cast(o))) + { + return static_cast(PyLong_AsDouble(const_cast(o))); + } + else + { + throw std::invalid_argument { "Incompatible python and cdf types" }; + } + } + if constexpr (helpers::is_any_of_v) + { + if (PyLong_Check(const_cast(o))) + { + return static_cast(PyLong_AsUnsignedLongLong(const_cast(o))); + } + else if (PyFloat_Check(const_cast(o))) + { + return static_cast(PyFloat_AsDouble(const_cast(o))); + } + else + { + throw std::invalid_argument { "Incompatible python and cdf types" }; + } + } + else if constexpr (helpers::is_any_of_v) + { + if (PyLong_Check(const_cast(o))) + { + return static_cast(PyLong_AsLongLong(const_cast(o))); + } + else if (PyFloat_Check(const_cast(o))) + { + return static_cast(PyFloat_AS_DOUBLE(const_cast(o))); + } + else + { + throw std::invalid_argument { "Incompatible python and cdf types" }; + } + } + else if constexpr (std::is_same_v) + { + if (PyFloat_Check(const_cast(o))) + { + return static_cast(PyFloat_AS_DOUBLE(const_cast(o))); + } + else if (PyLong_Check(const_cast(o))) + { + return static_cast(PyLong_AsDouble(const_cast(o))); + } + else + { + throw std::invalid_argument { "Incompatible python and cdf types" }; + } + } +} + template std::pair _numeric_to_nd_data_t(const py::buffer& buffer) { @@ -98,6 +166,10 @@ std::pair _numeric_to_nd_data_t(const py::bu std::copy(std::cbegin(info.shape), std::cend(info.shape), std::begin(shape)); if (info.size != 0) { + /* + * We could later imagine to avoid this copy by directly using the buffer memory + * this would require to tie the buffer lifetime to the variable lifetime + */ no_init_vector values(info.size); std::memcpy(values.data(), info.ptr, info.size * sizeof(T)); return { data_t { std::move(values), data_type }, std::move(shape) }; @@ -108,6 +180,18 @@ std::pair _numeric_to_nd_data_t(const py::bu } } + +template +std::pair _numeric_to_nd_data_t( + const py_list_or_py_tuple auto& values) +{ + using T = from_cdf_type_t; + auto [data, shape] + = _details::ranges::transform, typename Variable::shape_t>(values, + [](const PyObject* obj) -> T { return to_numerical(const_cast(obj)); }); + return std::pair(data_t(std::move(data), data_type), shape); +} + template std::pair _str_to_nd_data_t(const py::buffer& buffer) { @@ -121,106 +205,356 @@ std::pair _str_to_nd_data_t(const py::buffer } template -std::pair _time_to_nd_data_t(const py::buffer& buffer) +void to_cdf_string_t(PyObject* o, const std::span& out) { - py::buffer_info info = buffer.request(); - typename Variable::shape_t shape(info.ndim); - std::copy(std::cbegin(info.shape), std::cend(info.shape), std::begin(shape)); - no_init_vector values(info.size); - std::transform(reinterpret_cast(info.ptr), - reinterpret_cast(info.ptr) + info.size, std::begin(values), - [](const uint64_t& value) - { - return to_cdf_time(std::chrono::high_resolution_clock::time_point { - std::chrono::nanoseconds { value } }); - }); - return { data_t { std::move(values) }, std::move(shape) }; + if (PyUnicode_Check(o)) + { + Py_ssize_t size = 0; + auto py_str = PyUnicode_AsUTF8AndSize(o, &size); + if (static_cast(size) <= out.size()) + { + std::memcpy(out.data(), py_str, size); + std::memset(out.data() + size, 0, out.size() - size); + } + else + { + throw std::invalid_argument { fmt::format( + "String size exceeds allocated size in CDF variable, max size is {} and got {}", + out.size(), size) }; + } + } + else + { + throw std::invalid_argument { "Incompatible python and cdf string types" }; + } +} + +template +std::pair _str_to_nd_data_t( + const py_list_or_py_tuple auto& values, const auto& shape) +{ + using T = from_cdf_type_t; + auto [_data, _shape] + = _details::ranges::string_transform, typename Variable::shape_t>(values, + shape, [](const PyObject* obj, const std::span& out) + { to_cdf_string_t(const_cast(obj), out); }); + return { data_t { std::move(_data), data_type }, std::move(_shape) }; +} + +template +std::pair _time_to_nd_data_t(const py::array& arr) +{ + using T = from_cdf_type_t; + typename Variable::shape_t shape(arr.ndim()); + std::copy(arr.shape(), arr.shape() + arr.ndim(), std::begin(shape)); + no_init_vector values(arr.size()); + if (!to_cdf_time_t(arr, values.data())) + { + throw std::invalid_argument { "Incompatible python and cdf time types" }; + } + return { data_t { std::move(values), data_type }, std::move(shape) }; +} + +template +std::pair _time_to_nd_data_t( + const py_list_or_py_tuple auto& values) +{ + using T = from_cdf_type_t; + auto [data, shape] + = _details::ranges::transform, typename Variable::shape_t>(values, + [](const PyObject* obj) -> T { return to_cdf_time_t(const_cast(obj)); }); + return { data_t { std::move(data), data_type }, std::move(shape) }; } template -void _set_var_data_t(Variable& var, const py::buffer& buffer) +std::pair _set_var_data_t(const py::array& values) { - if constexpr (cdf_type == cdf::CDF_Types::CDF_UCHAR or cdf_type == cdf::CDF_Types::CDF_CHAR) + if constexpr (is_cdf_string_type(cdf_type)) { - auto [data, shape] = _str_to_nd_data_t(buffer); - var.set_data(std::move(data), std::move(shape)); + return _str_to_nd_data_t(values); } else { - if constexpr (cdf_type == cdf::CDF_Types::CDF_EPOCH - or cdf_type == cdf::CDF_Types::CDF_EPOCH16 - or cdf_type == cdf::CDF_Types::CDF_TIME_TT2000) + if constexpr (is_cdf_time_type(cdf_type)) { - auto [data, shape] = _time_to_nd_data_t>(buffer); - var.set_data(std::move(data), std::move(shape)); + return _time_to_nd_data_t(values); } else { - auto [data, shape] = _numeric_to_nd_data_t(buffer); - var.set_data(std::move(data), std::move(shape)); + return _numeric_to_nd_data_t(values); } } } -void set_values(Variable& var, const py::buffer& buffer, CDF_Types data_type) +template +std::pair _set_var_data_t( + const py_list_or_py_tuple auto& values, const auto& shape) { - switch (data_type) + + if constexpr (is_cdf_string_type(cdf_type)) { - case cdf::CDF_Types::CDF_UCHAR: // string - _set_var_data_t(var, buffer); - break; - case cdf::CDF_Types::CDF_CHAR: // string - _set_var_data_t(var, buffer); - break; - case cdf::CDF_Types::CDF_INT1: // int8 - _set_var_data_t(var, buffer); - break; - case cdf::CDF_Types::CDF_UINT1: // uint8 - _set_var_data_t(var, buffer); - break; - case cdf::CDF_Types::CDF_INT2: // int16 - _set_var_data_t(var, buffer); - break; - case cdf::CDF_Types::CDF_UINT2: // uint16 - _set_var_data_t(var, buffer); - break; - case cdf::CDF_Types::CDF_INT4: // int32 - _set_var_data_t(var, buffer); - break; - case cdf::CDF_Types::CDF_UINT4: // uint32 - _set_var_data_t(var, buffer); - break; - case cdf::CDF_Types::CDF_INT8: // int64 - _set_var_data_t(var, buffer); - break; - case cdf::CDF_Types::CDF_FLOAT: // float - _set_var_data_t(var, buffer); - break; - case cdf::CDF_Types::CDF_REAL4: // double - _set_var_data_t(var, buffer); - break; - case cdf::CDF_Types::CDF_DOUBLE: // double - _set_var_data_t(var, buffer); - break; - case cdf::CDF_Types::CDF_REAL8: // double - _set_var_data_t(var, buffer); - break; - case cdf::CDF_Types::CDF_TIME_TT2000: - _set_var_data_t(var, buffer); - break; - case cdf::CDF_Types::CDF_EPOCH: - _set_var_data_t(var, buffer); - break; - case cdf::CDF_Types::CDF_EPOCH16: - _set_var_data_t(var, buffer); - break; - default: - throw std::invalid_argument { "Unsuported CDF Type" }; - break; + return _str_to_nd_data_t(values, shape); + } + else + { + if constexpr (is_cdf_time_type(cdf_type)) + { + return _time_to_nd_data_t(values); + } + else + { + return _numeric_to_nd_data_t(values); + } } } +struct _min_storage_result +{ + uint8_t size_in_bytes = 1; + bool is_signed = false; + bool use_double = false; + + bool operator<(const _min_storage_result& other) const + { + if (use_double != other.use_double) + { + return other.use_double; + } + if (is_signed != other.is_signed) + { + return other.is_signed; + } + return size_in_bytes < other.size_in_bytes; + } + bool operator>(const _min_storage_result& other) const { return other < *this; } +}; + +[[nodiscard]] inline _min_storage_result _min_storage(PyObject* value) +{ + _min_storage_result min_storage; + if (PyLong_Check(value)) + { + // CDF format only supports up to signed 64 bits integers and unsigned 32 bits integers + int64_t val = PyLong_AsLongLong(const_cast(value)); + if (val < 0) + { + min_storage.is_signed = true; + if (val < std::numeric_limits::min()) + { + if (val < std::numeric_limits::min()) + { + if (val < std::numeric_limits::min()) + { + min_storage.size_in_bytes = 8; + } + else + { + min_storage.size_in_bytes + = min_storage.size_in_bytes > 4 ? min_storage.size_in_bytes : 4; + } + } + else + { + min_storage.size_in_bytes + = min_storage.size_in_bytes > 2 ? min_storage.size_in_bytes : 2; + } + } + } + else + { + uint64_t uval = PyLong_AsUnsignedLongLong(const_cast(value)); + if (uval > std::numeric_limits::max()) + { + min_storage.size_in_bytes = 8; + } + else if (uval > std::numeric_limits::max()) + { + min_storage.size_in_bytes + = min_storage.size_in_bytes > 4 ? min_storage.size_in_bytes : 4; + } + else if (uval > std::numeric_limits::max()) + { + min_storage.size_in_bytes + = min_storage.size_in_bytes > 2 ? min_storage.size_in_bytes : 2; + } + } + } + else if (PyFloat_Check(value)) + { + min_storage.use_double = true; + } + return min_storage; +} + + +[[nodiscard]] inline _min_storage_result _min_storage(const py_list_or_py_tuple auto& values) +{ + _min_storage_result min_storage; + for (const PyObject* obj : _details::ranges::py_list_or_tuple_view(values)) + { + if (PyList_Check(obj)) + { + auto inner_min_storage + = _min_storage(py::reinterpret_borrow(const_cast(obj))); + min_storage = min_storage > inner_min_storage ? min_storage : inner_min_storage; + } + else if (PyTuple_Check(obj)) + { + auto inner_min_storage + = _min_storage(py::reinterpret_borrow(const_cast(obj))); + min_storage = min_storage > inner_min_storage ? min_storage : inner_min_storage; + } + else + { + auto inner_min_storage = _min_storage(const_cast(obj)); + min_storage = min_storage > inner_min_storage ? min_storage : inner_min_storage; + } + } + return min_storage; +} + + +[[nodiscard]] inline CDF_Types _infer_best_type(const py_list_or_py_tuple auto& values) +{ + static const auto signed_types = std::array { CDF_Types::CDF_INT1, CDF_Types::CDF_INT2, + CDF_Types::CDF_INT4, CDF_Types::CDF_INT8 }; + static const auto unsigned_types = std::array { CDF_Types::CDF_UINT1, CDF_Types::CDF_UINT2, + CDF_Types::CDF_UINT4, CDF_Types::CDF_INT8 }; + for (const PyObject* obj : _details::ranges::py_list_or_tuple_view(values)) + { + if (PyBytes_Check(obj) or PyUnicode_Check(obj)) + { + return CDF_Types::CDF_UCHAR; + } + else if (PyLong_Check(obj)) + { + auto min_storage = _min_storage(values); + if (min_storage.use_double) + { + return CDF_Types::CDF_DOUBLE; + } + if (min_storage.is_signed) + { + return signed_types[(min_storage.size_in_bytes - 1) & 0x3]; + } + else + { + return unsigned_types[(min_storage.size_in_bytes - 1) & 0x3]; + } + } + else if (PyFloat_Check(obj)) + { + return CDF_Types::CDF_DOUBLE; + } + else if (PyList_Check(obj) or PyTuple_Check(obj)) + { + return _infer_best_type(py::reinterpret_borrow(const_cast(obj))); + } + else if (PyDateTime_Check(obj)) + { + return CDF_Types::CDF_TIME_TT2000; + } + else + { + throw std::invalid_argument { "Unsupported data type in input values" }; + } + } + return CDF_Types::CDF_NONE; +} + +inline void set_values(Variable& var, const py_list_or_py_tuple auto& values, + std::optional data_type, bool force = false) +{ + auto spec = analyze_collection(values); + if ((!data_type.has_value()) or (*data_type == CDF_Types::CDF_NONE)) + { + data_type = spec.inferred_cdf_type; + } + else + { + if (not force) + { + if (not are_compatible_types(*data_type, spec.inferred_cdf_type) and not spec.is_empty) + { + throw std::invalid_argument { + "Incompatible specified CDF data type and input values" + }; + } + if (not _details::are_compatible_shapes(var, spec.shape)) + { + throw std::invalid_argument { + "Incompatible specified CDF variable shape and input values" + }; + } + } + } + if (not spec.is_empty) + { + var.set_data(cdf_type_dispatch( + *data_type, [&spec](const auto& values) + { return _set_var_data_t(values, spec.shape); }, values)); + } + else + { + var.set_data(cdf_type_dispatch(*data_type, + []() + { + return std::pair { Variable::var_data_t { + no_init_vector> {}, T }, + Variable::shape_t {} }; + })); + } +} + +inline py::array ensure_utf8(const py::array& values) +{ + if (values.dtype().kind() == 'U') + { + auto np = py::module::import("numpy"); + py::array utf8_array = np.attr("char").attr("encode")(values, "utf-8"); + return utf8_array; + } + return values; +} + +inline void set_values( + Variable& var, const py::array& values, std::optional data_type, bool force = false) +{ + auto spec = analyze_collection(values); + if (!data_type.has_value() or (*data_type == CDF_Types::CDF_NONE)) + { + data_type = spec.inferred_cdf_type; + if (data_type == CDF_Types::CDF_NONE) + { + throw std::invalid_argument { + "Could not infer a compatible CDF data type from input numpy array" + }; + } + } + else + { + if (not force) + { + if (not are_compatible_types(var.type(), *data_type)) + { + throw std::invalid_argument { + "Incompatible specified CDF data type and input values" + }; + } + if (not _details::are_compatible_shapes(var, spec.shape)) + { + throw std::invalid_argument { + "Incompatible specified CDF variable shape and input values" + }; + } + } + } + var.set_data(cdf_type_dispatch( + *data_type, [](const py::array& values) { return _set_var_data_t(values); }, + values)); +} + template void def_variable_wrapper(T& mod) @@ -251,13 +585,70 @@ void def_variable_wrapper(T& mod) .def_buffer([](Variable& var) -> py::buffer_info { return make_buffer(var); }) .def_property_readonly("values", make_values_view, py::keep_alive<0, 1>()) .def_property_readonly("values_encoded", make_values_view, py::keep_alive<0, 1>()) - .def("_set_values", set_values, py::arg("values").noconvert(), py::arg("data_type")) - .def("_set_values", - [](Variable& var, const Variable& source) + .def( + "_set_values", + [](Variable& var, const py::array& values, std::optional data_type, + bool force) + { + if (not force and var.type() != CDF_Types::CDF_NONE) + { + py::warnings::warn( + "Overriding existing variable values without force=True is deprecated and " + "will raise an exception in future versions.", + PyExc_DeprecationWarning, 3); + } + set_values(var, ensure_utf8(values), data_type ? data_type : var.type()); + }, + py::arg("values").noconvert(), py::arg("data_type") = std::nullopt, + py::arg("force") = false) + .def( + "_set_values", + [](Variable& var, const py::list& values, std::optional data_type, + bool force) { + if (not force and var.type() != CDF_Types::CDF_NONE) + { + py::warnings::warn( + "Overriding existing variable values without force=True is deprecated and " + "will raise an exception in future versions.", + PyExc_DeprecationWarning, 3); + } + set_values(var, values, data_type ? data_type : var.type()); + }, + py::arg("values").noconvert(), py::arg("data_type") = std::nullopt, + py::arg("force") = false) + .def( + "_set_values", + [](Variable& var, const py::tuple& values, std::optional data_type, + bool force) + { + if (not force and var.type() != CDF_Types::CDF_NONE) + { + py::warnings::warn( + "Overriding existing variable values without force=True is deprecated and " + "will raise an exception in future versions.", + PyExc_DeprecationWarning, 3); + } + set_values(var, values, data_type ? data_type : var.type()); + }, + py::arg("values").noconvert(), py::arg("data_type") = std::nullopt, + py::arg("force") = false) + .def( + "_set_values", + [](Variable& var, const Variable& source, bool force) + { + if (var.type() != CDF_Types::CDF_NONE and not force) + { + if (var.type() != source.type()) + throw std::invalid_argument { "Incompatible variable types" }; + if (var.is_nrv() != source.is_nrv()) + throw std::invalid_argument { "Incompatible variable record vary" }; + if (var.shape() != source.shape()) + throw std::invalid_argument { "Incompatible variable shapes" }; + } var.set_data(source); }, - py::arg("source")) + py::arg("source"), py::arg("force") = false) .def("_add_attribute", static_cast(add_attribute), diff --git a/pyproject.toml b/pyproject.toml index c50297c..6cc99e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "pycdfpp" description="A modern C++ header only cdf library" authors = [{name="Alexis Jeandet", email="alexis.jeandet@member.fsf.org"}] summary = "A simple to use CDF files reader" -requires-python=">=3.8" +requires-python=">=3.9" license = {file="COPYING"} readme = "README.md" classifiers = [ @@ -21,6 +21,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", ] dependencies = ['numpy', 'pyyaml'] dynamic = [ @@ -33,7 +34,7 @@ repository = "https://github.com/SciQLop/CDFpp" [tool.meson-python.args] install = ['--tags=runtime,python-runtime'] -setup = ['-Doptimization=3', '-Dcpp_std=c++17', '--force-fallback-for=libdeflate,fmt,pybind11', '--vsenv'] +setup = ['-Doptimization=3', '-Dcpp_std=c++20', '--force-fallback-for=libdeflate,fmt,pybind11', '--vsenv'] [tool.cibuildwheel] diff --git a/scripts/CDF.hexpat b/scripts/CDF.hexpat index 2a633a0..3200c74 100644 --- a/scripts/CDF.hexpat +++ b/scripts/CDF.hexpat @@ -1,6 +1,7 @@ #pragma author Alexis Jeandet #pragma description Common Data Format File Format #pragma MIME application / x - cdf +#pragma eval_depth -1 #include "std/core.pat" #include "std/mem.pat" @@ -12,6 +13,8 @@ std::core::set_endian(std::mem::Endian::Big); u32 Magic1 @0; u32 Magic2 @4; +using PolymorphicRecord; + enum CDF_Types : u32 { CDF_NONE = 0, @@ -80,14 +83,30 @@ fn is_cdf_v3() return Magic1 == 0xCDF30001; }; -struct offset_t -{ +struct offset_ptr_t { if (is_cdf_v3()) - be u64 value; + { + if (std::mem::read_signed($, 8, std::mem::Endian::Big)==-1) + u64 value; + else + std::ptr::NullablePtr [[inline, name("Offset")]]; + } else - be u32 value; + { + if (std::mem::read_signed($, 4, std::mem::Endian::Big)==-1) + u32 value; + else + std::ptr::NullablePtr[[inline, name("Offset")]]; + } }; +struct offset_t { + if (is_cdf_v3()) + u64 value; + else + u32 value; +} [[inline]]; + struct ptr_t { if (is_cdf_v3()) @@ -99,12 +118,16 @@ struct ptr_t struct Header { - offset_t RecordSize; + if (is_cdf_v3()) + u64 RecordSize; + else + u32 RecordSize; cdf_record_type RecordType; -}; +}[[inline]]; struct CDF_Record : Header { + std::print("{} @0x{:X}", RecordType, addressof(RecordSize)); }; @@ -112,7 +135,7 @@ using GDR; struct CDR : CDF_Record { - ptr_t GDROffset; + offset_ptr_t GDROffset; u32 Version; u32 Release; u32 Encoding; @@ -127,16 +150,16 @@ struct CDR : CDF_Record struct GDR : CDF_Record { - offset_t rVDRhead; - offset_t zVDRhead; - offset_t ADRhead; + offset_ptr_t rVDRhead; + offset_ptr_t zVDRhead; + offset_ptr_t ADRhead; offset_t eof; u32 NrVars; u32 NumAttr; u32 rMaxRec; u32 rNumDims; u32 NzVars; - offset_t UIRhead; + offset_ptr_t UIRhead; u32 rfuC; u32 LeapSecondLastUpdated; u32 rfuE; @@ -144,7 +167,7 @@ struct GDR : CDF_Record struct AgrEDR: CDF_Record { - offset_t AEDRnext; + offset_ptr_t AEDRnext; s32 AttrNum; CDF_Types DataType; s32 Num; @@ -154,24 +177,31 @@ struct AgrEDR: CDF_Record u32 rfuC; u32 rfuD; u32 rfuE; - u8 data[RecordSize.value - ($-addressof(RecordSize))]; + u8 data[RecordSize - ($-addressof(RecordSize))]; +}; + +bitfield VDR_Flags { + bool record_variance :1; + bool has_pad_value :1; + bool compressed :1; + padding :29[[hidden]]; }; struct VDR : CDF_Record { - offset_t VDRnext; + offset_ptr_t VDRnext; CDF_Types DataType; u32 MaxRec; - offset_t VXRhead; - offset_t VXRtail; - u32 Flags; + offset_ptr_t VXRhead; + offset_ptr_t VXRtail; + VDR_Flags Flags; u32 SRecords; u32 rfuB; u32 rfuC; u32 rfuF; u32 NumElems; u32 Num; - offset_t CPRorSPRoffset; + offset_ptr_t CPRorSPRoffset; u32 BlockingFactor; char Name[256]; if (RecordType == cdf_record_type::zVDR) @@ -187,26 +217,25 @@ struct VDR : CDF_Record { u32 DimVarys[gdr.rNumDims]; } - if (Flags & 2 == 2) + if (Flags.has_pad_value) { - u8 pad_values[RecordSize.value - ($ - addressof(parent))]; + u8 pad_values[RecordSize - ($ - addressof(parent))]; } }; struct VXR : CDF_Record { - offset_t VXRnext; + offset_ptr_t VXRnext; s32 Nentries; - std::print("{}", Nentries); u32 NusedEntries; u32 First[Nentries]; u32 Last[Nentries]; - offset_t Offsets[Nentries]; + offset_ptr_t Offsets[Nentries]; }; struct UhandledRecord : CDF_Record { - u8 data[RecordSize.value - sizeof(Header)]; + u8 data[RecordSize - sizeof(Header)]; }; @@ -228,7 +257,8 @@ struct CDF { u32 Magic1; u32 Magic2; - PolymorphicRecord records[while (!std::mem::eof())]; + CDR cdr; + // PolymorphicRecord records[while (!std::mem::eof())]; }; diff --git a/scripts/chrono_profiling.py b/scripts/chrono_profiling.py new file mode 100644 index 0000000..a63af6a --- /dev/null +++ b/scripts/chrono_profiling.py @@ -0,0 +1,73 @@ +#!python + +"""Chrono profiling for pycdfpp datetime64 conversion. + +This script is expected to be run from the build directory. +To profile, you can use: + `perf record --debuginfod -F 16000 --call-graph dwarf -g -o perf.data python /chrono_profiling.py` +""" +import sys, os +sys.path.append(".") +try: + import pycdfpp +except ImportError as e: + raise ImportError( + "Please run this script from the build directory where pycdfpp is built." + ) from e + +from subprocess import run +from tempfile import NamedTemporaryFile + +prelude = """ +import sys +sys.path.append(".") +import os +os.environ["TZ"]="UTC" +import pycdfpp +import numpy as np +from datetime import datetime +""" + +build_tt2000_before_2017 = 'pycdfpp.to_tt2000((np.arange(0, 10_000_000, dtype=np.int64)*200_000_000_000).astype("datetime64[ns]"))' +build_tt2000_after_2017 = 'pycdfpp.to_tt2000((np.arange(0, 10_000_000, dtype=np.int64)*20_000_000_000 + 1500_000_000_000_000_000).astype("datetime64[ns]"))' +build_epoch16 = 'pycdfpp.to_epoch16((np.arange(0, 10_000_000, dtype=np.int64)*200_000_000_000).astype("datetime64[ns]"))' +build_epoch = 'pycdfpp.to_epoch((np.arange(0, 10_000_000, dtype=np.int64)*200_000_000_000).astype("datetime64[ns]"))' + +fragments = { + "tt2000_to_datetime64_before_2017": f""" +{prelude} +ref = {build_tt2000_before_2017} +for _ in range(10): + res = pycdfpp.to_datetime64(ref) +""", + "tt2000_to_datetime64_after_2017": f""" +{prelude} +ref = {build_tt2000_after_2017} +for _ in range(10): + res = pycdfpp.to_datetime64(ref) +""", + "epoch16_to_datetime64": f""" +{prelude} +ref = {build_epoch16} +for _ in range(10): + res = pycdfpp.to_datetime64(ref) +""", + "epoch_to_datetime64": f""" +{prelude} +ref = {build_epoch} +for _ in range(10): + res = pycdfpp.to_datetime64(ref) +""", +} + + +for name,func in fragments.items(): + with NamedTemporaryFile("w") as f: + f.write(func) + f.flush() + dest_dir = f"perf_data/{name}" + os.makedirs(f"{dest_dir}", exist_ok=True) + r=run(f"perf record --debuginfod -F 16000 --call-graph dwarf -g -o {dest_dir}/perf.data {sys.executable} {f.name}".split(), + check=True, + cwd=os.getcwd(), + ) diff --git a/simd/meson.build b/simd/meson.build new file mode 100644 index 0000000..1cfa319 --- /dev/null +++ b/simd/meson.build @@ -0,0 +1,76 @@ + +simd_deps = [] + + +if target_machine.cpu_family() == 'x86_64' + x86_vectorized_libs = [] + x86_vectorized_defs = [] + xsimd_arch_list = [] + if cpp.get_id() == 'msvc' + archs = [ + { + 'name':'avx512bw', + 'flags':['/arch:AVX512'], + 'xsimd_name':'avx512bw' + }, + { + 'name':'avx2', + 'flags':['/arch:AVX2'], + 'xsimd_name':'avx2' + }, + { + 'name':'sse2', + 'flags':['/arch:SSE2'], + 'xsimd_name':'sse2' + } + ] + else + archs = [ + { + 'name':'avx512bw', + 'flags':['-mavx512bw', '-mavx512cd', '-mavx512dq', '-mavx512vl'], + 'xsimd_name':'avx512bw' + }, + { + 'name':'avx2', + 'flags':['-mavx2'], + 'xsimd_name':'avx2' + }, + { + 'name':'sse2', + 'flags':['-msse2'], + 'xsimd_name':'sse2' + } + ] + endif + foreach arch : archs + enable_arch_def = '-DCDFPP_ENABLE_'+arch['name'].to_upper()+'_ARCH' + x86_vectorized_libs += [ + static_library('cdfpp_x86_vectorized_'+arch['name'], + files('../src/arch/x86/chrono_arch.cpp'), + include_directories : include_directories('../include'), + cpp_args : arch['flags'] + [enable_arch_def, '-DCDFPP_ARCH='+arch['xsimd_name']], + dependencies : [xsimd_dep, hedley_dep, fmt_dep], + ) + ] + x86_vectorized_defs += [enable_arch_def] + xsimd_arch_list += ['xsimd::'+arch['xsimd_name']] + endforeach + + xsimd_arch_list = 'xsimd::arch_list<' + ', '.join(xsimd_arch_list) + '>' + + x86_vectorized_dep = declare_dependency( + sources : files('../src/arch/x86/chrono.cpp'), + link_with : x86_vectorized_libs, + compile_args : x86_vectorized_defs + ['-DCDFPP_XSIMD_ARCH_LIST=@0@'.format(xsimd_arch_list)], + dependencies : [xsimd_dep, fmt_dep, hedley_dep], + ) + simd_deps += [x86_vectorized_dep] + +elif target_machine.cpu_family() == 'aarch64' +# investigate later if we can have gains with SIMD on ARM architectures + add_project_arguments('-DCDFPP_NO_SIMD', language : ['cpp']) +else +# investigate too WASM SIMD support and benefits + add_project_arguments('-DCDFPP_NO_SIMD', language : ['cpp']) +endif diff --git a/src/arch/arm/chrono.cpp b/src/arch/arm/chrono.cpp new file mode 100644 index 0000000..e69de29 diff --git a/src/arch/x86/chrono.cpp b/src/arch/x86/chrono.cpp new file mode 100644 index 0000000..37642ac --- /dev/null +++ b/src/arch/x86/chrono.cpp @@ -0,0 +1,29 @@ +#include + +namespace cdf::chrono::vectorized +{ + +auto _disp_to_ns_from_1970_tt2000 + = xsimd::dispatch(_to_ns_from_1970_tt2000_t {}); +auto _disp_to_ns_from_1970_epoch = xsimd::dispatch(_to_ns_from_1970_epoch_t {}); +auto _disp_to_ns_from_1970_epoch16 = xsimd::dispatch(_to_ns_from_1970_epoch16_t {}); + +} // namespace cdf::chrono::vectorized + +void vectorized_to_ns_from_1970( + const std::span& input, int64_t* const output) +{ + cdf::chrono::vectorized::_disp_to_ns_from_1970_tt2000(input, output); +} + +void vectorized_to_ns_from_1970( + const std::span& input, int64_t* const output) +{ + cdf::chrono::vectorized::_disp_to_ns_from_1970_epoch(input, output); +} + +void vectorized_to_ns_from_1970( + const std::span& input, int64_t* const output) +{ + cdf::chrono::vectorized::_disp_to_ns_from_1970_epoch16(input, output); +} diff --git a/src/arch/x86/chrono_arch.cpp b/src/arch/x86/chrono_arch.cpp new file mode 100644 index 0000000..79f428c --- /dev/null +++ b/src/arch/x86/chrono_arch.cpp @@ -0,0 +1,10 @@ +#include + +namespace cdf::chrono::vectorized +{ + +template void _to_ns_from_1970_tt2000_t::operator()(xsimd::CDFPP_ARCH, const std::span& input, int64_t* const output); +template void _to_ns_from_1970_epoch_t::operator()(xsimd::CDFPP_ARCH, const std::span& input, int64_t* const output); +template void _to_ns_from_1970_epoch16_t::operator()(xsimd::CDFPP_ARCH, const std::span& input, int64_t* const output); + +} // namespace cdf::chrono::vectorized diff --git a/subprojects/catch2.wrap b/subprojects/catch2.wrap index 96c1e10..e018133 100644 --- a/subprojects/catch2.wrap +++ b/subprojects/catch2.wrap @@ -1,10 +1,10 @@ [wrap-file] -directory = Catch2-3.11.0 -source_url = https://github.com/catchorg/Catch2/archive/v3.11.0.tar.gz -source_filename = Catch2-3.11.0.tar.gz -source_hash = 82fa1cb59dc28bab220935923f7469b997b259eb192fb9355db62da03c2a3137 -source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/catch2_3.11.0-1/Catch2-3.11.0.tar.gz -wrapdb_version = 3.11.0-1 +directory = Catch2-3.12.0 +source_url = https://github.com/catchorg/Catch2/archive/v3.12.0.tar.gz +source_filename = Catch2-3.12.0.tar.gz +source_hash = e077079f214afc99fee940d91c14cf1a8c1d378212226bb9f50efff75fe07b23 +source_fallback_url = https://wrapdb.mesonbuild.com/v2/catch2_3.12.0-1/get_source/Catch2-3.12.0.tar.gz +wrapdb_version = 3.12.0-1 [provide] catch2 = catch2_dep diff --git a/subprojects/google-benchmark.wrap b/subprojects/google-benchmark.wrap index 9015892..7e2e6e7 100644 --- a/subprojects/google-benchmark.wrap +++ b/subprojects/google-benchmark.wrap @@ -3,11 +3,11 @@ directory = benchmark-1.8.4 source_url = https://github.com/google/benchmark/archive/refs/tags/v1.8.4.tar.gz source_filename = benchmark-1.8.4.tar.gz source_hash = 3e7059b6b11fb1bbe28e33e02519398ca94c1818874ebed18e504dc6f709be45 -patch_filename = google-benchmark_1.8.4-4_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/google-benchmark_1.8.4-4/get_patch -patch_hash = d1af464d29eb42442c41bc0629f94dbc2e390d9a8656461a14adfee84bcb6250 -source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/google-benchmark_1.8.4-4/benchmark-1.8.4.tar.gz -wrapdb_version = 1.8.4-4 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/google-benchmark_1.8.4-5/benchmark-1.8.4.tar.gz +patch_filename = google-benchmark_1.8.4-5_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/google-benchmark_1.8.4-5/get_patch +patch_hash = 671ffed65f1e95e8c20edb7a06eb54476797e58169160b255f52dc71f4b83957 +wrapdb_version = 1.8.4-5 [provide] dependency_names = benchmark, benchmark_main diff --git a/subprojects/xsimd.wrap b/subprojects/xsimd.wrap new file mode 100644 index 0000000..483758c --- /dev/null +++ b/subprojects/xsimd.wrap @@ -0,0 +1,9 @@ +[wrap-git] +url = https://github.com/xtensor-stack/xsimd +revision = HEAD +depth = 1 + +method = cmake + +[provide] +xsimd = xsimd_dep diff --git a/tests/chrono/main.cpp b/tests/chrono/main.cpp index a547fda..6ffe1aa 100644 --- a/tests/chrono/main.cpp +++ b/tests/chrono/main.cpp @@ -1,17 +1,88 @@ #include #include -#if __has_include() #include #include -#else -#include -#endif + #include "cdfpp/chrono/cdf-chrono.hpp" #include "test_values.hpp" +TEST_CASE("Leap Seconds", "") +{ + using namespace cdf::chrono::leap_seconds; + SECTION("int64_t leap_second(int64_t ns_from_1970)") + { + for (const auto& item : leap_seconds_tt2000) + { + REQUIRE(cdf::_impl::leap_second(item.first + 1000000000) == item.second); + } + } + SECTION("int64_t leap_second_branchless(int64_t ns_from_1970)") + { + for (const auto& item : leap_seconds_tt2000) + { + REQUIRE(cdf::_impl::leap_second_branchless(item.first + 1000000000) == item.second); + } + } +} + + +TEST_CASE("To ns from 1970", "") +{ + using namespace cdf; + using namespace cdf::chrono; + using namespace cdf::chrono::leap_seconds; + SECTION("Basic test") + { + auto input = std::vector { 0_tt2k, 631108869184000000_tt2k }; + auto output = std::vector(std::size(input)); + cdf::_impl::scalar_to_ns_from_1970(input, output.data()); + REQUIRE(output[0] / 1'000'000LL == 946727935816); + REQUIRE(output[1] / 1'000'000LL == 1577836800000); + + // retry value by value because there are two different implementations + // one that works for all values and one that is optimized for after the last + // leap second (i.e., 2017-01-01T00:00:00Z), the first value triggers the + // optimized path assuming the input is sorted. + cdf::_impl::scalar_to_ns_from_1970({input.data(), 1}, output.data()); + REQUIRE(output[0] / 1'000'000LL == 946727935816); + cdf::_impl::scalar_to_ns_from_1970({input.data() + 1, 1}, output.data() + 1); + REQUIRE(output[1] / 1'000'000LL == 1577836800000); + } + SECTION("Scalar") + { + for (const auto& item : test_values) + { + if (item.unix_epoch >= 68688000) + { + int64_t output = 0; + cdf::_impl::scalar_to_ns_from_1970({&item.tt2000_epoch, 1}, &output); + int64_t expected = item.unix_epoch * 1'000'000'000LL; + REQUIRE(output == expected); + } + } + } + SECTION("Vectorized against scalar") + { + std::vector inputs(1024); + std::vector expected_outputs(inputs.size()); + for (std::size_t i = 0; i < inputs.size(); ++i) + { + inputs[i] = cdf::tt2000_t(-869399957816000000 + i * 1000000000LL); + } + cdf::_impl::scalar_to_ns_from_1970(inputs, expected_outputs.data()); + std::vector outputs(inputs.size()); + cdf::to_ns_from_1970(std::span{inputs}, outputs.data()); + for (std::size_t i = 0; i < outputs.size(); ++i) + { + REQUIRE(outputs[i] == expected_outputs[i]); + } + } +} + + TEST_CASE("cdf epoch to timepoint", "") { using namespace std::chrono; @@ -81,7 +152,7 @@ TEST_CASE("timepoint to cdf tt2000", "") if (item.unix_epoch >= 68688000) { auto tp = time_point {} + seconds(item.unix_epoch); - REQUIRE(item.tt2000_epoch.value == cdf::to_tt2000(tp).value); + REQUIRE(item.tt2000_epoch.nseconds == cdf::to_tt2000(tp).nseconds); } } } diff --git a/tests/chrono/test_values.hpp b/tests/chrono/test_values.hpp index 50ad47d..503d023 100644 --- a/tests/chrono/test_values.hpp +++ b/tests/chrono/test_values.hpp @@ -13,7 +13,7 @@ struct test_entry }; -test_entry test_values[] = { +inline constexpr test_entry test_values[] = { //1950-01-01 00:00:00+00:00 test_entry{ -631152000, { -1577879967816000000 }, { 61536067200000.0 }, { 61536067200.0, 0.0 } }, diff --git a/tests/endianness/main.cpp b/tests/endianness/main.cpp index e975b63..1c79c29 100644 --- a/tests/endianness/main.cpp +++ b/tests/endianness/main.cpp @@ -1,9 +1,6 @@ -#if __has_include() #include #include -#else -#include -#endif + #include "cdfpp/cdf-io/endianness.hpp" #include diff --git a/tests/full_corpus/test_full_corpus.py b/tests/full_corpus/test.py similarity index 100% rename from tests/full_corpus/test_full_corpus.py rename to tests/full_corpus/test.py diff --git a/tests/libdeflate_compression/main.cpp b/tests/libdeflate_compression/main.cpp index 624c019..be04cb4 100644 --- a/tests/libdeflate_compression/main.cpp +++ b/tests/libdeflate_compression/main.cpp @@ -1,9 +1,6 @@ -#if __has_include() #include #include -#else -#include -#endif + #include #ifdef CDFpp_USE_LIBDEFLATE #include "cdfpp/cdf-io/libdeflate.hpp" diff --git a/tests/majority/main.cpp b/tests/majority/main.cpp index ddaa37a..fc5c95b 100644 --- a/tests/majority/main.cpp +++ b/tests/majority/main.cpp @@ -1,9 +1,6 @@ -#if __has_include() #include #include -#else -#include -#endif + #include "cdfpp/cdf-io/majority-swap.hpp" #include "vector" diff --git a/tests/manual_save/main.cpp b/tests/manual_save/main.cpp index 7658b4c..1bd7645 100644 --- a/tests/manual_save/main.cpp +++ b/tests/manual_save/main.cpp @@ -22,7 +22,10 @@ int main(int argc, char** argv) { auto cdf = *maybe_cdf; cdf.compression = cdf::cdf_compression_type::gzip_compression; - cdf::io::save(cdf, "/tmp/test.cdf"); + if(!cdf::io::save(cdf, "/tmp/test.cdf")) + std::cout << "failed to save!\n"; + else + std::cout << "saved to /tmp/test.cdf\n"; if (auto res = cdf::io::save(cdf); std::size(res)) std::cout << "success!\n"; else diff --git a/tests/meson.build b/tests/meson.build new file mode 100644 index 0000000..0c28ec6 --- /dev/null +++ b/tests/meson.build @@ -0,0 +1,56 @@ +configure_file(output : 'tests_config.hpp', + configuration : { + 'DATA_PATH' : '"' + meson.current_source_dir() / 'resources' + '"' + } +) + +catch_dep = dependency('catch2-with-main', version:'>3.0.0', required : true) + + +foreach test_name:['endianness','simple_open', 'majority', 'chrono', 'nomap', 'records_loading', 'records_saving', + 'rle_compression', 'libdeflate_compression', 'zlib_compression', 'simple_save', 'zstd_compression'] + exe = executable('test-'+test_name, test_name+'/main.cpp', + dependencies:[catch_dep, cdfpp_dep], + install: false + ) + test(test_name, exe) +endforeach + +foreach py_test:['python_loading', 'python_saving', 'python_skeletons', + 'python_variable_set_values', 'full_corpus', 'python_chrono'] + test(py_test, python3, + args:[files(py_test+'/test.py')], + env:['PYTHONPATH='+meson.project_build_root()], + timeout: 300, + workdir:meson.current_build_dir()) +endforeach + + +python_wrapper_cpp = executable('python_wrapper_cpp','python_wrapper_cpp/main.cpp', + dependencies:[pybind11_dep, python3.dependency(embed:true), catch_dep, cdfpp_dep, fmt_dep], + install: false + ) + +test('python_wrapper_cpp', python_wrapper_cpp, + env:['PYTHONPATH='+meson.project_build_root()], + timeout: 300, + workdir:meson.current_build_dir()) + +manual_load = executable('manual_load','manual_load/main.cpp', + dependencies:[cdfpp_dep], + install: false + ) + +manual_load = executable('manual_save','manual_save/main.cpp', + dependencies:[cdfpp_dep], + install: false + ) + + +foreach example:['basic_cpp'] + exe = executable('example-'+example,'../examples/'+example+'/main.cpp', + dependencies:[cdfpp_dep], + cpp_args: ['-DDATA_PATH="@0@/resources"'.format(meson.current_source_dir())], + install: false + ) +endforeach diff --git a/tests/nomap/main.cpp b/tests/nomap/main.cpp index 2725ab4..130deff 100644 --- a/tests/nomap/main.cpp +++ b/tests/nomap/main.cpp @@ -7,12 +7,9 @@ #include -#if __has_include() #include #include -#else -#include -#endif + #include "cdfpp/nomap.hpp" diff --git a/tests/python_chrono/test.py b/tests/python_chrono/test.py new file mode 100755 index 0000000..b03e1d4 --- /dev/null +++ b/tests/python_chrono/test.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import os +from datetime import datetime, timedelta +import numpy as np +import unittest +import pycdfpp + +os.environ['TZ'] = 'UTC' + + +def make_datetime64_values(): + return np.arange(1e18, 11e17, 1e16, dtype=np.int64).astype("datetime64[ns]") + + +def make_datetime64_n_values(count:int, start:int=1e18, stop:int=2e18): + step = (stop - start) / count + return np.arange(start, stop, step, dtype=np.int64).astype("datetime64[ns]") + +def make_datetime_values(count:int=100): + return [ datetime(2000, 1, 1, 12, 0,5,microsecond=10000) + timedelta(seconds=i) for i in range(count) ] + + +def make_list_of_mixed_types(count:int=100): + ref = make_datetime_values(count) + mixed = ref.copy() + for i in range(count): + if i % 4 == 0: + mixed[i] = pycdfpp.to_tt2000(mixed[i]) + elif i % 4 == 1: + mixed[i] = pycdfpp.to_epoch(mixed[i]) + elif i % 4 == 2: + mixed[i] = pycdfpp.to_epoch16(mixed[i]) + return ref, mixed + + +class PycdfChrono(unittest.TestCase): + def test_simple_dt_tt2000(self): + ref = [datetime(2000, 1, 1, 0, 0, 0), + datetime(2020, 5, 15, 12, 30, 45), + datetime(1995, 7, 4, 18, 15, 30)] + res = pycdfpp.to_tt2000(ref) + self.assertListEqual(ref, pycdfpp.to_datetime(res)) + + def test_simple_dt_dt64(self): + ref = [datetime(2000, 1, 1, 0, 0, 0), + datetime(2020, 5, 15, 12, 30, 45), + datetime(1995, 7, 4, 18, 15, 30)] + res = pycdfpp.to_datetime64(ref) + self.assertListEqual(ref, pycdfpp.to_datetime(res)) + + def test_mix_dt64(self): + ref, mixed = make_list_of_mixed_types() + res = pycdfpp.to_datetime64(mixed) + self.assertListEqual(ref, pycdfpp.to_datetime(res)) + + def test_mix_dt(self): + ref, mixed = make_list_of_mixed_types() + res = pycdfpp.to_datetime(mixed) + self.assertListEqual(ref, res) + + def test_mix_tt2000(self): + ref, mixed = make_list_of_mixed_types() + res = pycdfpp.to_tt2000(mixed) + self.assertListEqual(ref, pycdfpp.to_datetime(res)) + + def test_simple_dt64_tt2000(self): + ref = make_datetime64_values() + self.assertTrue(np.all(ref == pycdfpp.to_datetime64(pycdfpp.to_tt2000(ref)))) + + def test_simple_dt64_epoch(self): + ref = make_datetime64_values() + self.assertTrue(np.all(ref == pycdfpp.to_datetime64(pycdfpp.to_epoch(ref)))) + + def test_simple_dt64_epoch16(self): + ref = make_datetime64_values() + self.assertTrue(np.all(ref == pycdfpp.to_datetime64(pycdfpp.to_epoch16(ref)))) + + def test_dt64_tt2000_variable_size(self): + # the underlying algorithm depends on the input size, so we need to test with different sizes + for size in (1, 10, 100, 1000, 10000, 2**20, 2**24): + ref = make_datetime64_n_values(int(size)) + self.assertTrue(np.all(ref == pycdfpp.to_datetime64(pycdfpp.to_tt2000(ref)))) + # it also depend on asumptions like the first value being after 2017 (the last leap second) + for size in (1, 10, 100, 1000, 10000, 2**20, 2**24): + ref = make_datetime64_n_values(int(size), start=1.5e18) + self.assertTrue(np.all(ref == pycdfpp.to_datetime64(pycdfpp.to_tt2000(ref)))) + # and the array being sorted + for size in (1, 10, 100, 1000, 10000, 2**20, 2**24): + ref = make_datetime64_n_values(int(size)) + np.random.shuffle(ref) + self.assertTrue(np.all(ref == pycdfpp.to_datetime64(pycdfpp.to_tt2000(ref)))) + + def test_todt64_empty(self): + self.assertEqual(pycdfpp.to_datetime64(pycdfpp.to_tt2000([])), + np.array([], dtype="datetime64[ns]")) + +class PycdfChronoErrors(unittest.TestCase): + def test_invalid_input(self): + with self.assertRaises(ValueError): + pycdfpp.to_datetime64(["not a datetime"]) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/python_loading/test.py b/tests/python_loading/test.py index bbe76d0..f27a5e4 100755 --- a/tests/python_loading/test.py +++ b/tests/python_loading/test.py @@ -276,11 +276,16 @@ def test_datetime_conversions(self): whole_var = f(cdf[var]) values = f(cdf[var].values) one_by_one = [f(v) for v in cdf[var].values] + if var == 'tt2000': + # tt2000 conv is voluntarily broken for dates before 1970-01-01 + values = values[5:] + one_by_one = one_by_one[5:] + whole_var = whole_var[5:] self.assertIsNotNone(whole_var) self.assertIsNotNone(values) self.assertIsNotNone(one_by_one) - self.assertTrue(np.all(whole_var == values)) - self.assertTrue(np.all(whole_var == one_by_one)) + self.assertTrue(np.all(whole_var == values), f"Mismatch in var {var}, for func {f}") + self.assertTrue(np.all(whole_var == one_by_one), f"Mismatch in var {var}, for func {f}") for attr in ('epoch', 'epoch16', 'tt2000'): whole_attr = f(cdf.attributes[attr][0]) @@ -294,7 +299,7 @@ def test_non_string_vars_implements_buffer_protocol(self): for name, var in cdf.items(): if var.type not in (pycdfpp.DataType.CDF_CHAR, pycdfpp.DataType.CDF_UCHAR): arr_from_buffer = np.array(var) - self.assertTrue(np.all(arr_from_buffer == var.values)) + self.assertTrue(np.all(arr_from_buffer == var.values), f"Broken var: {name}") def test_everything_have_repr(self): for cdf in self.cdfs: diff --git a/tests/python_saving/test.py b/tests/python_saving/test.py index 291402d..8ad3de3 100755 --- a/tests/python_saving/test.py +++ b/tests/python_saving/test.py @@ -11,6 +11,7 @@ os.environ['TZ'] = 'UTC' +print(f"Running tests with pycdfpp version: {pycdfpp.__version__} from {pycdfpp.__file__}") def make_cdf(): cdf = pycdfpp.CDF() diff --git a/tests/python_variable_set_values/test.py b/tests/python_variable_set_values/test.py index 40cca56..7727d23 100755 --- a/tests/python_variable_set_values/test.py +++ b/tests/python_variable_set_values/test.py @@ -102,10 +102,10 @@ def test_setting_CDF_EPOCH16_with_datetime64_ns_values(self): def test_setting_datetime64_ms_values(self): cdf = pycdfpp.CDF() values = make_datetime64_values() - cdf.add_variable("datetime64[ns]", values=values.astype("datetime64[ms]")) - self.assertIn("datetime64[ns]", cdf) + cdf.add_variable("datetime64[ms]", values=values.astype("datetime64[ms]")) + self.assertIn("datetime64[ms]", cdf) self.assertTrue(np.all(pycdfpp.to_datetime64( - cdf["datetime64[ns]"]) == values)) + cdf["datetime64[ms]"]) == values)) def test_setting_datetime_values(self): cdf = pycdfpp.CDF() diff --git a/tests/python_wrapper_cpp/main.cpp b/tests/python_wrapper_cpp/main.cpp index 0e41359..df289ff 100644 --- a/tests/python_wrapper_cpp/main.cpp +++ b/tests/python_wrapper_cpp/main.cpp @@ -1,18 +1,323 @@ +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "pycdfpp/collections.hpp" +#include "pycdfpp/data_types.hpp" +#include "pycdfpp/enums.hpp" + +using namespace cdf; + +#include #include +#include +#include +#include +#include +#include +#include + +#include + namespace py = pybind11; -int main(int argc, char** argv) + +PYBIND11_EMBEDDED_MODULE(test_module, m) { + if (!PyDateTimeAPI) + PyDateTime_IMPORT; + def_enums_wrappers(m); + py::native_enum(m, "BestTypeId", "enum.Enum") + .value("NONE", BestTypeId::None) + .value("DateTime", BestTypeId::DateTime) + .value("Float", BestTypeId::Float) + .value("Int8", BestTypeId::Int8) + .value("UInt8", BestTypeId::UInt8) + .value("Int16", BestTypeId::Int16) + .value("UInt16", BestTypeId::UInt16) + .value("Int32", BestTypeId::Int32) + .value("UInt32", BestTypeId::UInt32) + .value("Int64", BestTypeId::Int64) + .export_values() + .finalize(); + + m.def("analyze_collection_cdf_type", + [](py::list& input) + { + auto r = analyze_collection(input); + return r.inferred_cdf_type; + }); + m.def("analyze_collection_cdf_shape", + [](py::list& input) + { + auto r = analyze_collection(input); + return r.shape; + }); +} + +SCENARIO("Testing analyze_collection function", "[CDF]") +{ + using Catch::Matchers::ContainsSubstring; + auto run_test = [](auto values) + { + py::gil_scoped_acquire_simple acquire; + py::exec(fmt::format(R"( +from datetime import datetime +import test_module +if globals().get("values") is not None: + del values +if globals().get("cdf_type") is not None: + del cdf_type +if globals().get("shape") is not None: + del shape +values = {} +cdf_type = test_module.analyze_collection_cdf_type(values) +shape = test_module.analyze_collection_cdf_shape(values) + )", + values)); + CDF_Types cdf_type = py::globals()["cdf_type"].cast(); + std::vector shape = py::globals()["shape"].cast>(); + return std::make_pair(cdf_type, shape); + }; + py::scoped_interpreter guard {}; - if (argc > 1) + GIVEN("A bunch of well formed collections") + { + WHEN("Analyzing an empty list") + { + auto [cdf_type, shape] = run_test("[]"); + THEN("The inferred type is CDF_NONE and shape is empty") + { + REQUIRE(cdf_type == CDF_Types::CDF_NONE); + REQUIRE(shape.empty()); + } + } + WHEN("Analyzing a simple list of integers") + { + auto [cdf_type, shape] = run_test("[1, 2, 3, 4]"); + THEN("The inferred type is CDF_UINT1 and shape is [4]") + { + REQUIRE(cdf_type == CDF_Types::CDF_UINT1); + REQUIRE(shape == std::vector { 4 }); + } + } + WHEN("Analyzing an ND list of integers") + { + auto [cdf_type, shape] = run_test( + "[[[1, 2, 3], [4, 5, 6], [7, 8, 9]], [[10, 11, 12], [13, 14, 15], [16, 17, 18]]]"); + THEN("The inferred type is CDF_UINT1 and shape is [2, 3, 3]") + { + REQUIRE(cdf_type == CDF_Types::CDF_UINT1); + REQUIRE(shape == std::vector { 2, 3, 3 }); + } + } + WHEN("Analyzing a list of mixed list and tuples") + { + auto [cdf_type, shape] = run_test("[[1, 2, 3], (4, 5, 6), [7, 8, 9]]"); + THEN("The inferred type is CDF_UINT1 and shape is [3, 3]") + { + REQUIRE(cdf_type == CDF_Types::CDF_UINT1); + REQUIRE(shape == std::vector { 3, 3 }); + } + } + WHEN("Analyzing a list of floats") + { + auto [cdf_type, shape] = run_test("[1.0, 2.0, 3.0, 4.0]"); + THEN("The inferred type is CDF_DOUBLE and shape is [4]") + { + REQUIRE(cdf_type == CDF_Types::CDF_DOUBLE); + REQUIRE(shape == std::vector { 4 }); + } + } + WHEN("Analyzing a mixed list of integers and floats") + { + auto [cdf_type, shape] = run_test("[1, 2.0, 3, 4.0]"); + THEN("The inferred type is CDF_DOUBLE and shape is [4]") + { + REQUIRE(cdf_type == CDF_Types::CDF_DOUBLE); + REQUIRE(shape == std::vector { 4 }); + } + } + WHEN("Analyzing a list of datetime objects") + { + auto [cdf_type, shape] = run_test("[datetime(2020, 1, 1), datetime(2021, 1, 1)]"); + THEN("The inferred type is CDF_TIME_TT2000 and shape is [2]") + { + REQUIRE(cdf_type == CDF_Types::CDF_TIME_TT2000); + REQUIRE(shape == std::vector { 2 }); + } + } + GIVEN("A list of strings") + { + WHEN("All strings have the same length") + { + auto [cdf_type, shape] = run_test("['abc', 'def', 'ghi']"); + THEN("The inferred type is CDF_CHAR and shape is [3, 3]") + { + REQUIRE(cdf_type == CDF_Types::CDF_UCHAR); + REQUIRE(shape == std::vector { 3, 3 }); + } + } + WHEN("Strings have varying lengths") + { + auto [cdf_type, shape] = run_test("['a', 'de', 'ghi']"); + THEN("The inferred type is CDF_CHAR and shape is [3, 3]") + { + REQUIRE(cdf_type == CDF_Types::CDF_UCHAR); + REQUIRE(shape == std::vector { 3, 3 }); + } + } + } + GIVEN("A list of strings with unicode characters") + { + WHEN("All strings have the same length") + { + auto [cdf_type, shape] = run_test("['√abc', '√def', '√ghi']"); + THEN("The inferred type is CDF_CHAR and shape is [3, 6]") + { + REQUIRE(cdf_type == CDF_Types::CDF_UCHAR); + REQUIRE(shape == std::vector { 3, 6 }); + } + } + WHEN("Strings have varying lengths") + { + auto [cdf_type, shape] = run_test( + "['ASCII: ABCDEFG', 'Latin1: ©æêü÷Ƽ®¢¥', 'Chinese: 社安', 'Other: ႡႢႣႤႥႦ']"); + THEN("The inferred type is CDF_CHAR and shape is [4, 28]") + { + REQUIRE(cdf_type == CDF_Types::CDF_UCHAR); + REQUIRE(shape == std::vector { 4, 28 }); + } + } + WHEN("Strings have varying lengths (2D)") + { + auto [cdf_type, shape] = run_test( + "[['ASCII: ABCDEFG', 'Latin1: ©æêü÷Ƽ®¢¥'], ['Chinese: 社安', 'Other: " + "ႡႢႣႤႥႦ']]"); + THEN("The inferred type is CDF_CHAR and shape is [2, 2, 28]") + { + REQUIRE(cdf_type == CDF_Types::CDF_UCHAR); + REQUIRE(shape == std::vector { 2, 2, 28 }); + } + } + } + GIVEN("A list of integers") + { + WHEN("All values are within Int8 range") + { + auto [cdf_type, shape] = run_test("[-100, 0, 100, 127]"); + THEN("The inferred type is CDF_INT1 and shape is [4]") + { + REQUIRE(cdf_type == CDF_Types::CDF_INT1); + REQUIRE(shape == std::vector { 4 }); + } + } + WHEN("Values exceed Int8 but within Int16 range") + { + auto [cdf_type, shape] = run_test("[-200, 0, 200, 30000]"); + THEN("The inferred type is CDF_INT2 and shape is [4]") + { + REQUIRE(cdf_type == CDF_Types::CDF_INT2); + REQUIRE(shape == std::vector { 4 }); + } + } + WHEN("Values exceed Int16 but within Int32 range") + { + auto [cdf_type, shape] = run_test("[-70000, 0, 70000, 2000000000]"); + THEN("The inferred type is CDF_INT4 and shape is [4]") + { + REQUIRE(cdf_type == CDF_Types::CDF_INT4); + REQUIRE(shape == std::vector { 4 }); + } + } + WHEN("Values exceed Int32 range") + { + auto [cdf_type, shape] = run_test("[-5000000000, 0, 5000000000, 9000000000000]"); + THEN("The inferred type is CDF_INT8 and shape is [4]") + { + REQUIRE(cdf_type == CDF_Types::CDF_INT8); + REQUIRE(shape == std::vector { 4 }); + } + } + WHEN("All values are within UInt8 range") + { + auto [cdf_type, shape] = run_test("[0, 100, 200, 255]"); + THEN("The inferred type is CDF_UINT1 and shape is [4]") + { + REQUIRE(cdf_type == CDF_Types::CDF_UINT1); + REQUIRE(shape == std::vector { 4 }); + } + } + WHEN("Values exceed UInt8 but within UInt16 range") + { + auto [cdf_type, shape] = run_test("[0, 1000, 20000, 60000]"); + THEN("The inferred type is CDF_UINT2 and shape is [4]") + { + REQUIRE(cdf_type == CDF_Types::CDF_UINT2); + REQUIRE(shape == std::vector { 4 }); + } + } + WHEN("Values exceed UInt16 but within UInt32 range") + { + auto [cdf_type, shape] = run_test("[0, 100000, 2000000000, 4000000000]"); + THEN("The inferred type is CDF_UINT4 and shape is [4]") + { + REQUIRE(cdf_type == CDF_Types::CDF_UINT4); + REQUIRE(shape == std::vector { 4 }); + } + } + WHEN("Values exceed UInt32 range") + { + auto [cdf_type, shape] = run_test("[0, 10000000000, 20000000000, 0x10000000000]"); + THEN("The inferred type is CDF_INT8 and shape is [4]") + { + REQUIRE(cdf_type == CDF_Types::CDF_INT8); + REQUIRE(shape == std::vector { 4 }); + } + } + } + } + GIVEN("A bunch of broken collections") { - py::globals()["fname"]=std::string(argv[1]); - py::exec(R"( - import sys - print(sys.path) - import pycdfpp - cdf = pycdfpp.load(fname) - [(print(v.type), v.as_array()) for _,v in cdf.items() if v.type is not pycdfpp.CDF_CHAR] - )"); + WHEN("Analyzing a list with unsupported types") + { + THEN("An exception is raised") + { + REQUIRE_THROWS_WITH(run_test("[None, None, None]"), + ContainsSubstring("RuntimeError: Unsupported data type encountered")); + REQUIRE_THROWS_WITH(run_test("[{}, {}, {}]"), + ContainsSubstring("RuntimeError: Unsupported data type encountered")); + REQUIRE_THROWS_WITH(run_test("[set(), set(), set()]"), + ContainsSubstring("RuntimeError: Unsupported data type encountered")); + } + } + WHEN("Analyzing a list with irregular shapes") + { + THEN("An exception is raised") + { + REQUIRE_THROWS_WITH(run_test("[[1, 2], [3, 4, 5]]"), + ContainsSubstring("IndexError: Inconsistent shapes in nested lists/tuples")); + } + } + WHEN("Analyzing a list with mixed incompatible types") + { + THEN("An exception is raised") + { + REQUIRE_THROWS_WITH(run_test("[[1,2,3], 'string', [4,5,6]]"), + ContainsSubstring("IndexError: Incompatible types in nested lists/tuples")); + REQUIRE_THROWS_WITH(run_test("[ 1, [1,2,3]]"), + ContainsSubstring("IndexError: Incompatible types in nested lists/tuples")); + REQUIRE_THROWS_WITH(run_test("[ 1, datetime(2020,1,1)]"), + ContainsSubstring("IndexError: Incompatible types in nested lists/tuples")); + REQUIRE_THROWS_WITH(run_test("[ datetime(2020,1,1), 1.0]"), + ContainsSubstring("IndexError: Incompatible types in nested lists/tuples")); + } + } } } diff --git a/tests/records_loading/main.cpp b/tests/records_loading/main.cpp index 28fc07b..a1cd7e5 100644 --- a/tests/records_loading/main.cpp +++ b/tests/records_loading/main.cpp @@ -7,12 +7,8 @@ #include -#if __has_include() #include #include -#else -#include -#endif #include "cdfpp/cdf-io/loading/records-loading.hpp" diff --git a/tests/records_saving/main.cpp b/tests/records_saving/main.cpp index 5ab7c50..481b019 100644 --- a/tests/records_saving/main.cpp +++ b/tests/records_saving/main.cpp @@ -7,12 +7,8 @@ #include -#if __has_include() #include #include -#else -#include -#endif #include "cdfpp/cdf-io/saving/records-saving.hpp" diff --git a/tests/rle_compression/main.cpp b/tests/rle_compression/main.cpp index 88bee36..617dbd7 100644 --- a/tests/rle_compression/main.cpp +++ b/tests/rle_compression/main.cpp @@ -1,9 +1,6 @@ -#if __has_include() #include #include -#else -#include -#endif + #include "cdfpp/cdf-io/rle.hpp" #include diff --git a/tests/simple_open/main.cpp b/tests/simple_open/main.cpp index 6e1321a..29d5ac5 100644 --- a/tests/simple_open/main.cpp +++ b/tests/simple_open/main.cpp @@ -7,12 +7,8 @@ #include -#if __has_include() #include #include -#else -#include -#endif #include @@ -580,7 +576,7 @@ SCENARIO("Loading cdf files", "[CDF]") REQUIRE(has_attribute(cd, "Rules_of_use")); REQUIRE(compare_attribute_values( cd.attributes["Rules_of_use"], - u8"Access to the data is provided as-is, without any additional promises to respond quickly to outages, data quality, etc. We request that you acknowledge the Finnish Meteorological Institute, Tromsø Geophysical Observatory of the University of Tromsø, and Tartu Observatory for use of the FMI data." + "Access to the data is provided as-is, without any additional promises to respond quickly to outages, data quality, etc. We request that you acknowledge the Finnish Meteorological Institute, Tromsø Geophysical Observatory of the University of Tromsø, and Tartu Observatory for use of the FMI data." )); } } diff --git a/tests/simple_save/main.cpp b/tests/simple_save/main.cpp index 7990a83..5770b9d 100644 --- a/tests/simple_save/main.cpp +++ b/tests/simple_save/main.cpp @@ -7,12 +7,9 @@ #include -#if __has_include() #include #include -#else -#include -#endif + #include diff --git a/tests/zlib_compression/main.cpp b/tests/zlib_compression/main.cpp index f8feba8..99186f1 100644 --- a/tests/zlib_compression/main.cpp +++ b/tests/zlib_compression/main.cpp @@ -1,9 +1,6 @@ -#if __has_include() #include #include -#else -#include -#endif + #include #ifndef CDFpp_USE_LIBDEFLATE #include "cdfpp/cdf-io/zlib.hpp" diff --git a/tests/zstd_compression/main.cpp b/tests/zstd_compression/main.cpp index 95a871f..f288260 100644 --- a/tests/zstd_compression/main.cpp +++ b/tests/zstd_compression/main.cpp @@ -1,9 +1,6 @@ -#if __has_include() #include #include -#else -#include -#endif + #include #ifdef CDFPP_USE_ZSTD #include "cdfpp/cdf-io/zstd.hpp"