diff --git a/.gitignore b/.gitignore index 8dddad7..77ff7ad 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,15 @@ scripts venv -build* +build/ +build-*/ compile_flags.txt ._* tensor_test_files + +# MATLAB/Octave MEX files +*.mex +*.mexa64 +*.mexw32 +*.mexw64 +*.mexmaci64 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b750526..6cb3e62 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,6 +8,7 @@ repos: rev: v16.0.6 hooks: - id: clang-format + exclude: '\.m$' - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 diff --git a/CMakeLists.txt b/CMakeLists.txt index 76cf717..5fd39e7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,6 +28,12 @@ find_package(HDF5 REQUIRED COMPONENTS C) target_link_libraries(binsparse PUBLIC ${HDF5_C_LIBRARIES}) include(FetchContent) + +# Force cJSON to build as static library for MEX compatibility +set(BUILD_SHARED_LIBS_BACKUP ${BUILD_SHARED_LIBS}) +set(BUILD_SHARED_LIBS OFF CACHE INTERNAL "Build cJSON as static library") +set(ENABLE_CJSON_TEST OFF CACHE INTERNAL "Disable cJSON tests") + FetchContent_Declare( cJSON # GIT_REPOSITORY https://github.com/DaveGamble/cJSON.git @@ -36,8 +42,11 @@ FetchContent_Declare( ) FetchContent_MakeAvailable(cJSON) +# Restore original BUILD_SHARED_LIBS setting +set(BUILD_SHARED_LIBS ${BUILD_SHARED_LIBS_BACKUP}) + configure_file(${cJSON_SOURCE_DIR}/cJSON.h ${CMAKE_BINARY_DIR}/include/cJSON/cJSON.h) -target_link_libraries(${PROJECT_NAME} PUBLIC cjson) +target_link_libraries(${PROJECT_NAME} PRIVATE cjson) # Set up include directories properly for both build and install target_include_directories(${PROJECT_NAME} diff --git a/bindings/matlab/README.md b/bindings/matlab/README.md new file mode 100644 index 0000000..ff776f4 --- /dev/null +++ b/bindings/matlab/README.md @@ -0,0 +1,281 @@ + + +# Binsparse MATLAB/Octave Bindings + +This directory contains MATLAB and GNU Octave MEX bindings for the Binsparse C library, providing a simple interface to access Binsparse functionality from both MATLAB and Octave. + +## Quick Start + +### Prerequisites + +**For MATLAB:** +1. **MATLAB** with MEX compiler support +2. **Binsparse C library** headers (included in this repository) + +**For Octave:** +1. **GNU Octave** with mkoctfile (usually included) +2. **C compiler** (gcc recommended) +3. **Binsparse C library** headers (included in this repository) + +### Setup MEX Compiler (if needed) + +**For MATLAB:** +If you haven't configured a MEX compiler yet: + +```matlab +mex -setup +``` + +Choose a compatible C compiler when prompted. + +**For Octave:** +Octave usually comes with mkoctfile pre-configured. Verify it works: + +```bash +mkoctfile --version +``` + +### Build the Bindings + +#### Option 1: MATLAB + +1. Open MATLAB and navigate to this directory: + ```matlab + cd('path/to/binsparse-reference-c/bindings/matlab') + ``` + +2. Build the MEX functions: + ```matlab + build_matlab_bindings() + ``` + +3. Test the installation: + ```matlab + test_binsparse_read() + test_binsparse_write() + ``` + +#### Option 2: Octave (from within Octave) + +1. Open Octave and navigate to this directory: + ```octave + cd('path/to/binsparse-reference-c/bindings/matlab') + ``` + +2. Build the MEX functions: + ```octave + build_octave_bindings() + ``` + +3. Test the installation: + ```octave + test_binsparse_read() + test_binsparse_write() + ``` + +#### Option 3: Octave (from command line) + +1. Navigate to this directory: + ```bash + cd path/to/binsparse-reference-c/bindings/matlab + ``` + +2. Build using the shell script: + ```bash + ./compile_octave.sh + ``` + +3. Test the installation: + ```bash + octave --eval "test_binsparse_read()" + octave --eval "test_binsparse_write()" + ``` + +## Usage Examples + +### Reading Binsparse Files + +**In MATLAB or Octave:** + +```matlab +% Read a Binsparse matrix file +matrix = binsparse_read('path/to/matrix.bsp.h5'); + +% Read from a specific group +matrix = binsparse_read('path/to/matrix.bsp.h5', 'group_name'); + +% Matrix will be a struct with fields: +% - values: array of matrix values +% - indices_0, indices_1: row/column indices +% - pointers_to_1: pointer array (for CSR/CSC formats) +% - nrows, ncols, nnz: matrix dimensions +% - is_iso: boolean for iso-value matrices +% - format: string ('COO', 'CSR', 'CSC', etc.) +% - structure: string ('general', 'symmetric', etc.) +``` + +### Writing Binsparse Files + +```matlab +% Create a matrix struct (example: 3x3 COO matrix) +matrix = struct(); +matrix.values = [1.0; 2.0; 3.0]; +matrix.indices_0 = uint64([0; 1; 2]); % 0-based row indices +matrix.indices_1 = uint64([0; 1; 2]); % 0-based col indices +matrix.pointers_to_1 = uint64([]); % Empty for COO +matrix.nrows = 3; +matrix.ncols = 3; +matrix.nnz = 3; +matrix.is_iso = false; +matrix.format = 'COO'; +matrix.structure = 'general'; + +% Write to file +binsparse_write('output.bsp.h5', matrix); + +% Write with optional parameters +binsparse_write('output.bsp.h5', matrix, 'my_group'); +binsparse_write('output.bsp.h5', matrix, 'my_group', '{"author": "me"}'); +binsparse_write('output.bsp.h5', matrix, 'my_group', '{"author": "me"}', 6); +``` + +### Error Handling + +The MEX functions include proper error handling: + +```matlab +try + matrix = binsparse_read('nonexistent_file.bsp.h5') +catch ME + fprintf('Error: %s\n', ME.message) +end +``` + +## Files Description + +| File | Description | +|------|-------------| +| `binsparse_read.c` | MEX function for reading Binsparse matrix files | +| `binsparse_write.c` | MEX function for writing Binsparse matrix files | +| `build_matlab_bindings.m` | Main build script for MATLAB MEX functions | +| `build_octave_bindings.m` | Main build script for Octave MEX functions | +| `compile_binsparse_read.m` | Simple compilation script for read function (MATLAB) | +| `compile_binsparse_write.m` | Simple compilation script for write function (MATLAB) | +| `compile_binsparse_read_octave.m` | Simple compilation script for read function (Octave) | +| `compile_binsparse_write_octave.m` | Simple compilation script for write function (Octave) | +| `compile_octave.sh` | Shell script for building Octave MEX functions | +| `test_binsparse_read.m` | Test script for read functionality | +| `test_binsparse_write.m` | Test script for write functionality | +| `bsp_matrix_create.m` | Utility function for creating matrix structs | +| `bsp_matrix_info.m` | Utility function for displaying matrix information | +| `README.md` | This documentation file | + +## Technical Details + +### MEX Function Structure + +The Binsparse MEX functions demonstrate: + +1. **Header inclusion**: Proper inclusion of `` +2. **Type conversion**: Complete mapping between MATLAB and Binsparse types +3. **Error handling**: Using Binsparse error types (`bsp_error_t`) +4. **Memory management**: Safe allocation and cleanup +5. **MATLAB interface**: Proper MEX function structure with validation + +### Build Process + +**MATLAB build process:** + +1. Locates Binsparse include directory (`../../include/`) +2. Compiles MEX functions using MATLAB's `mex` command +3. Links against MATLAB MEX libraries +4. Validates compilation with test functions + +**Octave build process:** + +1. Locates Binsparse include directory (`../../include/`) +2. Compiles MEX functions using `mkoctfile --mex` +3. Links against Octave MEX libraries +4. Validates compilation with test functions + +## Extending the Bindings + +To add new Binsparse functionality: + +1. Create a new `.c` file with MEX function structure +2. Include `` and relevant headers +3. Add the filename to `mex_files` list in `build_matlab_bindings.m` +4. Create corresponding test functions + +### Example MEX Function Template + +```c +#include "mex.h" +#include + +void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[]) { + // Input validation + if (nrhs != 1) { + mexErrMsgIdAndTxt("BinSparse:InvalidInput", "Expected 1 input argument"); + } + + // Call Binsparse functions + bsp_error_t error = /* your_binsparse_function() */; + + // Handle errors + if (error != BSP_SUCCESS) { + mexErrMsgIdAndTxt("BinSparse:Error", "%s", bsp_get_error_string(error)); + } + + // Return results to MATLAB + plhs[0] = /* create_matlab_output() */; +} +``` + +## Troubleshooting + +### Common Issues + +1. **MEX compiler not found** + - **MATLAB**: Run `mex -setup` to configure a compiler + - **Octave**: Install mkoctfile (usually comes with Octave) + - Ensure you have a compatible C compiler installed + +2. **Include paths not found** + - Verify you're running from the `bindings/matlab` directory + - Check that `../../include/binsparse/binsparse.h` exists + +3. **Compilation errors** + - **MATLAB**: Try building with verbose output: `build_matlab_bindings('verbose')` + - **Octave**: Try building with verbose output: `build_octave_bindings('verbose')` or `./compile_octave.sh --verbose` + - Check compiler compatibility with your MATLAB/Octave version + +### Platform-Specific Notes + +- **Windows**: + - MATLAB: Microsoft Visual Studio or compatible compiler + - Octave: MinGW-w64 (often included with Octave installer) +- **macOS**: Xcode command line tools required for both MATLAB and Octave +- **Linux**: GCC or compatible compiler should work for both MATLAB and Octave + +## Development Status + +This provides complete MATLAB/Octave bindings for Binsparse. Currently implemented: + +- ✅ Matrix reading (`binsparse_read`) +- ✅ Matrix writing (`binsparse_write`) +- ✅ Complete type support (all Binsparse types including complex numbers) +- ✅ Optional parameters (groups, JSON metadata, compression) +- ✅ Comprehensive error handling +- ✅ Build system and testing framework +- ✅ Round-trip compatibility (read → write → read) +- ⏳ Tensor support (future work) +- ⏳ Advanced sparse matrix operations (future work) + +## License + +This code is licensed under the BSD-3-Clause license, same as the main Binsparse project. diff --git a/bindings/matlab/binsparse_read.c b/bindings/matlab/binsparse_read.c new file mode 100644 index 0000000..cf471fe --- /dev/null +++ b/bindings/matlab/binsparse_read.c @@ -0,0 +1,242 @@ +/* + * SPDX-FileCopyrightText: 2024 Binsparse Developers + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +/** + * binsparse_read.c - Read Binsparse matrices into MATLAB + * + * This MEX function reads Binsparse format matrices and returns them + * as MATLAB structs compatible with bsp_matrix_create. + * + * Usage in MATLAB/Octave: + * matrix = binsparse_read(filename) + * matrix = binsparse_read(filename, group) + */ + +#include "mex.h" +#include +#include + +static inline void* bsp_matlab_malloc(size_t size) { + void* ptr = mxMalloc(size); + mexMakeMemoryPersistent(ptr); + return ptr; +} + +static const bsp_allocator_t bsp_matlab_allocator = { + .malloc = bsp_matlab_malloc, .free = mxFree}; + +static inline mxClassID get_mxClassID(bsp_type_t type) { + switch (type) { + case BSP_UINT8: + return mxUINT8_CLASS; + case BSP_UINT16: + return mxUINT16_CLASS; + case BSP_UINT32: + return mxUINT32_CLASS; + case BSP_UINT64: + return mxUINT64_CLASS; + case BSP_INT8: + return mxINT8_CLASS; + case BSP_INT16: + return mxINT16_CLASS; + case BSP_INT32: + return mxINT32_CLASS; + case BSP_INT64: + return mxINT64_CLASS; + case BSP_FLOAT32: + return mxSINGLE_CLASS; + case BSP_FLOAT64: + return mxDOUBLE_CLASS; + case BSP_BINT8: // Treat BSP_BINT8 as UINT8 + return mxUINT8_CLASS; + case BSP_COMPLEX_FLOAT32: + return mxSINGLE_CLASS; + case BSP_COMPLEX_FLOAT64: + return mxDOUBLE_CLASS; + default: + return mxUNKNOWN_CLASS; + } +} + +static inline mxComplexity get_mxComplexity(bsp_type_t type) { + if (type == BSP_COMPLEX_FLOAT32 || type == BSP_COMPLEX_FLOAT64) { + return mxCOMPLEX; + } else { + return mxREAL; + } +} + +mxArray* bsp_array_to_matlab(bsp_array_t* array) { + if (array->data == NULL || array->size == 0) { + // Return empty array + return mxCreateDoubleMatrix(1, 1, mxREAL); + } + + if (get_mxClassID(array->type) == mxUNKNOWN_CLASS) { + mexWarnMsgIdAndTxt("BinSparse:UnsupportedType", + "Unsupported array type %d, returning empty array", + (int) array->type); + return mxCreateDoubleMatrix(1, 1, mxREAL); + } + + mxArray* mx_array = NULL; + + if ((array->allocator.malloc == bsp_matlab_allocator.malloc && + array->allocator.free == bsp_matlab_allocator.free) && + get_mxComplexity(array->type) == mxREAL) { + // Create mx_array in a zero-copy fashion. + + mx_array = mxCreateNumericMatrix(0, 1, get_mxClassID(array->type), + get_mxComplexity(array->type)); + + mxSetData(mx_array, array->data); + mxSetM(mx_array, array->size); + + array->data = NULL; + array->size = 0; + } else { + mx_array = mxCreateNumericMatrix(array->size, 1, get_mxClassID(array->type), + get_mxComplexity(array->type)); + + if (get_mxComplexity(array->type) == mxREAL) { + memcpy(mxGetData(mx_array), array->data, + array->size * bsp_type_size(array->type)); + } else { + if (array->type == BSP_COMPLEX_FLOAT32) { + float* in_data = + (float*) array->data; // Treat as array of adjacent real/imag pairs + float* real_data = (float*) mxGetData(mx_array); + float* imag_data = (float*) mxGetImagData(mx_array); + for (size_t i = 0; i < array->size; i++) { + real_data[i] = in_data[2 * i]; // Real part + imag_data[i] = in_data[2 * i + 1]; // Imaginary part + } + } else { + double* in_data = + (double*) array->data; // Treat as array of adjacent real/imag pairs + double* real_data = mxGetPr(mx_array); + double* imag_data = mxGetPi(mx_array); + for (size_t i = 0; i < array->size; i++) { + real_data[i] = in_data[2 * i]; // Real part + imag_data[i] = in_data[2 * i + 1]; // Imaginary part + } + } + } + } + + return mx_array; +} + +/** + * Convert bsp_matrix_t to MATLAB struct + */ +mxArray* bsp_matrix_to_matlab_struct(bsp_matrix_t* matrix) { + const char* field_names[] = { + "values", "indices_0", "indices_1", "pointers_to_1", "nrows", + "ncols", "nnz", "is_iso", "format", "structure"}; + + mxArray* mx_struct = mxCreateStructMatrix(1, 1, 10, field_names); + + // Convert arrays + mxSetField(mx_struct, 0, "values", bsp_array_to_matlab(&matrix->values)); + mxSetField(mx_struct, 0, "indices_0", + bsp_array_to_matlab(&matrix->indices_0)); + mxSetField(mx_struct, 0, "indices_1", + bsp_array_to_matlab(&matrix->indices_1)); + mxSetField(mx_struct, 0, "pointers_to_1", + bsp_array_to_matlab(&matrix->pointers_to_1)); + + // Convert scalar fields + mxSetField(mx_struct, 0, "nrows", + mxCreateDoubleScalar((double) matrix->nrows)); + mxSetField(mx_struct, 0, "ncols", + mxCreateDoubleScalar((double) matrix->ncols)); + mxSetField(mx_struct, 0, "nnz", mxCreateDoubleScalar((double) matrix->nnz)); + mxSetField(mx_struct, 0, "is_iso", mxCreateLogicalScalar(matrix->is_iso)); + + // Convert format string + mxSetField(mx_struct, 0, "format", + mxCreateString(bsp_get_matrix_format_string(matrix->format))); + + // Convert structure string + mxSetField(mx_struct, 0, "structure", + mxCreateString(bsp_get_structure_string(matrix->structure))); + + return mx_struct; +} + +/** + * Main MEX function entry point + */ +void mexFunction(int nlhs, mxArray* plhs[], int nrhs, const mxArray* prhs[]) { + char* filename = NULL; + char* group = NULL; + bsp_matrix_t matrix; + bsp_error_t error; + + // Check input arguments + if (nrhs < 1 || nrhs > 2) { + mexErrMsgIdAndTxt("BinSparse:InvalidArgs", + "Usage: matrix = binsparse_read(filename [, group])"); + } + + if (nlhs > 1) { + mexErrMsgIdAndTxt("BinSparse:TooManyOutputs", "Too many output arguments"); + } + + // Get filename + if (!mxIsChar(prhs[0])) { + mexErrMsgIdAndTxt("BinSparse:InvalidFilename", "Filename must be a string"); + } + + filename = mxArrayToString(prhs[0]); + if (!filename) { + mexErrMsgIdAndTxt("BinSparse:MemoryError", + "Failed to convert filename string"); + } + + // Get optional group name + if (nrhs == 2) { + if (!mxIsChar(prhs[1])) { + mxFree(filename); + mexErrMsgIdAndTxt("BinSparse:InvalidGroup", + "Group name must be a string"); + } + + group = mxArrayToString(prhs[1]); + if (!group) { + mxFree(filename); + mexErrMsgIdAndTxt("BinSparse:MemoryError", + "Failed to convert group string"); + } + } + + // Read the matrix using Binsparse + error = + bsp_read_matrix_allocator(&matrix, filename, group, bsp_matlab_allocator); + + if (error != BSP_SUCCESS) { + // Clean up + if (filename) + mxFree(filename); + if (group) + mxFree(group); + + mexErrMsgIdAndTxt("BinSparse:ReadError", "Failed to read matrix: %s", + bsp_get_error_string(error)); + } + + // Convert to MATLAB struct + plhs[0] = bsp_matrix_to_matlab_struct(&matrix); + + // Clean up + bsp_destroy_matrix_t(&matrix); + + if (filename) + mxFree(filename); + if (group) + mxFree(group); +} diff --git a/bindings/matlab/binsparse_write.c b/bindings/matlab/binsparse_write.c new file mode 100644 index 0000000..9e56bd4 --- /dev/null +++ b/bindings/matlab/binsparse_write.c @@ -0,0 +1,357 @@ +/* + * SPDX-FileCopyrightText: 2024 Binsparse Developers + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +/** + * binsparse_write.c - Write MATLAB structs to Binsparse format + * + * This MEX function writes MATLAB structs (compatible with bsp_matrix_create) + * to Binsparse format files. + * + * Usage in MATLAB/Octave: + * binsparse_write(filename, matrix) + * binsparse_write(filename, matrix, group) + * binsparse_write(filename, matrix, group, json_string) + * binsparse_write(filename, matrix, group, json_string, compression_level) + */ + +#include "mex.h" +#include +#include + +/** + * Convert MATLAB array to bsp_array_t + */ +bsp_error_t matlab_to_bsp_array(const mxArray* mx_array, bsp_array_t* array) { + if (mxIsEmpty(mx_array)) { + bsp_construct_default_array_t(array); + return BSP_SUCCESS; + } + + size_t size = mxGetNumberOfElements(mx_array); + mxClassID class_id = mxGetClassID(mx_array); + bool is_complex = mxIsComplex(mx_array); + + // Determine BSP type from MATLAB class + bsp_type_t bsp_type; + size_t element_size; + + if (is_complex) { + if (class_id == mxDOUBLE_CLASS) { + bsp_type = BSP_COMPLEX_FLOAT64; + element_size = sizeof(double _Complex); + } else if (class_id == mxSINGLE_CLASS) { + bsp_type = BSP_COMPLEX_FLOAT32; + element_size = sizeof(float _Complex); + } else { + return BSP_INVALID_TYPE; + } + } else { + switch (class_id) { + case mxDOUBLE_CLASS: + bsp_type = BSP_FLOAT64; + element_size = sizeof(double); + break; + case mxSINGLE_CLASS: + bsp_type = BSP_FLOAT32; + element_size = sizeof(float); + break; + case mxUINT64_CLASS: + bsp_type = BSP_UINT64; + element_size = sizeof(uint64_t); + break; + case mxUINT32_CLASS: + bsp_type = BSP_UINT32; + element_size = sizeof(uint32_t); + break; + case mxUINT16_CLASS: + bsp_type = BSP_UINT16; + element_size = sizeof(uint16_t); + break; + case mxUINT8_CLASS: + bsp_type = BSP_UINT8; + element_size = sizeof(uint8_t); + break; + case mxINT64_CLASS: + bsp_type = BSP_INT64; + element_size = sizeof(int64_t); + break; + case mxINT32_CLASS: + bsp_type = BSP_INT32; + element_size = sizeof(int32_t); + break; + case mxINT16_CLASS: + bsp_type = BSP_INT16; + element_size = sizeof(int16_t); + break; + case mxINT8_CLASS: + bsp_type = BSP_INT8; + element_size = sizeof(int8_t); + break; + default: + return BSP_INVALID_TYPE; + } + } + + // Allocate BSP array + bsp_error_t error = bsp_construct_array_t(array, size, bsp_type); + if (error != BSP_SUCCESS) { + return error; + } + + // Copy data + if (is_complex) { + // Handle complex numbers: interleave real/imaginary parts + if (class_id == mxDOUBLE_CLASS) { + double* real_data = mxGetPr(mx_array); + double* imag_data = mxGetPi(mx_array); + double* out_data = (double*) array->data; + for (size_t i = 0; i < size; i++) { + out_data[2 * i] = real_data[i]; // Real part + out_data[2 * i + 1] = imag_data[i]; // Imaginary part + } + } else { // mxSINGLE_CLASS + float* real_data = (float*) mxGetData(mx_array); + float* imag_data = (float*) mxGetImagData(mx_array); + float* out_data = (float*) array->data; + for (size_t i = 0; i < size; i++) { + out_data[2 * i] = real_data[i]; // Real part + out_data[2 * i + 1] = imag_data[i]; // Imaginary part + } + } + } else { + // Simple copy for real types + memcpy(array->data, mxGetData(mx_array), size * element_size); + } + + return BSP_SUCCESS; +} + +/** + * Convert MATLAB struct to bsp_matrix_t + */ +bsp_error_t matlab_struct_to_bsp_matrix(const mxArray* mx_struct, + bsp_matrix_t* matrix) { + bsp_construct_default_matrix_t(matrix); + + // Extract and convert arrays + mxArray* values_field = mxGetField(mx_struct, 0, "values"); + mxArray* indices_0_field = mxGetField(mx_struct, 0, "indices_0"); + mxArray* indices_1_field = mxGetField(mx_struct, 0, "indices_1"); + mxArray* pointers_to_1_field = mxGetField(mx_struct, 0, "pointers_to_1"); + + if (!values_field || !indices_0_field || !indices_1_field || + !pointers_to_1_field) { + bsp_destroy_matrix_t(matrix); + return BSP_INVALID_STRUCTURE; + } + + bsp_error_t error; + error = matlab_to_bsp_array(values_field, &matrix->values); + if (error != BSP_SUCCESS) { + bsp_destroy_matrix_t(matrix); + return error; + } + + error = matlab_to_bsp_array(indices_0_field, &matrix->indices_0); + if (error != BSP_SUCCESS) { + bsp_destroy_matrix_t(matrix); + return error; + } + + error = matlab_to_bsp_array(indices_1_field, &matrix->indices_1); + if (error != BSP_SUCCESS) { + bsp_destroy_matrix_t(matrix); + return error; + } + + error = matlab_to_bsp_array(pointers_to_1_field, &matrix->pointers_to_1); + if (error != BSP_SUCCESS) { + bsp_destroy_matrix_t(matrix); + return error; + } + + // Extract scalar fields + mxArray* nrows_field = mxGetField(mx_struct, 0, "nrows"); + mxArray* ncols_field = mxGetField(mx_struct, 0, "ncols"); + mxArray* nnz_field = mxGetField(mx_struct, 0, "nnz"); + mxArray* is_iso_field = mxGetField(mx_struct, 0, "is_iso"); + + if (!nrows_field || !ncols_field || !nnz_field || !is_iso_field) { + bsp_destroy_matrix_t(matrix); + return BSP_INVALID_STRUCTURE; + } + + matrix->nrows = (size_t) mxGetScalar(nrows_field); + matrix->ncols = (size_t) mxGetScalar(ncols_field); + matrix->nnz = (size_t) mxGetScalar(nnz_field); + matrix->is_iso = mxIsLogicalScalarTrue(is_iso_field); + + // Extract format string + mxArray* format_field = mxGetField(mx_struct, 0, "format"); + if (!format_field || !mxIsChar(format_field)) { + bsp_destroy_matrix_t(matrix); + return BSP_INVALID_STRUCTURE; + } + + char* format_str = mxArrayToString(format_field); + if (!format_str) { + bsp_destroy_matrix_t(matrix); + return BSP_INVALID_STRUCTURE; + } + + matrix->format = bsp_get_matrix_format(format_str); + mxFree(format_str); + + if (matrix->format == BSP_INVALID_FORMAT) { + bsp_destroy_matrix_t(matrix); + return BSP_INVALID_FORMAT; + } + + // Extract structure string + mxArray* structure_field = mxGetField(mx_struct, 0, "structure"); + if (!structure_field || !mxIsChar(structure_field)) { + bsp_destroy_matrix_t(matrix); + return BSP_INVALID_STRUCTURE; + } + + char* structure_str = mxArrayToString(structure_field); + if (!structure_str) { + bsp_destroy_matrix_t(matrix); + return BSP_INVALID_STRUCTURE; + } + + matrix->structure = bsp_get_structure(structure_str); + mxFree(structure_str); + + if (matrix->structure == BSP_INVALID_STRUCTURE) { + matrix->structure = BSP_GENERAL; // Default fallback + } + + return BSP_SUCCESS; +} + +/** + * Main MEX function entry point + */ +void mexFunction(int nlhs, mxArray* plhs[], int nrhs, const mxArray* prhs[]) { + char* filename = NULL; + char* group = NULL; + char* json_string = NULL; + int compression_level = 1; // Default compression + bsp_matrix_t matrix; + bsp_error_t error; + + // Check input arguments + if (nrhs < 2 || nrhs > 5) { + mexErrMsgIdAndTxt("BinSparse:InvalidArgs", + "Usage: binsparse_write(filename, matrix [, group [, " + "json_string [, compression_level]]])"); + } + + if (nlhs > 0) { + mexErrMsgIdAndTxt("BinSparse:TooManyOutputs", + "No output arguments expected"); + } + + // Get filename + if (!mxIsChar(prhs[0])) { + mexErrMsgIdAndTxt("BinSparse:InvalidFilename", "Filename must be a string"); + } + + filename = mxArrayToString(prhs[0]); + if (!filename) { + mexErrMsgIdAndTxt("BinSparse:MemoryError", + "Failed to convert filename string"); + } + + // Get matrix struct + if (!mxIsStruct(prhs[1])) { + mxFree(filename); + mexErrMsgIdAndTxt("BinSparse:InvalidMatrix", "Matrix must be a struct"); + } + + // Convert MATLAB struct to bsp_matrix_t + error = matlab_struct_to_bsp_matrix(prhs[1], &matrix); + if (error != BSP_SUCCESS) { + mxFree(filename); + mexErrMsgIdAndTxt("BinSparse:ConversionError", + "Failed to convert MATLAB struct to matrix: %s", + bsp_get_error_string(error)); + } + + // Get optional group name + if (nrhs >= 3 && !mxIsEmpty(prhs[2])) { + if (!mxIsChar(prhs[2])) { + bsp_destroy_matrix_t(&matrix); + mxFree(filename); + mexErrMsgIdAndTxt("BinSparse:InvalidGroup", + "Group name must be a string"); + } + + group = mxArrayToString(prhs[2]); + if (!group) { + bsp_destroy_matrix_t(&matrix); + mxFree(filename); + mexErrMsgIdAndTxt("BinSparse:MemoryError", + "Failed to convert group string"); + } + } + + // Get optional JSON string + if (nrhs >= 4 && !mxIsEmpty(prhs[3])) { + if (!mxIsChar(prhs[3])) { + bsp_destroy_matrix_t(&matrix); + if (group) + mxFree(group); + mxFree(filename); + mexErrMsgIdAndTxt("BinSparse:InvalidJSON", "JSON must be a string"); + } + + json_string = mxArrayToString(prhs[3]); + if (!json_string) { + bsp_destroy_matrix_t(&matrix); + if (group) + mxFree(group); + mxFree(filename); + mexErrMsgIdAndTxt("BinSparse:MemoryError", + "Failed to convert JSON string"); + } + } + + // Get optional compression level + if (nrhs >= 5 && !mxIsEmpty(prhs[4])) { + if (!mxIsNumeric(prhs[4]) || mxIsComplex(prhs[4]) || + mxGetNumberOfElements(prhs[4]) != 1) { + bsp_destroy_matrix_t(&matrix); + if (json_string) + mxFree(json_string); + if (group) + mxFree(group); + mxFree(filename); + mexErrMsgIdAndTxt("BinSparse:InvalidCompression", + "Compression level must be a scalar integer"); + } + + compression_level = (int) mxGetScalar(prhs[4]); + } + + // Write the matrix using Binsparse + error = + bsp_write_matrix(filename, matrix, group, json_string, compression_level); + + // Clean up + bsp_destroy_matrix_t(&matrix); + if (json_string) + mxFree(json_string); + if (group) + mxFree(group); + mxFree(filename); + + if (error != BSP_SUCCESS) { + mexErrMsgIdAndTxt("BinSparse:WriteError", "Failed to write matrix: %s", + bsp_get_error_string(error)); + } +} diff --git a/bindings/matlab/bsp_matrix_create.m b/bindings/matlab/bsp_matrix_create.m new file mode 100644 index 0000000..99a1d56 --- /dev/null +++ b/bindings/matlab/bsp_matrix_create.m @@ -0,0 +1,71 @@ +% SPDX-FileCopyrightText: 2024 Binsparse Developers +% +% SPDX-License-Identifier: BSD-3-Clause + +function matrix = bsp_matrix_create(varargin) +% BSP_MATRIX_CREATE - Create a Binsparse matrix struct +% +% Creates a MATLAB struct analogous to the C bsp_matrix_t structure. +% The struct contains four native MATLAB arrays and metadata fields. +% +% Usage: +% matrix = bsp_matrix_create() % Empty matrix +% matrix = bsp_matrix_create(values, indices_0, indices_1, pointers_to_1, ... +% nrows, ncols, nnz, is_iso, format, structure) +% +% Fields: +% values - MATLAB array of matrix values +% indices_0 - MATLAB array of first dimension indices +% indices_1 - MATLAB array of second dimension indices +% pointers_to_1 - MATLAB array of pointers for compressed formats +% nrows - Number of rows (integer) +% ncols - Number of columns (integer) +% nnz - Number of non-zeros (integer) +% is_iso - Logical indicating if matrix has single value (logical) +% format - Matrix format string ('CSR', 'CSC', 'COO', etc.) +% structure - Matrix structure string ('general', 'symmetric', etc.) +% +% Example: +% % Create empty matrix +% matrix = bsp_matrix_create(); +% +% % Create COO matrix +% values = [1.0, 2.0, 3.0]; +% rows = [1, 2, 3]; +% cols = [1, 2, 3]; +% matrix = bsp_matrix_create(values, rows, cols, [], 3, 3, 3, false, 'COO', 'general'); + +if nargin == 0 + % Create empty/default matrix + matrix = struct(... + 'values', double([]), ... + 'indices_0', uint64([]), ... + 'indices_1', uint64([]), ... + 'pointers_to_1', uint64([]), ... + 'nrows', uint64(0), ... + 'ncols', uint64(0), ... + 'nnz', uint64(0), ... + 'is_iso', false, ... + 'format', '', ... + 'structure', 'general'); + +elseif nargin == 10 + % Create matrix with all fields specified + matrix = struct(... + 'values', varargin{1}, ... + 'indices_0', varargin{2}, ... + 'indices_1', varargin{3}, ... + 'pointers_to_1', varargin{4}, ... + 'nrows', uint64(varargin{5}), ... + 'ncols', uint64(varargin{6}), ... + 'nnz', uint64(varargin{7}), ... + 'is_iso', logical(varargin{8}), ... + 'format', char(varargin{9}), ... + 'structure', char(varargin{10})); + +else + error('bsp_matrix_create:InvalidArgs', ... + 'Expected 0 or 10 arguments, got %d', nargin); +end + +end diff --git a/bindings/matlab/bsp_matrix_info.m b/bindings/matlab/bsp_matrix_info.m new file mode 100644 index 0000000..6cce27e --- /dev/null +++ b/bindings/matlab/bsp_matrix_info.m @@ -0,0 +1,51 @@ +% SPDX-FileCopyrightText: 2024 Binsparse Developers +% +% SPDX-License-Identifier: BSD-3-Clause + +function bsp_matrix_info(matrix) +% BSP_MATRIX_INFO - Display information about a Binsparse matrix struct +% +% Usage: +% bsp_matrix_info(matrix) +% +% Displays: +% - Matrix dimensions and number of non-zeros +% - Format and structure information +% - Array sizes and types for each field + +if ~isstruct(matrix) + error('bsp_matrix_info:InvalidInput', 'Input must be a struct'); +end + +% Display basic matrix information +fprintf('%lu x %lu matrix with %lu nnz\n', ... + matrix.nrows, matrix.ncols, matrix.nnz); +fprintf('%s format with %s structure\n', ... + matrix.format, matrix.structure); + +if matrix.is_iso + fprintf('ISO matrix (single value)\n'); +end + +% Display array information +if ~isempty(matrix.values) + fprintf('%lu values of type %s\n', ... + length(matrix.values), class(matrix.values)); +end + +if ~isempty(matrix.indices_0) + fprintf('%lu indices_0 of type %s\n', ... + length(matrix.indices_0), class(matrix.indices_0)); +end + +if ~isempty(matrix.indices_1) + fprintf('%lu indices_1 of type %s\n', ... + length(matrix.indices_1), class(matrix.indices_1)); +end + +if ~isempty(matrix.pointers_to_1) + fprintf('%lu pointers_to_1 of type %s\n', ... + length(matrix.pointers_to_1), class(matrix.pointers_to_1)); +end + +end diff --git a/bindings/matlab/build_matlab_bindings.m b/bindings/matlab/build_matlab_bindings.m new file mode 100644 index 0000000..8816d0f --- /dev/null +++ b/bindings/matlab/build_matlab_bindings.m @@ -0,0 +1,149 @@ +% SPDX-FileCopyrightText: 2024 Binsparse Developers +% +% SPDX-License-Identifier: BSD-3-Clause + +function build_matlab_bindings(varargin) +% BUILD_MATLAB_BINDINGS - Build Binsparse MATLAB MEX functions +% +% This script provides a simple interface to build MATLAB bindings +% for the Binsparse library. It automatically detects include paths +% and sets up the compilation environment. +% +% Usage: +% build_matlab_bindings() % Build all available MEX functions +% build_matlab_bindings('verbose') % Build with verbose output +% build_matlab_bindings('clean') % Clean compiled MEX files +% +% Prerequisites: +% - MATLAB with working MEX compiler (run 'mex -setup' if needed) +% - Binsparse C library headers (in ../../include/) +% +% Note: This script currently builds a simple demonstration MEX function. +% Additional Binsparse functionality can be added by creating more +% MEX wrapper functions. + +% Parse input arguments +verbose = any(strcmpi(varargin, 'verbose')); +clean_only = any(strcmpi(varargin, 'clean')); + +fprintf('=== Binsparse MATLAB Bindings Build Script ===\n\n'); + +if clean_only + fprintf('Cleaning compiled MEX files...\n'); + clean_mex_files(); + return; +end + +% Check MEX compiler +if ~check_mex_compiler() + error('MEX compiler not properly configured. Run "mex -setup" first.'); +end + +% Find and validate paths +paths = get_build_paths(); +if verbose + fprintf('Build paths:\n'); + fprintf(' MATLAB dir: %s\n', paths.matlab_dir); + fprintf(' Include dir: %s\n', paths.include_dir); + fprintf(' Root dir: %s\n', paths.binsparse_root); + fprintf('\n'); +end + +% Compile MEX functions +compile_mex_functions(paths, verbose); + +fprintf('\n=== Build Complete ===\n'); +fprintf('Run the test functions to verify the installation:\n'); +fprintf(' test_binsparse_read()\n'); +fprintf(' test_binsparse_write()\n\n'); + +end + +function success = check_mex_compiler() + % Check if MEX compiler is configured + try + % Try to get MEX configuration + cc = mex.getCompilerConfigurations('C'); + success = ~isempty(cc); + if success + fprintf('MEX compiler found: %s\n', cc(1).Name); + end + catch + success = false; + end +end + +function paths = get_build_paths() + % Get and validate build paths + paths.matlab_dir = pwd; + paths.binsparse_root = fullfile(paths.matlab_dir, '..', '..'); + paths.include_dir = fullfile(paths.binsparse_root, 'include'); + + if ~exist(paths.include_dir, 'dir') + error('Binsparse include directory not found: %s\nEnsure you are running this script from the bindings/matlab directory.', paths.include_dir); + end + + % Check for main header file + main_header = fullfile(paths.include_dir, 'binsparse', 'binsparse.h'); + if ~exist(main_header, 'file') + error('Main Binsparse header not found: %s', main_header); + end +end + +function compile_mex_functions(paths, verbose) + % Compile all MEX functions + + % List of MEX functions to compile + mex_files = {'binsparse_read.c', 'binsparse_write.c'}; + + fprintf('Compiling MEX functions...\n'); + + for i = 1:length(mex_files) + mex_file = mex_files{i}; + if ~exist(mex_file, 'file') + warning('MEX source file not found: %s', mex_file); + continue; + end + + fprintf(' Compiling %s... ', mex_file); + + % Prepare MEX command with library linking + lib_dir = fullfile(paths.binsparse_root, 'build'); + lib_path = fullfile(lib_dir, 'libbinsparse.a'); + cjson_lib = fullfile(lib_dir, '_deps', 'cjson-build', 'libcjson.so'); + + mex_args = {'-I', paths.include_dir, mex_file, lib_path, cjson_lib, '-lhdf5_serial'}; + if verbose + mex_args = [mex_args, {'-v'}]; + end + + try + mex(mex_args{:}); + fprintf('SUCCESS\n'); + catch ME + fprintf('FAILED\n'); + fprintf(' Error: %s\n', ME.message); + end + end +end + +function clean_mex_files() + % Clean compiled MEX files + + % Get MEX file extension for current platform + mex_ext = mexext(); + + % Find and delete MEX files + mex_files = dir(['*.' mex_ext]); + + if isempty(mex_files) + fprintf('No MEX files found to clean.\n'); + return; + end + + fprintf('Removing %d MEX file(s):\n', length(mex_files)); + for i = 1:length(mex_files) + fprintf(' %s\n', mex_files(i).name); + delete(mex_files(i).name); + end +end diff --git a/bindings/matlab/build_octave_bindings.m b/bindings/matlab/build_octave_bindings.m new file mode 100644 index 0000000..87ba30f --- /dev/null +++ b/bindings/matlab/build_octave_bindings.m @@ -0,0 +1,185 @@ +% SPDX-FileCopyrightText: 2024 Binsparse Developers +% +% SPDX-License-Identifier: BSD-3-Clause + +function build_octave_bindings(varargin) +% BUILD_OCTAVE_BINDINGS - Build Binsparse Octave MEX functions +% +% This script provides a simple interface to build Octave bindings +% for the Binsparse library using mkoctfile. It automatically detects +% include paths and sets up the compilation environment. +% +% Usage: +% build_octave_bindings() % Build all available MEX functions +% build_octave_bindings('verbose') % Build with verbose output +% build_octave_bindings('clean') % Clean compiled MEX files +% +% Prerequisites: +% - GNU Octave with mkoctfile (usually included with Octave) +% - Binsparse C library headers (in ../../include/) +% - C compiler (gcc recommended) +% +% Note: This script builds Octave-compatible MEX functions using mkoctfile +% instead of MATLAB's mex command. + +% Parse input arguments +verbose = any(strcmpi(varargin, 'verbose')); +clean_only = any(strcmpi(varargin, 'clean')); + +fprintf('=== Binsparse Octave Bindings Build Script ===\n\n'); + +if clean_only + fprintf('Cleaning compiled MEX files...\n'); + clean_mex_files(); + return; +end + +% Check if we're running in Octave +if ~is_octave() + warning('This script is designed for Octave. For MATLAB, use build_matlab_bindings.m'); +end + +% Check mkoctfile availability +if ~check_mkoctfile() + error('mkoctfile not found. Please ensure Octave is properly installed.'); +end + +% Find and validate paths +paths = get_build_paths(); +if verbose + fprintf('Build paths:\n'); + fprintf(' Current dir: %s\n', paths.current_dir); + fprintf(' Include dir: %s\n', paths.include_dir); + fprintf(' Root dir: %s\n', paths.binsparse_root); + fprintf('\n'); +end + +% Compile MEX functions +compile_octave_functions(paths, verbose); + +fprintf('\n=== Build Complete ===\n'); +fprintf('Run the test functions to verify the installation:\n'); +fprintf(' test_binsparse_read()\n'); +fprintf(' test_binsparse_write()\n\n'); + +end + +function result = is_octave() + % Check if running in Octave + result = exist('OCTAVE_VERSION', 'builtin') ~= 0; +end + +function success = check_mkoctfile() + % Check if mkoctfile is available + try + [status, ~] = system('mkoctfile --version'); + success = (status == 0); + if success && nargout == 0 + fprintf('mkoctfile found and working\n'); + end + catch + success = false; + end +end + +function paths = get_build_paths() + % Get and validate build paths + paths.current_dir = pwd; + paths.binsparse_root = fullfile(paths.current_dir, '..', '..'); + paths.include_dir = fullfile(paths.binsparse_root, 'include'); + + if ~exist(paths.include_dir, 'dir') + error('Binsparse include directory not found: %s\nEnsure you are running this script from the bindings/matlab directory.', paths.include_dir); + end + + % Check for main header file + main_header = fullfile(paths.include_dir, 'binsparse', 'binsparse.h'); + if ~exist(main_header, 'file') + error('Main Binsparse header not found: %s', main_header); + end +end + +function compile_octave_functions(paths, verbose) + % Compile all MEX functions using mkoctfile + + % List of MEX functions to compile + mex_files = {'binsparse_read.c', 'binsparse_write.c'}; + + fprintf('Compiling MEX functions with mkoctfile...\n'); + + for i = 1:length(mex_files) + mex_file = mex_files{i}; + if ~exist(mex_file, 'file') + warning('MEX source file not found: %s', mex_file); + continue; + end + + fprintf(' Compiling %s... ', mex_file); + + % Prepare mkoctfile command with library linking + include_flag = sprintf('-I%s', paths.include_dir); + lib_dir = fullfile(paths.binsparse_root, 'build'); + lib_path = fullfile(lib_dir, 'libbinsparse.a'); + cjson_lib_dir = fullfile(lib_dir, '_deps', 'cjson-build'); + + if verbose + cmd = sprintf('mkoctfile --mex --verbose -fPIC %s %s -Wl,--whole-archive %s -Wl,--no-whole-archive -L%s -lcjson -lhdf5_serial', ... + include_flag, mex_file, lib_path, cjson_lib_dir); + else + cmd = sprintf('mkoctfile --mex -fPIC %s %s -Wl,--whole-archive %s -Wl,--no-whole-archive -L%s -lcjson -lhdf5_serial', ... + include_flag, mex_file, lib_path, cjson_lib_dir); + end + + if verbose + fprintf('\n Command: %s\n', cmd); + end + + % Execute mkoctfile + [status, output] = system(cmd); + + if status == 0 + fprintf('SUCCESS\n'); + if verbose && ~isempty(output) + fprintf(' Output: %s\n', output); + end + else + fprintf('FAILED\n'); + fprintf(' Error output:\n%s\n', output); + end + end +end + +function clean_mex_files() + % Clean compiled MEX files (Octave uses different extensions) + + % Octave MEX extensions vary by platform + if ispc + extensions = {'mexw32', 'mexw64'}; + elseif ismac + extensions = {'mexmaci64'}; + else + extensions = {'mexa64', 'mex'}; + end + + found_files = {}; + + % Find files with any of the MEX extensions + for i = 1:length(extensions) + ext = extensions{i}; + files = dir(['*.' ext]); + for j = 1:length(files) + found_files{end+1} = files(j).name; + end + end + + if isempty(found_files) + fprintf('No MEX files found to clean.\n'); + return; + end + + fprintf('Removing %d MEX file(s):\n', length(found_files)); + for i = 1:length(found_files) + fprintf(' %s\n', found_files{i}); + delete(found_files{i}); + end +end diff --git a/bindings/matlab/compile_binsparse_read.m b/bindings/matlab/compile_binsparse_read.m new file mode 100644 index 0000000..c95f93d --- /dev/null +++ b/bindings/matlab/compile_binsparse_read.m @@ -0,0 +1,53 @@ +% SPDX-FileCopyrightText: 2024 Binsparse Developers +% +% SPDX-License-Identifier: BSD-3-Clause + +function compile_binsparse_read() +% COMPILE_BINSPARSE_READ - Quick compilation script for binsparse_read +% +% This script compiles just the binsparse_read MEX function with proper +% library linking. + +fprintf('Compiling binsparse_read MEX function...\n'); + +% Get paths +matlab_dir = pwd; +binsparse_root = fullfile(matlab_dir, '..', '..'); +include_dir = fullfile(binsparse_root, 'include'); +lib_dir = fullfile(binsparse_root, 'build'); + +% Check for required files +lib_path = fullfile(lib_dir, 'libbinsparse.a'); +cjson_lib = fullfile(lib_dir, '_deps', 'cjson-build', 'libcjson.so'); + +if ~exist(lib_path, 'file') + error('libbinsparse.a not found at: %s\nBuild the library first with cmake.', lib_path); +end + +if ~exist(cjson_lib, 'file') + error('libcjson.so not found at: %s\nBuild the library first with cmake.', cjson_lib); +end + +fprintf('Using libraries:\n'); +fprintf(' libbinsparse.a: %s\n', lib_path); +fprintf(' libcjson.so: %s\n', cjson_lib); + +try + % Compile with linking + mex('-I', include_dir, 'binsparse_read.c', lib_path, cjson_lib, '-lhdf5_serial', '-v'); + fprintf('Successfully compiled binsparse_read!\n'); + + % Test if it loads + fprintf('Testing MEX function...\n'); + if exist('binsparse_read', 'file') + fprintf('binsparse_read MEX function is ready to use.\n'); + else + warning('MEX function compiled but not found in path.'); + end + +catch ME + fprintf('Compilation failed: %s\n', ME.message); + rethrow(ME); +end + +end diff --git a/bindings/matlab/compile_binsparse_read_octave.m b/bindings/matlab/compile_binsparse_read_octave.m new file mode 100644 index 0000000..110f0db --- /dev/null +++ b/bindings/matlab/compile_binsparse_read_octave.m @@ -0,0 +1,76 @@ +% SPDX-FileCopyrightText: 2024 Binsparse Developers +% +% SPDX-License-Identifier: BSD-3-Clause + +function compile_binsparse_read_octave() +% COMPILE_BINSPARSE_READ_OCTAVE - Quick Octave compilation for binsparse_read +% +% This script compiles just the binsparse_read MEX function using mkoctfile +% with proper library linking for Octave. + +fprintf('Compiling binsparse_read MEX function for Octave...\n'); + +% Check if we're in Octave +if ~(exist('OCTAVE_VERSION', 'builtin') ~= 0) + warning('This script is designed for Octave. For MATLAB, use compile_binsparse_read.m'); +end + +% Get paths +matlab_dir = pwd; +binsparse_root = fullfile(matlab_dir, '..', '..'); +include_dir = fullfile(binsparse_root, 'include'); +lib_dir = fullfile(binsparse_root, 'build'); + +% Check for required files +lib_path = fullfile(lib_dir, 'libbinsparse.a'); +cjson_lib_path = fullfile(lib_dir, '_deps', 'cjson-build', 'libcjson.a'); + +if ~exist(lib_path, 'file') + error('libbinsparse.a not found at: %s\nBuild the library first with cmake.', lib_path); +end + +if ~exist(cjson_lib_path, 'file') + error('libcjson.a not found at: %s\nRebuild with static cJSON support.', cjson_lib_path); +end + +fprintf('Using libraries:\n'); +fprintf(' libbinsparse.a: %s\n', lib_path); +fprintf(' libcjson.a: %s\n', cjson_lib_path); + +% Build command for mkoctfile with -fPIC and proper static linking +cmd = sprintf('mkoctfile --mex -fPIC -I%s binsparse_read.c -Wl,--whole-archive %s %s -Wl,--no-whole-archive -lhdf5_serial', ... + include_dir, lib_path, cjson_lib_path); + +fprintf('Running: %s\n', cmd); + +try + % Execute mkoctfile + [status, output] = system(cmd); + + if status == 0 + fprintf('Successfully compiled binsparse_read!\n'); + if ~isempty(output) + fprintf('Output: %s\n', output); + end + + % Test if it loads + fprintf('Testing MEX function...\n'); + if exist('binsparse_read', 'file') + fprintf('binsparse_read MEX function is ready to use.\n'); + else + warning('MEX function compiled but not found in path.'); + end + else + fprintf('Compilation failed with status %d\n', status); + if ~isempty(output) + fprintf('Error output: %s\n', output); + end + error('mkoctfile compilation failed'); + end + +catch ME + fprintf('Compilation failed: %s\n', ME.message); + rethrow(ME); +end + +end diff --git a/bindings/matlab/compile_binsparse_write.m b/bindings/matlab/compile_binsparse_write.m new file mode 100644 index 0000000..d62bb08 --- /dev/null +++ b/bindings/matlab/compile_binsparse_write.m @@ -0,0 +1,53 @@ +% SPDX-FileCopyrightText: 2024 Binsparse Developers +% +% SPDX-License-Identifier: BSD-3-Clause + +function compile_binsparse_write() +% COMPILE_BINSPARSE_WRITE - Quick compilation script for binsparse_write +% +% This script compiles just the binsparse_write MEX function with proper +% library linking. + +fprintf('Compiling binsparse_write MEX function...\n'); + +% Get paths +matlab_dir = pwd; +binsparse_root = fullfile(matlab_dir, '..', '..'); +include_dir = fullfile(binsparse_root, 'include'); +lib_dir = fullfile(binsparse_root, 'build'); + +% Check for required files +lib_path = fullfile(lib_dir, 'libbinsparse.a'); +cjson_lib = fullfile(lib_dir, '_deps', 'cjson-build', 'libcjson.so'); + +if ~exist(lib_path, 'file') + error('libbinsparse.a not found at: %s\nBuild the library first with cmake.', lib_path); +end + +if ~exist(cjson_lib, 'file') + error('libcjson.so not found at: %s\nBuild the library first with cmake.', cjson_lib); +end + +fprintf('Using libraries:\n'); +fprintf(' libbinsparse.a: %s\n', lib_path); +fprintf(' libcjson.so: %s\n', cjson_lib); + +try + % Compile with linking + mex('-I', include_dir, 'binsparse_write.c', lib_path, cjson_lib, '-lhdf5_serial', '-v'); + fprintf('Successfully compiled binsparse_write!\n'); + + % Test if it loads + fprintf('Testing MEX function...\n'); + if exist('binsparse_write', 'file') + fprintf('binsparse_write MEX function is ready to use.\n'); + else + warning('MEX function compiled but not found in path.'); + end + +catch ME + fprintf('Compilation failed: %s\n', ME.message); + rethrow(ME); +end + +end diff --git a/bindings/matlab/compile_binsparse_write_octave.m b/bindings/matlab/compile_binsparse_write_octave.m new file mode 100644 index 0000000..d1fa4ea --- /dev/null +++ b/bindings/matlab/compile_binsparse_write_octave.m @@ -0,0 +1,76 @@ +% SPDX-FileCopyrightText: 2024 Binsparse Developers +% +% SPDX-License-Identifier: BSD-3-Clause + +function compile_binsparse_write_octave() +% COMPILE_BINSPARSE_WRITE_OCTAVE - Quick Octave compilation for binsparse_write +% +% This script compiles just the binsparse_write MEX function using mkoctfile +% with proper library linking for Octave. + +fprintf('Compiling binsparse_write MEX function for Octave...\n'); + +% Check if we're in Octave +if ~(exist('OCTAVE_VERSION', 'builtin') ~= 0) + warning('This script is designed for Octave. For MATLAB, use compile_binsparse_write.m'); +end + +% Get paths +matlab_dir = pwd; +binsparse_root = fullfile(matlab_dir, '..', '..'); +include_dir = fullfile(binsparse_root, 'include'); +lib_dir = fullfile(binsparse_root, 'build'); + +% Check for required files +lib_path = fullfile(lib_dir, 'libbinsparse.a'); +cjson_lib_path = fullfile(lib_dir, '_deps', 'cjson-build', 'libcjson.a'); + +if ~exist(lib_path, 'file') + error('libbinsparse.a not found at: %s\nBuild the library first with cmake.', lib_path); +end + +if ~exist(cjson_lib_path, 'file') + error('libcjson.a not found at: %s\nRebuild with static cJSON support.', cjson_lib_path); +end + +fprintf('Using libraries:\n'); +fprintf(' libbinsparse.a: %s\n', lib_path); +fprintf(' libcjson.a: %s\n', cjson_lib_path); + +% Build command for mkoctfile with -fPIC and proper static linking +cmd = sprintf('mkoctfile --mex -fPIC -I%s binsparse_write.c -Wl,--whole-archive %s %s -Wl,--no-whole-archive -lhdf5_serial', ... + include_dir, lib_path, cjson_lib_path); + +fprintf('Running: %s\n', cmd); + +try + % Execute mkoctfile + [status, output] = system(cmd); + + if status == 0 + fprintf('Successfully compiled binsparse_write!\n'); + if ~isempty(output) + fprintf('Output: %s\n', output); + end + + % Test if it loads + fprintf('Testing MEX function...\n'); + if exist('binsparse_write', 'file') + fprintf('binsparse_write MEX function is ready to use.\n'); + else + warning('MEX function compiled but not found in path.'); + end + else + fprintf('Compilation failed with status %d\n', status); + if ~isempty(output) + fprintf('Error output: %s\n', output); + end + error('mkoctfile compilation failed'); + end + +catch ME + fprintf('Compilation failed: %s\n', ME.message); + rethrow(ME); +end + +end diff --git a/bindings/matlab/compile_octave.sh b/bindings/matlab/compile_octave.sh new file mode 100755 index 0000000..641edf2 --- /dev/null +++ b/bindings/matlab/compile_octave.sh @@ -0,0 +1,181 @@ +#!/bin/bash + +# SPDX-FileCopyrightText: 2024 Binsparse Developers +# +# SPDX-License-Identifier: BSD-3-Clause + +# compile_octave.sh - Build Binsparse Octave MEX functions from command line +# +# This script compiles the Binsparse MEX functions for Octave using mkoctfile +# from the command line, without needing to start Octave first. +# +# Usage: +# ./compile_octave.sh +# ./compile_octave.sh --verbose +# ./compile_octave.sh --clean + +set -e # Exit on any error + +# Color output for better readability +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Function to print colored output +print_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +print_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Parse command line arguments +VERBOSE=false +CLEAN=false + +for arg in "$@"; do + case $arg in + --verbose|-v) + VERBOSE=true + shift + ;; + --clean|-c) + CLEAN=true + shift + ;; + --help|-h) + echo "Usage: $0 [--verbose] [--clean] [--help]" + echo " --verbose, -v: Enable verbose output" + echo " --clean, -c: Clean compiled MEX files" + echo " --help, -h: Show this help message" + exit 0 + ;; + *) + print_error "Unknown option: $arg" + echo "Use --help for usage information" + exit 1 + ;; + esac +done + +print_info "Binsparse Octave MEX Compilation Script" +echo "========================================" + +# Check if mkoctfile is available +if ! command -v mkoctfile &> /dev/null; then + print_error "mkoctfile not found. Please install GNU Octave." + exit 1 +fi + +mkoctfile_version=$(mkoctfile --version | head -n1) +print_info "Found mkoctfile: $mkoctfile_version" + +# Get script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +print_info "Working directory: $SCRIPT_DIR" + +# Find Binsparse include directory +BINSPARSE_ROOT="$(realpath "$SCRIPT_DIR/../..")" +INCLUDE_DIR="$BINSPARSE_ROOT/include" + +if [ ! -d "$INCLUDE_DIR" ]; then + print_error "Binsparse include directory not found: $INCLUDE_DIR" + print_error "Make sure you're running this script from the bindings/matlab directory" + exit 1 +fi + +if [ ! -f "$INCLUDE_DIR/binsparse/binsparse.h" ]; then + print_error "Main Binsparse header not found: $INCLUDE_DIR/binsparse/binsparse.h" + exit 1 +fi + +print_info "Using Binsparse include directory: $INCLUDE_DIR" + +# Change to script directory +cd "$SCRIPT_DIR" + +# Clean function +clean_mex_files() { + print_info "Cleaning compiled MEX files..." + + # MEX file extensions for different platforms + extensions=("mex" "mexa64" "mexw32" "mexw64" "mexmaci64") + + found_files=0 + for ext in "${extensions[@]}"; do + if ls *.${ext} 1> /dev/null 2>&1; then + for file in *.${ext}; do + print_info "Removing $file" + rm "$file" + ((found_files++)) + done + fi + done + + if [ $found_files -eq 0 ]; then + print_info "No MEX files found to clean" + else + print_success "Cleaned $found_files MEX file(s)" + fi +} + +# Handle clean option +if [ "$CLEAN" = true ]; then + clean_mex_files + exit 0 +fi + +# List of MEX files to compile +MEX_FILES=("binsparse_read.c" "binsparse_write.c") + +print_info "Compiling MEX functions..." + +# Compile each MEX file +for mex_file in "${MEX_FILES[@]}"; do + if [ ! -f "$mex_file" ]; then + print_warning "MEX source file not found: $mex_file" + continue + fi + + print_info "Compiling $mex_file..." + + # Build mkoctfile command with library linking + LIB_DIR="$BINSPARSE_ROOT/build" + LIB_PATH="$LIB_DIR/libbinsparse.a" + CJSON_LIB_DIR="$LIB_DIR/_deps/cjson-build" + + CMD="mkoctfile --mex -fPIC -I\"$INCLUDE_DIR\" $mex_file -Wl,--whole-archive \"$LIB_PATH\" -Wl,--no-whole-archive -L\"$CJSON_LIB_DIR\" -lcjson -lhdf5_serial" + + if [ "$VERBOSE" = true ]; then + CMD="$CMD --verbose" + print_info "Command: $CMD" + fi + + # Execute compilation + if eval $CMD; then + print_success "Successfully compiled $mex_file" + else + print_error "Failed to compile $mex_file" + exit 1 + fi +done + +print_success "All MEX functions compiled successfully!" +echo "" +print_info "To test the functions, start Octave and run:" +echo " test_binsparse_read()" +echo " test_binsparse_write()" +echo "" +print_info "Or test from command line:" +echo " octave --eval \"test_binsparse_read()\"" diff --git a/bindings/matlab/test_binsparse_read.m b/bindings/matlab/test_binsparse_read.m new file mode 100644 index 0000000..dc6e479 --- /dev/null +++ b/bindings/matlab/test_binsparse_read.m @@ -0,0 +1,71 @@ +% SPDX-FileCopyrightText: 2024 Binsparse Developers +% +% SPDX-License-Identifier: BSD-3-Clause + +function test_binsparse_read() +% TEST_BINSPARSE_READ - Test the binsparse_read MEX function +% +% This function demonstrates how to use the binsparse_read MEX function +% to read Binsparse format matrices into MATLAB/Octave. + +fprintf('=== Testing Binsparse Read Function ===\n\n'); + +% Check if the MEX function exists +if ~exist('binsparse_read', 'file') + error('binsparse_read MEX function not found. Build it first.'); +end + +try + % Test error handling with invalid inputs + fprintf('Test 1: Error handling\n'); + + try + matrix = binsparse_read(); % No arguments + fprintf('FAILED - Should have thrown error\n'); + catch ME + fprintf(' Correctly caught error for no arguments\n'); + end + + try + matrix = binsparse_read(123); % Non-string argument + fprintf('FAILED - Should have thrown error\n'); + catch ME + fprintf(' Correctly caught error for invalid filename type\n'); + end + + try + matrix = binsparse_read('nonexistent_file.h5'); % File doesn't exist + fprintf('FAILED - Should have thrown error\n'); + catch ME + fprintf(' Correctly caught error for nonexistent file\n'); + end + + fprintf(' Status: PASS\n\n'); + + fprintf('=== Basic Error Handling Tests Passed ===\n'); + fprintf('To test actual file reading, you need a valid Binsparse file.\n\n'); + + fprintf('Usage examples:\n'); + fprintf(' matrix = binsparse_read(''myfile.h5''); %% Read from HDF5\n'); + fprintf(' matrix = binsparse_read(''myfile.h5'', ''group''); %% Read from specific group\n'); + fprintf(' matrix = binsparse_read(''myfile.mtx''); %% Read Matrix Market file\n\n'); + + fprintf('The returned matrix will have these fields:\n'); + fprintf(' matrix.values - Values array\n'); + fprintf(' matrix.indices_0 - First dimension indices\n'); + fprintf(' matrix.indices_1 - Second dimension indices\n'); + fprintf(' matrix.pointers_to_1 - Pointers for compressed formats\n'); + fprintf(' matrix.nrows - Number of rows\n'); + fprintf(' matrix.ncols - Number of columns\n'); + fprintf(' matrix.nnz - Number of non-zeros\n'); + fprintf(' matrix.is_iso - ISO matrix flag\n'); + fprintf(' matrix.format - Matrix format string\n'); + fprintf(' matrix.structure - Matrix structure string\n'); + +catch ME + fprintf('=== TEST FAILED ===\n'); + fprintf('Error: %s\n', ME.message); + rethrow(ME); +end + +end diff --git a/bindings/matlab/test_binsparse_write.m b/bindings/matlab/test_binsparse_write.m new file mode 100644 index 0000000..54ca6be --- /dev/null +++ b/bindings/matlab/test_binsparse_write.m @@ -0,0 +1,122 @@ +% SPDX-FileCopyrightText: 2024 Binsparse Developers +% +% SPDX-License-Identifier: BSD-3-Clause + +function test_binsparse_write() +% TEST_BINSPARSE_WRITE - Test the binsparse_write MEX function +% +% This function creates a simple test matrix and writes it to a temporary +% Binsparse file using the binsparse_write MEX function. + +fprintf('=== Testing binsparse_write MEX function ===\n\n'); + +% Check if binsparse_write exists +if ~exist('binsparse_write', 'file') + error('binsparse_write MEX function not found. Please compile it first.'); +end + +try + % Create a simple test matrix in the expected struct format + fprintf('Creating test matrix...\n'); + + % Simple 3x3 COO matrix: + % [1.0, 0.0, 2.0] + % [0.0, 3.0, 0.0] + % [4.0, 0.0, 5.0] + + matrix = struct(); + matrix.values = [1.0; 3.0; 2.0; 4.0; 5.0]; % Values in COO order + matrix.indices_0 = uint64([0; 1; 0; 2; 2]); % Row indices (0-based) + matrix.indices_1 = uint64([0; 1; 2; 0; 2]); % Column indices (0-based) + matrix.pointers_to_1 = uint64([]); % Empty for COO format + matrix.nrows = 3; + matrix.ncols = 3; + matrix.nnz = 5; + matrix.is_iso = false; + matrix.format = 'COO'; + matrix.structure = 'general'; + + fprintf('Matrix created:\n'); + fprintf(' Format: %s\n', matrix.format); + fprintf(' Size: %dx%d\n', matrix.nrows, matrix.ncols); + fprintf(' NNZ: %d\n', matrix.nnz); + fprintf(' Structure: %s\n', matrix.structure); + + % Create temporary filename + temp_file = tempname(); + temp_file = [temp_file, '.bsp.h5']; + + fprintf('\nWriting matrix to: %s\n', temp_file); + + % Test basic write + binsparse_write(temp_file, matrix); + fprintf('✓ Basic write successful\n'); + + % Test with group + binsparse_write(temp_file, matrix, 'test_group'); + fprintf('✓ Write with group successful\n'); + + % Test with group and JSON metadata + json_metadata = '{"test": "metadata", "created_by": "test_binsparse_write"}'; + binsparse_write(temp_file, matrix, 'test_group_json', json_metadata); + fprintf('✓ Write with group and JSON successful\n'); + + % Test with all parameters + binsparse_write(temp_file, matrix, 'test_group_full', json_metadata, 6); + fprintf('✓ Write with all parameters successful\n'); + + % Verify file exists and has reasonable size + if exist(temp_file, 'file') + info = dir(temp_file); + fprintf('✓ Output file created (size: %d bytes)\n', info.bytes); + else + error('Output file was not created'); + end + + % Test round-trip if binsparse_read is available + if exist('binsparse_read', 'file') + fprintf('\nTesting round-trip (write → read)...\n'); + + % Read back the matrix + read_matrix = binsparse_read(temp_file, 'test_group_full'); + + % Basic verification + if read_matrix.nrows == matrix.nrows && ... + read_matrix.ncols == matrix.ncols && ... + read_matrix.nnz == matrix.nnz + fprintf('✓ Round-trip dimensions match\n'); + else + warning('Round-trip dimensions mismatch'); + end + + if strcmp(read_matrix.format, matrix.format) && ... + strcmp(read_matrix.structure, matrix.structure) + fprintf('✓ Round-trip format/structure match\n'); + else + warning('Round-trip format/structure mismatch'); + end + else + fprintf('\nSkipping round-trip test (binsparse_read not available)\n'); + end + + % Clean up + if exist(temp_file, 'file') + delete(temp_file); + fprintf('✓ Temporary file cleaned up\n'); + end + + fprintf('\n=== All tests passed! ===\n'); + fprintf('binsparse_write is working correctly.\n\n'); + +catch ME + % Clean up on error + if exist('temp_file', 'var') && exist(temp_file, 'file') + delete(temp_file); + end + + fprintf('\n=== Test failed ===\n'); + fprintf('Error: %s\n', ME.message); + rethrow(ME); +end + +end diff --git a/bindings/matlab/test_bsp_matrix_struct.m b/bindings/matlab/test_bsp_matrix_struct.m new file mode 100644 index 0000000..f3f3062 --- /dev/null +++ b/bindings/matlab/test_bsp_matrix_struct.m @@ -0,0 +1,76 @@ +% SPDX-FileCopyrightText: 2024 Binsparse Developers +% +% SPDX-License-Identifier: BSD-3-Clause + +function test_bsp_matrix_struct() +% TEST_BSP_MATRIX_STRUCT - Test the Binsparse matrix struct functionality +% +% This function demonstrates and tests the basic MATLAB struct +% that mirrors the C bsp_matrix_t structure. + +fprintf('=== Testing Binsparse Matrix Struct ===\n\n'); + +try + % Test 1: Create empty matrix + fprintf('Test 1: Creating empty matrix\n'); + empty_matrix = bsp_matrix_create(); + fprintf('Empty matrix created successfully\n'); + bsp_matrix_info(empty_matrix); + fprintf('\n'); + + % Test 2: Create simple COO matrix + fprintf('Test 2: Creating simple COO matrix\n'); + % 3x3 identity matrix in COO format + values = [1.0, 1.0, 1.0]; + rows = uint64([0, 1, 2]); % 0-based indexing like C + cols = uint64([0, 1, 2]); % 0-based indexing like C + pointers = uint64([]); % Empty for COO format + + coo_matrix = bsp_matrix_create(values, rows, cols, pointers, ... + 3, 3, 3, false, 'COO', 'general'); + fprintf('COO matrix created successfully\n'); + bsp_matrix_info(coo_matrix); + fprintf('\n'); + + % Test 3: Create CSR matrix + fprintf('Test 3: Creating simple CSR matrix\n'); + % Same 3x3 identity in CSR format + csr_values = [1.0, 1.0, 1.0]; + csr_cols = uint64([0, 1, 2]); + csr_rows = uint64([]); % Not used in CSR + csr_ptrs = uint64([0, 1, 2, 3]); % Row pointers + + csr_matrix = bsp_matrix_create(csr_values, csr_rows, csr_cols, csr_ptrs, ... + 3, 3, 3, false, 'CSR', 'general'); + fprintf('CSR matrix created successfully\n'); + bsp_matrix_info(csr_matrix); + fprintf('\n'); + + % Test 4: Test field access + fprintf('Test 4: Testing field access\n'); + fprintf('Matrix format: %s\n', csr_matrix.format); + fprintf('Matrix structure: %s\n', csr_matrix.structure); + fprintf('Is ISO: %s\n', mat2str(csr_matrix.is_iso)); + fprintf('First value: %.1f\n', csr_matrix.values(1)); + fprintf('\n'); + + % Test 5: Test error handling + fprintf('Test 5: Testing error handling\n'); + try + invalid_matrix = bsp_matrix_create(1, 2, 3); % Wrong number of args + fprintf('FAILED - Should have thrown error\n'); + catch ME + fprintf('Successfully caught error: %s\n', ME.message); + end + fprintf('\n'); + + fprintf('=== All Tests Passed ===\n'); + fprintf('The Binsparse matrix struct is working correctly!\n'); + +catch ME + fprintf('=== TEST FAILED ===\n'); + fprintf('Error: %s\n', ME.message); + rethrow(ME); +end + +end diff --git a/src/write_matrix.c b/src/write_matrix.c index d87244a..7d1956d 100644 --- a/src/write_matrix.c +++ b/src/write_matrix.c @@ -137,9 +137,6 @@ bsp_error_t bsp_write_matrix_to_group(hid_t f, bsp_matrix_t matrix, const char* user_json, int compression_level) { cJSON* user_json_cjson = cJSON_Parse(user_json); - if (user_json_cjson == NULL) { - return BSP_ERROR_FORMAT; - } bsp_error_t error = bsp_write_matrix_to_group_cjson( f, matrix, user_json_cjson, compression_level); cJSON_Delete(user_json_cjson); @@ -154,6 +151,7 @@ bsp_error_t bsp_write_matrix_cjson(const char* fname, bsp_matrix_t matrix, bsp_error_t error = bsp_write_matrix_to_group_cjson(f, matrix, user_json, compression_level); if (error != BSP_SUCCESS) { + printf("AGH! HDF5!\n"); H5Fclose(f); return error; } @@ -169,6 +167,7 @@ bsp_error_t bsp_write_matrix_cjson(const char* fname, bsp_matrix_t matrix, bsp_error_t error = bsp_write_matrix_to_group_cjson(g, matrix, user_json, compression_level); if (error != BSP_SUCCESS) { + printf("AGH! Inner write!\n"); H5Gclose(g); H5Fclose(f); return error; @@ -183,9 +182,7 @@ bsp_error_t bsp_write_matrix(const char* fname, bsp_matrix_t matrix, const char* group, const char* user_json, int compression_level) { cJSON* user_json_cjson = cJSON_Parse(user_json); - if (user_json_cjson == NULL) { - return BSP_ERROR_FORMAT; - } + bsp_error_t error = bsp_write_matrix_cjson( fname, matrix, group, user_json_cjson, compression_level); cJSON_Delete(user_json_cjson);