├── tests ├── subprojects │ ├── .gitkeep │ ├── .gitignore │ └── catch2.wrap ├── .gitignore ├── createnpy.py ├── test-write.cpp ├── .clang-tidy ├── meson.build └── test-read.cpp ├── .clang-format ├── meson.build ├── LICENSE ├── .github └── workflows │ └── ci.yml ├── README.md └── include └── npy.hpp /tests/subprojects/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | test 2 | *.npy 3 | -------------------------------------------------------------------------------- /tests/subprojects/.gitignore: -------------------------------------------------------------------------------- 1 | Catch2-* 2 | packagecache 3 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: Google 2 | ColumnLimit: 120 3 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('libnpy', 2 | 'cpp', 3 | version : '1.0.1', 4 | license : 'MIT' 5 | ) 6 | 7 | libnpy_dep = declare_dependency( 8 | include_directories : include_directories('include') 9 | ) 10 | -------------------------------------------------------------------------------- /tests/subprojects/catch2.wrap: -------------------------------------------------------------------------------- 1 | [wrap-file] 2 | directory = Catch2-2.13.8 3 | source_url = https://github.com/catchorg/Catch2/archive/v2.13.8.tar.gz 4 | source_filename = Catch2-2.13.8.tar.gz 5 | source_hash = b9b592bd743c09f13ee4bf35fc30eeee2748963184f6bea836b146e6cc2a585a 6 | patch_filename = catch2_2.13.8-1_patch.zip 7 | patch_url = https://wrapdb.mesonbuild.com/v2/catch2_2.13.8-1/get_patch 8 | patch_hash = 3565968970ce11c1d128aa09771512047f5f9c205d2e341abf33d4a985bd0eb5 9 | 10 | [provide] 11 | catch2 = catch2_dep 12 | 13 | -------------------------------------------------------------------------------- /tests/createnpy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import numpy 4 | import os 5 | 6 | if not os.path.isdir("data"): 7 | os.mkdir("data") 8 | 9 | data_categories = { 10 | "matrix": [ [1, 2, 3], [4, 5, 6], ], 11 | "empty": [ [], [], ], 12 | "scalar": 42, 13 | } 14 | 15 | dtypes = ['f4', 'f8', 16 | 'i1', 'i2', 'i4', 'i8', 17 | 'u1', 'u2', 'u4', 'u8', 18 | 'c8', 'c16', ] 19 | 20 | for d in dtypes: 21 | for data_name, data in data_categories.items(): 22 | a = numpy.array(data, numpy.dtype(d)) 23 | numpy.save(f"data/{data_name}_{d}.npy", a) 24 | numpy.save(f"data/{data_name}_{d}_t.npy", a.T) 25 | 26 | booldata = [ [False, True, False], [True, False, True], ] 27 | a = numpy.array(booldata, numpy.dtype(bool)) 28 | numpy.save("data/bool.npy", a) 29 | numpy.save("data/bool_t.npy", a.T) 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Leon Merten Lohse 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | formatting-check: 13 | name: Formatting Check 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Run clang-format style check for C++ sources. 18 | uses: jidicula/clang-format-action@v4.11.0 19 | with: 20 | clang-format-version: '16' 21 | 22 | linux: 23 | runs-on: ubuntu-latest 24 | defaults: 25 | run: 26 | working-directory: ./tests 27 | steps: 28 | - uses: actions/checkout@v3 29 | - uses: actions/setup-python@v4 30 | with: 31 | python-version: '3.x' 32 | - run: pip install meson ninja 33 | - run: meson setup builddir/ 34 | env: 35 | CC: gcc 36 | - run: pip install numpy 37 | - run: meson test -C builddir/ -v 38 | - uses: actions/upload-artifact@v3 39 | if: failure() 40 | with: 41 | name: Linux_Meson_Testlog 42 | path: tests/builddir/meson-logs/testlog.txt 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /tests/test-write.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "npy.hpp" 7 | 8 | int test_save() { 9 | const std::vector data1{1, 2, 3, 4, 5, 6}; 10 | const std::vector leshape11{2, 3}; 11 | const std::vector leshape12{6}; 12 | 13 | const npy::npy_data data11{data1, leshape11, false}; 14 | write_npy("data/out11.npy", data11); 15 | 16 | const npy::npy_data data12{data1, leshape12, false}; 17 | write_npy("data/out12.npy", data12); 18 | 19 | const npy::npy_data_ptr data11_ptr{data1.data(), leshape11, false}; 20 | write_npy("data/out11_ptr.npy", data11_ptr); 21 | 22 | const std::vector data2{7}; 23 | const std::vector leshape21{1, 1, 1}; 24 | const std::vector leshape22{}; 25 | 26 | const npy::npy_data data21{data2, leshape21, false}; 27 | write_npy("data/out21.npy", data21); 28 | const npy::npy_data data22{data2, leshape22, false}; 29 | write_npy("data/out22.npy", data22); 30 | 31 | const std::vector data3{}; 32 | const std::vector leshape31{4, 0}; 33 | 34 | const npy::npy_data data31{data3, leshape31, false}; 35 | write_npy("data/out31.npy", data31); 36 | 37 | return 0; 38 | } 39 | 40 | int main() { 41 | test_save(); 42 | 43 | return 0; 44 | } 45 | -------------------------------------------------------------------------------- /tests/.clang-tidy: -------------------------------------------------------------------------------- 1 | --- 2 | Checks: 'clang-diagnostic-*,clang-analyzer-*,cppcoreguidelines-*,modernize-*,-modernize-use-trailing-return-type,-cppcoreguidelines-avoid-magic-numbers,-cppcoreguidelines-pro-type-reinterpret-cast' 3 | WarningsAsErrors: true 4 | HeaderFilterRegex: 'include/npy.hpp' 5 | AnalyzeTemporaryDtors: false 6 | FormatStyle: google 7 | CheckOptions: 8 | - key: cert-dcl16-c.NewSuffixes 9 | value: 'L;LL;LU;LLU' 10 | - key: cert-oop54-cpp.WarnOnlyIfThisHasSuspiciousField 11 | value: '0' 12 | - key: cppcoreguidelines-explicit-virtual-functions.IgnoreDestructors 13 | value: '1' 14 | - key: cppcoreguidelines-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic 15 | value: '1' 16 | - key: google-readability-braces-around-statements.ShortStatementLines 17 | value: '1' 18 | - key: google-readability-function-size.StatementThreshold 19 | value: '800' 20 | - key: google-readability-namespace-comments.ShortNamespaceLines 21 | value: '10' 22 | - key: google-readability-namespace-comments.SpacesBeforeComments 23 | value: '2' 24 | - key: modernize-loop-convert.MaxCopySize 25 | value: '16' 26 | - key: modernize-loop-convert.MinConfidence 27 | value: reasonable 28 | - key: modernize-loop-convert.NamingStyle 29 | value: CamelCase 30 | - key: modernize-pass-by-value.IncludeStyle 31 | value: llvm 32 | - key: modernize-replace-auto-ptr.IncludeStyle 33 | value: llvm 34 | - key: modernize-use-nullptr.NullMacros 35 | value: 'NULL' 36 | ... 37 | 38 | -------------------------------------------------------------------------------- /tests/meson.build: -------------------------------------------------------------------------------- 1 | project('libnpy-tests', 'cpp', 2 | default_options : ['cpp_std=c++14', 'warning_level=3']) 3 | 4 | pymod = import('python') 5 | python = pymod.find_installation('python3', required : true) 6 | 7 | 8 | catch2_dep = dependency('catch2') 9 | 10 | datafiles = [ 11 | 'bool.npy', 12 | 'empty_c16.npy', 13 | 'empty_c16_t.npy', 14 | 'empty_c8.npy', 15 | 'empty_c8_t.npy', 16 | 'empty_f4.npy', 17 | 'empty_f4_t.npy', 18 | 'empty_f8.npy', 19 | 'empty_f8_t.npy', 20 | 'empty_i1.npy', 21 | 'empty_i1_t.npy', 22 | 'empty_i2.npy', 23 | 'empty_i2_t.npy', 24 | 'empty_i4.npy', 25 | 'empty_i4_t.npy', 26 | 'empty_i8.npy', 27 | 'empty_i8_t.npy', 28 | 'empty_u1.npy', 29 | 'empty_u1_t.npy', 30 | 'empty_u2.npy', 31 | 'empty_u2_t.npy', 32 | 'empty_u4.npy', 33 | 'empty_u4_t.npy', 34 | 'empty_u8.npy', 35 | 'empty_u8_t.npy', 36 | 'matrix_c16.npy', 37 | 'matrix_c16_t.npy', 38 | 'matrix_c8.npy', 39 | 'matrix_c8_t.npy', 40 | 'matrix_f4.npy', 41 | 'matrix_f4_t.npy', 42 | 'matrix_f8.npy', 43 | 'matrix_f8_t.npy', 44 | 'matrix_i1.npy', 45 | 'matrix_i1_t.npy', 46 | 'matrix_i2.npy', 47 | 'matrix_i2_t.npy', 48 | 'matrix_i4.npy', 49 | 'matrix_i4_t.npy', 50 | 'matrix_i8.npy', 51 | 'matrix_i8_t.npy', 52 | 'matrix_u1.npy', 53 | 'matrix_u1_t.npy', 54 | 'matrix_u2.npy', 55 | 'matrix_u2_t.npy', 56 | 'matrix_u4.npy', 57 | 'matrix_u4_t.npy', 58 | 'matrix_u8.npy', 59 | 'matrix_u8_t.npy', 60 | 'scalar_c16.npy', 61 | 'scalar_c16_t.npy', 62 | 'scalar_c8.npy', 63 | 'scalar_c8_t.npy', 64 | 'scalar_f4.npy', 65 | 'scalar_f4_t.npy', 66 | 'scalar_f8.npy', 67 | 'scalar_f8_t.npy', 68 | 'scalar_i1.npy', 69 | 'scalar_i1_t.npy', 70 | 'scalar_i2.npy', 71 | 'scalar_i2_t.npy', 72 | 'scalar_i4.npy', 73 | 'scalar_i4_t.npy', 74 | 'scalar_i8.npy', 75 | 'scalar_i8_t.npy', 76 | 'scalar_u1.npy', 77 | 'scalar_u1_t.npy', 78 | 'scalar_u2.npy', 79 | 'scalar_u2_t.npy', 80 | 'scalar_u4.npy', 81 | 'scalar_u4_t.npy', 82 | 'scalar_u8.npy', 83 | 'scalar_u8_t.npy', ] 84 | 85 | gen_data = custom_target('gen-data', 86 | input : 'createnpy.py', 87 | output : datafiles, 88 | command : [python, '@INPUT@']) 89 | 90 | libnpy_inc = include_directories('../include') 91 | #libnpy_dep = declare_dependency(include_directories : libnpy_inc) 92 | 93 | read_src = ['test-read.cpp', '../include/npy.hpp'] 94 | read_exe = executable('test-read', sources : read_src, 95 | include_directories : libnpy_inc, 96 | dependencies : catch2_dep) 97 | 98 | write_src = ['test-write.cpp', '../include/npy.hpp'] 99 | write_exe = executable('test-write', sources : write_src, 100 | include_directories : libnpy_inc, 101 | dependencies : catch2_dep) 102 | 103 | test('test read', read_exe, depends : gen_data) 104 | test('test write', write_exe, depends : gen_data) 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # libnpy 2 | 3 | libnpy is a simple C++ library for reading and writing of numpy's [.npy files](https://docs.scipy.org/doc/numpy/neps/npy-format.html). 4 | 5 | Refer to [format.py](https://github.com/numpy/numpy/blob/master/numpy/lib/format.py) for a detailed description of the .npy format. 6 | 7 | This libraries primary purpose is *writing* numerical data easily and efficiently into the .npy format. 8 | It also allows *reading* .npy files, although only a very limited subset of data types are supported. 9 | 10 | ## Features 11 | - Writing C++ vectors (std::vector) to .npy files 12 | - Reading (some) simple .npy files into C++ vectors 13 | 14 | ## Supported data types and mapping 15 | Only *scalar* *numeric* data types are supported. There is no natural way to represent more complex objects and parsing the header becomes tremendously more complex. 16 | Supported types: 17 | - unsigned integer 18 | - signed integer 19 | - floating point 20 | - complex floating point (std::complex, ...) 21 | 22 | ## Usage 23 | libnpy is a header only library. You only need to download `npy.hpp` into your include path. Neither special compiler flags nor a specific build system are required. 24 | 25 | Optional: If you use meson, you can use the provided `meson.build` file to declare the dependency on libnpy. 26 | 27 | The API has changed in the last release. The old C-style API is still available, but might get removed in the future. 28 | 29 | ### Reading data: 30 | ```c++ 31 | #include "npy.hpp" 32 | #include 33 | #include 34 | 35 | int main() { 36 | 37 | const std::string path {"data.npy"}; 38 | npy::npy_data d = npy::read_npy(path); 39 | 40 | std::vector data = d.data; 41 | std::vector shape = d.shape; 42 | bool fortran_order = d.fortran_order; 43 | } 44 | 45 | ``` 46 | 47 | ### Writing data: 48 | ```c++ 49 | #include "npy.hpp" 50 | #include 51 | #include 52 | 53 | int main() { 54 | const std::vector data{1, 2, 3, 4, 5, 6}; 55 | 56 | npy::npy_data d; 57 | d.data = data; 58 | d.shape = {2, 3}; 59 | d.fortran_order = false; // optional 60 | 61 | const std::string path{"out.npy"}; 62 | npy::write_npy(path, d); 63 | } 64 | 65 | ``` 66 | 67 | This will involve an additional copy of the data, which might be undesireable for larger data. The copy can be avoided by using `npy::npy_data_ptr` as follows. 68 | 69 | ```c++ 70 | #include "npy.hpp" 71 | #include 72 | #include 73 | 74 | int main() { 75 | const std::vector data{1, 2, 3, 4, 5, 6}; 76 | 77 | npy::npy_data_ptr d; 78 | d.data_ptr = data.data(); 79 | d.shape = {2, 3}; 80 | d.fortran_order = false; // optional 81 | 82 | const std::string path{"out.npy"}; 83 | npy::write_npy(path, d); 84 | } 85 | 86 | ``` 87 | 88 | See `test/` for further examples. 89 | C++14 is required. 90 | 91 | ## Tests 92 | The tests can be build with `meson>=0.55` and depend on catch2. 93 | ``` 94 | cd tests 95 | meson setup builddir 96 | meson test -Cbuilddir 97 | ``` 98 | 99 | ## Known limitations 100 | 1. Only a few data types are supported. 101 | 102 | 2. The numpy header is a literal Python dictionary and the Python syntax is very permissive. libnpy's parser was only tested with numpy's implemenation of the .npy format. 103 | 104 | ## Contributing 105 | Feel free to send me a pull request, open an issue, or contact me directly. 106 | 107 | The code is formatted with clang-format. 108 | Please test your changes by running the tests and static analysis. 109 | Meson automatically builds a target for clang-tidy: 110 | ``` 111 | cd tests 112 | meson setup builddir 113 | ninja -C builddir clang-tidy 114 | ``` 115 | 116 | ## License 117 | The project is licensed under the [MIT](LICENSE) license 118 | -------------------------------------------------------------------------------- /tests/test-read.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "npy.hpp" 9 | 10 | #define CATCH_CONFIG_MAIN 11 | #include "catch2/catch.hpp" 12 | 13 | const std::vector expect_shape{2, 3}; 14 | const std::vector expect_shape_t{3, 2}; 15 | const std::vector expect_data_int{1, 2, 3, 4, 5, 6}; 16 | const std::vector expect_data_bool{false, true, false, true, false, true}; 17 | const int expect_scalar = 42; 18 | 19 | template 20 | std::vector cast_data(const std::vector &input) { 21 | std::vector output; 22 | output.resize(input.size()); 23 | std::transform(input.cbegin(), input.cend(), output.begin(), [](int i) { return static_cast(i); }); 24 | return output; 25 | } 26 | 27 | TEST_CASE("Load single precision data", "[read]") { 28 | auto d = npy::read_npy("data/matrix_f4.npy"); 29 | 30 | REQUIRE(d.shape == expect_shape); 31 | REQUIRE(d.data == cast_data(expect_data_int)); 32 | REQUIRE(d.fortran_order == false); 33 | 34 | auto dt = npy::read_npy("data/matrix_f4_t.npy"); 35 | 36 | REQUIRE(dt.shape == expect_shape_t); 37 | REQUIRE(dt.data == cast_data(expect_data_int)); 38 | REQUIRE(dt.fortran_order == true); 39 | } 40 | 41 | TEST_CASE("Load double precision data", "[read]") { 42 | auto d = npy::read_npy("data/matrix_f8.npy"); 43 | 44 | REQUIRE(d.shape == expect_shape); 45 | REQUIRE(d.data == cast_data(expect_data_int)); 46 | REQUIRE(d.fortran_order == false); 47 | 48 | auto dt = npy::read_npy("data/matrix_f8_t.npy"); 49 | 50 | REQUIRE(dt.shape == expect_shape_t); 51 | REQUIRE(dt.data == cast_data(expect_data_int)); 52 | REQUIRE(dt.fortran_order == true); 53 | } 54 | 55 | TEST_CASE("Load single precision complex data", "[read]") { 56 | auto d = npy::read_npy>("data/matrix_c8.npy"); 57 | 58 | REQUIRE(d.shape == expect_shape); 59 | REQUIRE(d.data == cast_data>(expect_data_int)); 60 | REQUIRE(d.fortran_order == false); 61 | 62 | auto dt = npy::read_npy>("data/matrix_c8_t.npy"); 63 | 64 | REQUIRE(dt.shape == expect_shape_t); 65 | REQUIRE(dt.data == cast_data>(expect_data_int)); 66 | REQUIRE(dt.fortran_order == true); 67 | } 68 | 69 | TEST_CASE("Load double precision complex data", "[read]") { 70 | auto d = npy::read_npy>("data/matrix_c16.npy"); 71 | 72 | REQUIRE(d.shape == expect_shape); 73 | REQUIRE(d.data == cast_data>(expect_data_int)); 74 | REQUIRE(d.fortran_order == false); 75 | 76 | auto dt = npy::read_npy>("data/matrix_c16_t.npy"); 77 | 78 | REQUIRE(dt.shape == expect_shape_t); 79 | REQUIRE(dt.data == cast_data>(expect_data_int)); 80 | REQUIRE(dt.fortran_order == true); 81 | } 82 | 83 | TEST_CASE("Load int8 data", "[read]") { 84 | auto d = npy::read_npy("data/matrix_i1.npy"); 85 | 86 | REQUIRE(d.shape == expect_shape); 87 | REQUIRE(d.data == cast_data(expect_data_int)); 88 | REQUIRE(d.fortran_order == false); 89 | 90 | auto dt = npy::read_npy("data/matrix_i1_t.npy"); 91 | 92 | REQUIRE(dt.shape == expect_shape_t); 93 | REQUIRE(dt.data == cast_data(expect_data_int)); 94 | REQUIRE(dt.fortran_order == true); 95 | } 96 | 97 | TEST_CASE("Load int16 data", "[read]") { 98 | auto d = npy::read_npy("data/matrix_i2.npy"); 99 | 100 | REQUIRE(d.shape == expect_shape); 101 | REQUIRE(d.data == cast_data(expect_data_int)); 102 | REQUIRE(d.fortran_order == false); 103 | 104 | auto dt = npy::read_npy("data/matrix_i2_t.npy"); 105 | 106 | REQUIRE(dt.shape == expect_shape_t); 107 | REQUIRE(dt.data == cast_data(expect_data_int)); 108 | REQUIRE(dt.fortran_order == true); 109 | } 110 | 111 | TEST_CASE("Load int32 data", "[read]") { 112 | auto d = npy::read_npy("data/matrix_i4.npy"); 113 | 114 | REQUIRE(d.shape == expect_shape); 115 | REQUIRE(d.data == cast_data(expect_data_int)); 116 | REQUIRE(d.fortran_order == false); 117 | 118 | auto dt = npy::read_npy("data/matrix_i4_t.npy"); 119 | 120 | REQUIRE(dt.shape == expect_shape_t); 121 | REQUIRE(dt.data == cast_data(expect_data_int)); 122 | REQUIRE(dt.fortran_order == true); 123 | } 124 | 125 | TEST_CASE("Load int64 data", "[read]") { 126 | auto d = npy::read_npy("data/matrix_i8.npy"); 127 | 128 | REQUIRE(d.shape == expect_shape); 129 | REQUIRE(d.data == cast_data(expect_data_int)); 130 | REQUIRE(d.fortran_order == false); 131 | 132 | auto dt = npy::read_npy("data/matrix_i8_t.npy"); 133 | 134 | REQUIRE(dt.shape == expect_shape_t); 135 | REQUIRE(dt.data == cast_data(expect_data_int)); 136 | REQUIRE(dt.fortran_order == true); 137 | } 138 | 139 | TEST_CASE("Load uint8 data", "[read]") { 140 | auto d = npy::read_npy("data/matrix_u1.npy"); 141 | 142 | REQUIRE(d.shape == expect_shape); 143 | REQUIRE(d.data == cast_data(expect_data_int)); 144 | REQUIRE(d.fortran_order == false); 145 | 146 | auto dt = npy::read_npy("data/matrix_u1_t.npy"); 147 | 148 | REQUIRE(dt.shape == expect_shape_t); 149 | REQUIRE(dt.data == cast_data(expect_data_int)); 150 | REQUIRE(dt.fortran_order == true); 151 | } 152 | 153 | TEST_CASE("Load uint16 data", "[read]") { 154 | auto d = npy::read_npy("data/matrix_u2.npy"); 155 | 156 | REQUIRE(d.shape == expect_shape); 157 | REQUIRE(d.data == cast_data(expect_data_int)); 158 | REQUIRE(d.fortran_order == false); 159 | 160 | auto dt = npy::read_npy("data/matrix_u2_t.npy"); 161 | 162 | REQUIRE(dt.shape == expect_shape_t); 163 | REQUIRE(dt.data == cast_data(expect_data_int)); 164 | REQUIRE(dt.fortran_order == true); 165 | } 166 | 167 | TEST_CASE("Load uint32 data", "[read]") { 168 | auto d = npy::read_npy("data/matrix_u4.npy"); 169 | 170 | REQUIRE(d.shape == expect_shape); 171 | REQUIRE(d.data == cast_data(expect_data_int)); 172 | REQUIRE(d.fortran_order == false); 173 | 174 | auto dt = npy::read_npy("data/matrix_u4_t.npy"); 175 | 176 | REQUIRE(dt.shape == expect_shape_t); 177 | REQUIRE(dt.data == cast_data(expect_data_int)); 178 | REQUIRE(dt.fortran_order == true); 179 | } 180 | 181 | TEST_CASE("Load uint64 data", "[read]") { 182 | auto d = npy::read_npy("data/matrix_u8.npy"); 183 | 184 | REQUIRE(d.shape == expect_shape); 185 | REQUIRE(d.data == cast_data(expect_data_int)); 186 | REQUIRE(d.fortran_order == false); 187 | 188 | auto dt = npy::read_npy("data/matrix_u8_t.npy"); 189 | 190 | REQUIRE(dt.shape == expect_shape_t); 191 | REQUIRE(dt.data == cast_data(expect_data_int)); 192 | REQUIRE(dt.fortran_order == true); 193 | } 194 | 195 | /* 196 | * bool not supported 197 | TEST_CASE( "Load bool data", "[read]" ) { 198 | auto d = npy::read_npy("data/bool.npy"); 199 | 200 | REQUIRE(d.shape == expect_shape); 201 | REQUIRE(d.data == expect_data_bool); 202 | REQUIRE(d.fortran_order == false); 203 | 204 | auto dt = npy::read_npy("data/bool_t.npy"); 205 | 206 | REQUIRE(dt.shape == expect_shape_t); 207 | REQUIRE(d.data == expect_data_bool); 208 | REQUIRE(dt.fortran_order == true); 209 | } 210 | */ 211 | 212 | TEST_CASE("Load _scalar_ single precision data", "[read]") { 213 | auto d = npy::read_npy("data/scalar_f4.npy"); 214 | 215 | REQUIRE(d.shape.size() == 0); 216 | REQUIRE(d.data.size() == 1); 217 | REQUIRE(d.data[0] == static_cast(expect_scalar)); 218 | REQUIRE(d.fortran_order == false); 219 | 220 | auto dt = npy::read_npy("data/scalar_f4_t.npy"); 221 | 222 | REQUIRE(dt.shape.size() == 0); 223 | REQUIRE(dt.data.size() == 1); 224 | REQUIRE(dt.data[0] == static_cast(expect_scalar)); 225 | REQUIRE(dt.fortran_order == false); 226 | } 227 | 228 | TEST_CASE("Load _empty_ single precision data", "[read]") { 229 | auto d = npy::read_npy("data/empty_f4.npy"); 230 | 231 | REQUIRE(d.shape.size() == 2); 232 | REQUIRE(d.data.size() == 0); 233 | REQUIRE(d.fortran_order == false); 234 | 235 | auto dt = npy::read_npy("data/empty_f4_t.npy"); 236 | 237 | REQUIRE(dt.shape.size() == 2); 238 | REQUIRE(dt.data.size() == 0); 239 | REQUIRE(dt.fortran_order == false); 240 | } 241 | 242 | TEST_CASE("Load _empty_ single precision data with wrong data type", "[read]") { 243 | REQUIRE_THROWS_WITH([]() { auto d = npy::read_npy("data/empty_i8.npy"); }(), 244 | "formatting error: typestrings not matching"); 245 | } 246 | -------------------------------------------------------------------------------- /include/npy.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017-2023 Leon Merten Lohse 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | 23 | #ifndef NPY_HPP_ 24 | #define NPY_HPP_ 25 | 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | #include 40 | #include 41 | #include 42 | #include 43 | 44 | namespace npy { 45 | 46 | /* Compile-time test for byte order. 47 | If your compiler does not define these per default, you may want to define 48 | one of these constants manually. 49 | Defaults to little endian order. */ 50 | #if defined(__BYTE_ORDER) && __BYTE_ORDER == __BIG_ENDIAN || defined(__BIG_ENDIAN__) || defined(__ARMEB__) || \ 51 | defined(__THUMBEB__) || defined(__AARCH64EB__) || defined(_MIBSEB) || defined(__MIBSEB) || defined(__MIBSEB__) 52 | const bool big_endian = true; 53 | #else 54 | const bool big_endian = false; 55 | #endif 56 | 57 | const size_t magic_string_length = 6; 58 | const std::array magic_string = {'\x93', 'N', 'U', 'M', 'P', 'Y'}; 59 | 60 | const char little_endian_char = '<'; 61 | const char big_endian_char = '>'; 62 | const char no_endian_char = '|'; 63 | 64 | constexpr std::array endian_chars = {little_endian_char, big_endian_char, no_endian_char}; 65 | constexpr std::array numtype_chars = {'f', 'i', 'u', 'c'}; 66 | 67 | constexpr char host_endian_char = (big_endian ? big_endian_char : little_endian_char); 68 | 69 | /* npy array length */ 70 | using ndarray_len_t = unsigned long int; 71 | using shape_t = std::vector; 72 | 73 | using version_t = std::pair; 74 | 75 | struct dtype_t { 76 | char byteorder; 77 | char kind; 78 | unsigned int itemsize; 79 | 80 | inline std::string str() const { 81 | std::stringstream ss; 82 | ss << byteorder << kind << itemsize; 83 | return ss.str(); 84 | } 85 | 86 | inline std::tuple tie() const { 87 | return std::tie(byteorder, kind, itemsize); 88 | } 89 | }; 90 | 91 | struct header_t { 92 | dtype_t dtype; 93 | bool fortran_order; 94 | shape_t shape; 95 | }; 96 | 97 | inline void write_magic(std::ostream &ostream, version_t version) { 98 | ostream.write(magic_string.data(), magic_string_length); 99 | ostream.put(version.first); 100 | ostream.put(version.second); 101 | } 102 | 103 | inline version_t read_magic(std::istream &istream) { 104 | std::array buf{}; 105 | istream.read(buf.data(), sizeof(buf)); 106 | 107 | if (!istream) { 108 | throw std::runtime_error("io error: failed reading file"); 109 | } 110 | 111 | if (!std::equal(magic_string.begin(), magic_string.end(), buf.begin())) 112 | throw std::runtime_error("this file does not have a valid npy format."); 113 | 114 | version_t version; 115 | version.first = buf[magic_string_length]; 116 | version.second = buf[magic_string_length + 1]; 117 | 118 | return version; 119 | } 120 | 121 | const std::unordered_map dtype_map = { 122 | {std::type_index(typeid(float)), {host_endian_char, 'f', sizeof(float)}}, 123 | {std::type_index(typeid(double)), {host_endian_char, 'f', sizeof(double)}}, 124 | {std::type_index(typeid(long double)), {host_endian_char, 'f', sizeof(long double)}}, 125 | {std::type_index(typeid(char)), {no_endian_char, 'i', sizeof(char)}}, 126 | {std::type_index(typeid(signed char)), {no_endian_char, 'i', sizeof(signed char)}}, 127 | {std::type_index(typeid(short)), {host_endian_char, 'i', sizeof(short)}}, 128 | {std::type_index(typeid(int)), {host_endian_char, 'i', sizeof(int)}}, 129 | {std::type_index(typeid(long)), {host_endian_char, 'i', sizeof(long)}}, 130 | {std::type_index(typeid(long long)), {host_endian_char, 'i', sizeof(long long)}}, 131 | {std::type_index(typeid(unsigned char)), {no_endian_char, 'u', sizeof(unsigned char)}}, 132 | {std::type_index(typeid(unsigned short)), {host_endian_char, 'u', sizeof(unsigned short)}}, 133 | {std::type_index(typeid(unsigned int)), {host_endian_char, 'u', sizeof(unsigned int)}}, 134 | {std::type_index(typeid(unsigned long)), {host_endian_char, 'u', sizeof(unsigned long)}}, 135 | {std::type_index(typeid(unsigned long long)), {host_endian_char, 'u', sizeof(unsigned long long)}}, 136 | {std::type_index(typeid(std::complex)), {host_endian_char, 'c', sizeof(std::complex)}}, 137 | {std::type_index(typeid(std::complex)), {host_endian_char, 'c', sizeof(std::complex)}}, 138 | {std::type_index(typeid(std::complex)), {host_endian_char, 'c', sizeof(std::complex)}}}; 139 | 140 | // helpers 141 | inline bool is_digits(const std::string &str) { return std::all_of(str.begin(), str.end(), ::isdigit); } 142 | 143 | template 144 | inline bool in_array(T val, const std::array &arr) { 145 | return std::find(std::begin(arr), std::end(arr), val) != std::end(arr); 146 | } 147 | 148 | inline dtype_t parse_descr(std::string typestring) { 149 | if (typestring.length() < 3) { 150 | throw std::runtime_error("invalid typestring (length)"); 151 | } 152 | 153 | char byteorder_c = typestring.at(0); 154 | char kind_c = typestring.at(1); 155 | std::string itemsize_s = typestring.substr(2); 156 | 157 | if (!in_array(byteorder_c, endian_chars)) { 158 | throw std::runtime_error("invalid typestring (byteorder)"); 159 | } 160 | 161 | if (!in_array(kind_c, numtype_chars)) { 162 | throw std::runtime_error("invalid typestring (kind)"); 163 | } 164 | 165 | if (!is_digits(itemsize_s)) { 166 | throw std::runtime_error("invalid typestring (itemsize)"); 167 | } 168 | unsigned int itemsize = std::stoul(itemsize_s); 169 | 170 | return {byteorder_c, kind_c, itemsize}; 171 | } 172 | 173 | namespace pyparse { 174 | 175 | /** 176 | Removes leading and trailing whitespaces 177 | */ 178 | inline std::string trim(const std::string &str) { 179 | const std::string whitespace = " \t"; 180 | auto begin = str.find_first_not_of(whitespace); 181 | 182 | if (begin == std::string::npos) return ""; 183 | 184 | auto end = str.find_last_not_of(whitespace); 185 | 186 | return str.substr(begin, end - begin + 1); 187 | } 188 | 189 | inline std::string get_value_from_map(const std::string &mapstr) { 190 | size_t sep_pos = mapstr.find_first_of(":"); 191 | if (sep_pos == std::string::npos) return ""; 192 | 193 | std::string tmp = mapstr.substr(sep_pos + 1); 194 | return trim(tmp); 195 | } 196 | 197 | /** 198 | Parses the string representation of a Python dict 199 | 200 | The keys need to be known and may not appear anywhere else in the data. 201 | */ 202 | inline std::unordered_map parse_dict(std::string in, const std::vector &keys) { 203 | std::unordered_map map; 204 | 205 | if (keys.size() == 0) return map; 206 | 207 | in = trim(in); 208 | 209 | // unwrap dictionary 210 | if ((in.front() == '{') && (in.back() == '}')) 211 | in = in.substr(1, in.length() - 2); 212 | else 213 | throw std::runtime_error("Not a Python dictionary."); 214 | 215 | std::vector> positions; 216 | 217 | for (auto const &value : keys) { 218 | size_t pos = in.find("'" + value + "'"); 219 | 220 | if (pos == std::string::npos) throw std::runtime_error("Missing '" + value + "' key."); 221 | 222 | std::pair position_pair{pos, value}; 223 | positions.push_back(position_pair); 224 | } 225 | 226 | // sort by position in dict 227 | std::sort(positions.begin(), positions.end()); 228 | 229 | for (size_t i = 0; i < positions.size(); ++i) { 230 | std::string raw_value; 231 | size_t begin{positions[i].first}; 232 | size_t end{std::string::npos}; 233 | 234 | std::string key = positions[i].second; 235 | 236 | if (i + 1 < positions.size()) end = positions[i + 1].first; 237 | 238 | raw_value = in.substr(begin, end - begin); 239 | 240 | raw_value = trim(raw_value); 241 | 242 | if (raw_value.back() == ',') raw_value.pop_back(); 243 | 244 | map[key] = get_value_from_map(raw_value); 245 | } 246 | 247 | return map; 248 | } 249 | 250 | /** 251 | Parses the string representation of a Python boolean 252 | */ 253 | inline bool parse_bool(const std::string &in) { 254 | if (in == "True") return true; 255 | if (in == "False") return false; 256 | 257 | throw std::runtime_error("Invalid python boolan."); 258 | } 259 | 260 | /** 261 | Parses the string representation of a Python str 262 | */ 263 | inline std::string parse_str(const std::string &in) { 264 | if ((in.front() == '\'') && (in.back() == '\'')) return in.substr(1, in.length() - 2); 265 | 266 | throw std::runtime_error("Invalid python string."); 267 | } 268 | 269 | /** 270 | Parses the string represenatation of a Python tuple into a vector of its items 271 | */ 272 | inline std::vector parse_tuple(std::string in) { 273 | std::vector v; 274 | const char seperator = ','; 275 | 276 | in = trim(in); 277 | 278 | if ((in.front() == '(') && (in.back() == ')')) 279 | in = in.substr(1, in.length() - 2); 280 | else 281 | throw std::runtime_error("Invalid Python tuple."); 282 | 283 | std::istringstream iss(in); 284 | 285 | for (std::string token; std::getline(iss, token, seperator);) { 286 | v.push_back(token); 287 | } 288 | 289 | return v; 290 | } 291 | 292 | template 293 | inline std::string write_tuple(const std::vector &v) { 294 | if (v.size() == 0) return "()"; 295 | 296 | std::ostringstream ss; 297 | ss.imbue(std::locale("C")); 298 | 299 | if (v.size() == 1) { 300 | ss << "(" << v.front() << ",)"; 301 | } else { 302 | const std::string delimiter = ", "; 303 | // v.size() > 1 304 | ss << "("; 305 | std::copy(v.begin(), v.end() - 1, std::ostream_iterator(ss, delimiter.c_str())); 306 | ss << v.back(); 307 | ss << ")"; 308 | } 309 | 310 | return ss.str(); 311 | } 312 | 313 | inline std::string write_boolean(bool b) { 314 | if (b) 315 | return "True"; 316 | else 317 | return "False"; 318 | } 319 | 320 | } // namespace pyparse 321 | 322 | inline header_t parse_header(std::string header) { 323 | /* 324 | The first 6 bytes are a magic string: exactly "x93NUMPY". 325 | The next 1 byte is an unsigned byte: the major version number of the file 326 | format, e.g. x01. The next 1 byte is an unsigned byte: the minor version 327 | number of the file format, e.g. x00. Note: the version of the file format 328 | is not tied to the version of the numpy package. The next 2 bytes form a 329 | little-endian unsigned short int: the length of the header data HEADER_LEN. 330 | The next HEADER_LEN bytes form the header data describing the array's 331 | format. It is an ASCII string which contains a Python literal expression of 332 | a dictionary. It is terminated by a newline ('n') and padded with spaces 333 | ('x20') to make the total length of the magic string + 4 + HEADER_LEN be 334 | evenly divisible by 16 for alignment purposes. The dictionary contains 335 | three keys: 336 | 337 | "descr" : dtype.descr 338 | An object that can be passed as an argument to the numpy.dtype() 339 | constructor to create the array's dtype. "fortran_order" : bool Whether the 340 | array data is Fortran-contiguous or not. Since Fortran-contiguous arrays 341 | are a common form of non-C-contiguity, we allow them to be written directly 342 | to disk for efficiency. "shape" : tuple of int The shape of the array. For 343 | repeatability and readability, this dictionary is formatted using 344 | pprint.pformat() so the keys are in alphabetic order. 345 | */ 346 | 347 | // remove trailing newline 348 | if (header.back() != '\n') throw std::runtime_error("invalid header"); 349 | header.pop_back(); 350 | 351 | // parse the dictionary 352 | std::vector keys{"descr", "fortran_order", "shape"}; 353 | auto dict_map = npy::pyparse::parse_dict(header, keys); 354 | 355 | if (dict_map.size() == 0) throw std::runtime_error("invalid dictionary in header"); 356 | 357 | std::string descr_s = dict_map["descr"]; 358 | std::string fortran_s = dict_map["fortran_order"]; 359 | std::string shape_s = dict_map["shape"]; 360 | 361 | std::string descr = npy::pyparse::parse_str(descr_s); 362 | dtype_t dtype = parse_descr(descr); 363 | 364 | // convert literal Python bool to C++ bool 365 | bool fortran_order = npy::pyparse::parse_bool(fortran_s); 366 | 367 | // parse the shape tuple 368 | auto shape_v = npy::pyparse::parse_tuple(shape_s); 369 | 370 | shape_t shape; 371 | for (auto item : shape_v) { 372 | auto dim = static_cast(std::stoul(item)); 373 | shape.push_back(dim); 374 | } 375 | 376 | return {dtype, fortran_order, shape}; 377 | } 378 | 379 | inline std::string write_header_dict(const std::string &descr, bool fortran_order, const shape_t &shape) { 380 | std::string s_fortran_order = npy::pyparse::write_boolean(fortran_order); 381 | std::string shape_s = npy::pyparse::write_tuple(shape); 382 | 383 | return "{'descr': '" + descr + "', 'fortran_order': " + s_fortran_order + ", 'shape': " + shape_s + ", }"; 384 | } 385 | 386 | inline void write_header(std::ostream &out, const header_t &header) { 387 | std::string header_dict = write_header_dict(header.dtype.str(), header.fortran_order, header.shape); 388 | 389 | size_t length = magic_string_length + 2 + 2 + header_dict.length() + 1; 390 | 391 | version_t version{1, 0}; 392 | if (length >= 255 * 255) { 393 | length = magic_string_length + 2 + 4 + header_dict.length() + 1; 394 | version = {2, 0}; 395 | } 396 | size_t padding_len = 16 - length % 16; 397 | std::string padding(padding_len, ' '); 398 | 399 | // write magic 400 | write_magic(out, version); 401 | 402 | // write header length 403 | if (version == version_t{1, 0}) { 404 | auto header_len = static_cast(header_dict.length() + padding.length() + 1); 405 | 406 | std::array header_len_le16{static_cast((header_len >> 0) & 0xff), 407 | static_cast((header_len >> 8) & 0xff)}; 408 | out.write(reinterpret_cast(header_len_le16.data()), 2); 409 | } else { 410 | auto header_len = static_cast(header_dict.length() + padding.length() + 1); 411 | 412 | std::array header_len_le32{ 413 | static_cast((header_len >> 0) & 0xff), static_cast((header_len >> 8) & 0xff), 414 | static_cast((header_len >> 16) & 0xff), static_cast((header_len >> 24) & 0xff)}; 415 | out.write(reinterpret_cast(header_len_le32.data()), 4); 416 | } 417 | 418 | out << header_dict << padding << '\n'; 419 | } 420 | 421 | inline std::string read_header(std::istream &istream) { 422 | // check magic bytes an version number 423 | version_t version = read_magic(istream); 424 | 425 | uint32_t header_length = 0; 426 | if (version == version_t{1, 0}) { 427 | std::array header_len_le16{}; 428 | istream.read(reinterpret_cast(header_len_le16.data()), 2); 429 | header_length = (header_len_le16[0] << 0) | (header_len_le16[1] << 8); 430 | 431 | if ((magic_string_length + 2 + 2 + header_length) % 16 != 0) { 432 | // TODO(llohse): display warning 433 | } 434 | } else if (version == version_t{2, 0}) { 435 | std::array header_len_le32{}; 436 | istream.read(reinterpret_cast(header_len_le32.data()), 4); 437 | 438 | header_length = 439 | (header_len_le32[0] << 0) | (header_len_le32[1] << 8) | (header_len_le32[2] << 16) | (header_len_le32[3] << 24); 440 | 441 | if ((magic_string_length + 2 + 4 + header_length) % 16 != 0) { 442 | // TODO(llohse): display warning 443 | } 444 | } else { 445 | throw std::runtime_error("unsupported file format version"); 446 | } 447 | 448 | auto buf_v = std::vector(header_length); 449 | istream.read(buf_v.data(), header_length); 450 | std::string header(buf_v.data(), header_length); 451 | 452 | return header; 453 | } 454 | 455 | inline ndarray_len_t comp_size(const shape_t &shape) { 456 | ndarray_len_t size = 1; 457 | for (ndarray_len_t i : shape) size *= i; 458 | 459 | return size; 460 | } 461 | 462 | template 463 | struct npy_data { 464 | std::vector data = {}; 465 | shape_t shape = {}; 466 | bool fortran_order = false; 467 | }; 468 | 469 | template 470 | struct npy_data_ptr { 471 | const Scalar *data_ptr = nullptr; 472 | shape_t shape = {}; 473 | bool fortran_order = false; 474 | }; 475 | 476 | template 477 | inline npy_data read_npy(std::istream &in) { 478 | std::string header_s = read_header(in); 479 | 480 | // parse header 481 | header_t header = parse_header(header_s); 482 | 483 | // check if the typestring matches the given one 484 | const dtype_t dtype = dtype_map.at(std::type_index(typeid(Scalar))); 485 | 486 | if (header.dtype.tie() != dtype.tie()) { 487 | throw std::runtime_error("formatting error: typestrings not matching"); 488 | } 489 | 490 | // compute the data size based on the shape 491 | auto size = static_cast(comp_size(header.shape)); 492 | 493 | npy_data data; 494 | 495 | data.shape = header.shape; 496 | data.fortran_order = header.fortran_order; 497 | 498 | data.data.resize(size); 499 | 500 | // read the data 501 | in.read(reinterpret_cast(data.data.data()), sizeof(Scalar) * size); 502 | 503 | return data; 504 | } 505 | 506 | template 507 | inline npy_data read_npy(const std::string &filename) { 508 | std::ifstream stream(filename, std::ifstream::binary); 509 | if (!stream) { 510 | throw std::runtime_error("io error: failed to open a file."); 511 | } 512 | 513 | return read_npy(stream); 514 | } 515 | 516 | template 517 | inline void write_npy(std::ostream &out, const npy_data &data) { 518 | // static_assert(has_typestring::value, "scalar type not 519 | // understood"); 520 | const dtype_t dtype = dtype_map.at(std::type_index(typeid(Scalar))); 521 | 522 | header_t header{dtype, data.fortran_order, data.shape}; 523 | write_header(out, header); 524 | 525 | auto size = static_cast(comp_size(data.shape)); 526 | 527 | out.write(reinterpret_cast(data.data.data()), sizeof(Scalar) * size); 528 | } 529 | 530 | template 531 | inline void write_npy(const std::string &filename, const npy_data &data) { 532 | std::ofstream stream(filename, std::ofstream::binary); 533 | if (!stream) { 534 | throw std::runtime_error("io error: failed to open a file."); 535 | } 536 | 537 | write_npy(stream, data); 538 | } 539 | 540 | template 541 | inline void write_npy(std::ostream &out, const npy_data_ptr &data_ptr) { 542 | const dtype_t dtype = dtype_map.at(std::type_index(typeid(Scalar))); 543 | 544 | header_t header{dtype, data_ptr.fortran_order, data_ptr.shape}; 545 | write_header(out, header); 546 | 547 | auto size = static_cast(comp_size(data_ptr.shape)); 548 | 549 | out.write(reinterpret_cast(data_ptr.data_ptr), sizeof(Scalar) * size); 550 | } 551 | 552 | template 553 | inline void write_npy(const std::string &filename, const npy_data_ptr &data_ptr) { 554 | std::ofstream stream(filename, std::ofstream::binary); 555 | if (!stream) { 556 | throw std::runtime_error("io error: failed to open a file."); 557 | } 558 | 559 | write_npy(stream, data_ptr); 560 | } 561 | 562 | // old interface 563 | 564 | // NOLINTBEGIN(*-avoid-c-arrays) 565 | template 566 | inline void SaveArrayAsNumpy(const std::string &filename, bool fortran_order, unsigned int n_dims, 567 | const unsigned long shape[], const Scalar *data) { 568 | const npy_data_ptr ptr{data, {shape, shape + n_dims}, fortran_order}; 569 | 570 | write_npy(filename, ptr); 571 | } 572 | 573 | template 574 | inline void SaveArrayAsNumpy(const std::string &filename, bool fortran_order, unsigned int n_dims, 575 | const unsigned long shape[], const std::vector &data) { 576 | SaveArrayAsNumpy(filename, fortran_order, n_dims, shape, data.data()); 577 | } 578 | 579 | template 580 | inline void LoadArrayFromNumpy(const std::string &filename, std::vector &shape, bool &fortran_order, 581 | std::vector &data) { 582 | const npy_data n_data = read_npy(filename); 583 | 584 | shape = n_data.shape; 585 | fortran_order = n_data.fortran_order; 586 | 587 | std::copy(n_data.data.begin(), n_data.data.end(), std::back_inserter(data)); 588 | } 589 | 590 | template 591 | inline void LoadArrayFromNumpy(const std::string &filename, std::vector &shape, 592 | std::vector &data) { 593 | bool fortran_order = false; 594 | LoadArrayFromNumpy(filename, shape, fortran_order, data); 595 | } 596 | // NOLINTEND(*-avoid-c-arrays) 597 | 598 | } // namespace npy 599 | 600 | #endif // NPY_HPP_ 601 | --------------------------------------------------------------------------------