RMNLib Documentation

Warning

🚧 DEVELOPMENT STATUS: ALPHA - NOT READY FOR USE 🚧

⚠️ This project is in early development and is NOT suitable for production use.

  • API is unstable and subject to major changes

  • Many features are incomplete or untested

  • Documentation may be outdated or incorrect

  • Breaking changes will occur without notice

  • DO NOT USE in any production environment

This documentation is shared for development purposes only. Check back later for stable releases.


RMNLib is a C library for reading, writing, and manipulating Core Scientific Dataset Model (CSDM) files. It provides a comprehensive API for working with multidimensional scientific datasets, including sparse sampling, geographic coordinates, and various dimension types.

Requirements

Ensure you have installed:

  • A C compiler (e.g., clang or gcc)

  • Make

  • Doxygen

  • Python 3 with sphinx and breathe (pip install -r docs/requirements.txt)

Building the Library

Compile the static library:

make

This produces libRMN.a.

Usage

Basic Library Setup

Include the main library header in your C code:

#include "RMNLibrary.h"

Link against the static library when compiling:

gcc -o myprogram myprogram.c -L. -lRMNLib -lm

Creating Datasets

RMNLib revolves around datasets containing multidimensional scientific data. Here’s a simple linear dimension example:

OCStringRef error = NULL;

// Simulate damped complex oscillation data (typical NMR/ESR experiment)
OCIndex count = 1024;
double complex *values = malloc(count * sizeof(double complex));
double frequency = 100.0;    // Hz
double decayRate = 50.0;     // s⁻¹ (decay constant)

for (int i = 0; i < count; i++) {
    double time = i * 1.0e-6;  // 1 µs sampling interval
    double amplitude = exp(-decayRate * time);
    double phase = 2.0 * M_PI * frequency * time;
    values[i] = amplitude * (cos(phase) + I * sin(phase));
}

// Create RMNLib dataset and export as CSDM file
SIDimensionRef freqDim = SIDimensionCreateWithQuantity(kSIQuantityFrequency, &error);
SIScalarRef increment = SIScalarCreateFromExpression(STR("1.0 µs"), &error);
SILinearDimensionRef timeDim = SILinearDimensionCreateMinimal(
    kSIQuantityTime,  // quantityName
    count,            // count
    increment,        // increment
    freqDim,          // reciprocal
    &error);          // outError
OCRelease(increment);
OCRelease(freqDim);

OCDataRef data = OCDataCreateWithBytes(values, count * sizeof(double complex));
OCArrayRef components = OCArrayCreate();
OCArrayAddValue(components, (OCTypeRef)data);
SIUnitRef voltUnit = SIUnitFromExpression(STR("V"), NULL, &error);
DependentVariableRef depVar = DependentVariableCreateMinimal(
    voltUnit,                    // unit
    kSIQuantityElectricPotential, // quantityName
    STR("scalar"),               // quantityType
    kOCNumberComplex128Type,     // numericType
    components,                  // components
    &error);                     // outError
OCRelease(components);
OCRelease(data);
if (!voltUnit || error) goto cleanup;

// Create dataset structure
OCArrayRef dimensions = OCArrayCreate();
OCArrayRef dependentVariables = OCArrayCreate();
OCArrayAddValue(dimensions, (OCTypeRef)timeDim);
OCArrayAddValue(dependentVariables, (OCTypeRef)depVar);
OCRelease(timeDim);
OCRelease(depVar);
OCRelease(voltUnit);

DatasetRef dataset = DatasetCreateMinimal(
    dimensions,           // dimensions
    dependentVariables,   // dependentVariables
    &error);              // outError
OCRelease(dimensions);
OCRelease(dependentVariables);

// Export as CSDM file (binary_dir can be NULL to use same directory as JSON)
if (dataset && !error) {
    bool success = DatasetExport(
        dataset,              // ds
        "experiment.csdf",    // json_path
        NULL,                 // binary_dir (auto-determined from json_path)
        &error);              // outError
    if (success) {
        printf("Dataset saved to experiment.csdf\n");
    }
}

// Clean up allocated memory
free(values);
if (dataset) OCRelease(dataset);
if (error) { printf("Error: %s\n", OCStringGetCString(error)); OCRelease(error); }

Monotonic Dimensions

For non-uniform sampling, use monotonic dimensions with explicit coordinates:

OCStringRef error = NULL;

// T1 inversion recovery with non-uniform time spacing
// Recovery times span multiple orders of magnitude (µs to seconds)
OCStringRef timeExpressions[] = {
    STR("10.0 µs"), STR("50.0 µs"), STR("100.0 µs"), STR("500.0 µs"),
    STR("1.0 ms"), STR("5.0 ms"), STR("10.0 ms"), STR("50.0 ms"), STR("100.0 ms"), STR("500.0 ms"),
    STR("1.0 s"), STR("2.0 s"), STR("5.0 s"), STR("10.0 s")
};
int numPoints = sizeof(timeExpressions) / sizeof(timeExpressions[0]);

// Convert string expressions to SIScalar objects and store in OCArray
OCMutableArrayRef recoveryTimes = OCArrayCreateMutable(numPoints, &kOCTypeArrayCallBacks);
for (int i = 0; i < numPoints; i++) {
    SIScalarRef timeScalar = SIScalarCreateFromExpression(timeExpressions[i], &error);
    if (!timeScalar || error) goto cleanup;
    OCArrayAppendValue(recoveryTimes, (const void*)timeScalar);
    OCRelease(timeScalar);  // Array now owns it
}

// Calculate experimental inversion recovery data: M(t) = M0 * (1 - 2*exp(-t/T1))
double T1_seconds = 0.5;  // 500 ms relaxation time
double M0 = 1000.0;       // Initial magnetization
double *magnetization = malloc(numPoints * sizeof(double));

// Create second unit once for efficiency (reused in loop)
SIUnitRef secondUnit = SIUnitFromExpression(STR("s"), NULL, &error);
if (!secondUnit || error) goto cleanup;

// Calculate magnetization values for each recovery time
for (int i = 0; i < numPoints; i++) {
    // Get time value in seconds from SIScalar in the array
    SIScalarRef timeScalar = (SIScalarRef)OCArrayGetValueAtIndex(recoveryTimes, i);
    double timeInSeconds = SIScalarDoubleValueInUnit(timeScalar, secondUnit);
    magnetization[i] = M0 * (1.0 - 2.0 * exp(-timeInSeconds / T1_seconds));
}

// Create RMNLib dataset from experimental data
// We can use the recoveryTimes array directly as coordinates
SIMonotonicDimensionRef timeDim = SIMonotonicDimensionCreateMinimal(
    kSIQuantityTime,             // quantityName
    (OCArrayRef)recoveryTimes,   // coordinates
    NULL,                        // reciprocal
    &error);                     // outError

OCDataRef magnetizationData = OCDataCreateWithBytes(magnetization, numPoints * sizeof(double));
OCMutableArrayRef components = OCArrayCreateMutable(1, &kOCTypeArrayCallBacks);
OCArrayAppendValue(components, (const void*)magnetizationData);

SIUnitRef dimensionlessUnit = SIUnitFromExpression(STR("dimensionless"), NULL, &error);
DependentVariableRef magnetizationVar = DependentVariableCreateMinimal(
    dimensionlessUnit,       // unit
    kSIQuantityDimensionless, // quantityName
    STR("scalar"),           // quantityType
    kOCNumberFloat64Type,    // numericType
    (OCArrayRef)components,  // components
    &error);                 // outError
OCRelease(components);
OCRelease(magnetizationData);
if (!dimensionlessUnit || error) goto cleanup;

OCMutableArrayRef dimensions = OCArrayCreateMutable(1, &kOCTypeArrayCallBacks);
OCMutableArrayRef dependentVariables = OCArrayCreateMutable(1, &kOCTypeArrayCallBacks);
OCArrayAppendValue(dimensions, (const void*)timeDim);
OCArrayAppendValue(dependentVariables, (const void*)magnetizationVar);
OCRelease(timeDim);
OCRelease(magnetizationVar);
OCRelease(dimensionlessUnit);

DatasetRef inversionRecoveryDataset = DatasetCreateMinimal(
    (OCArrayRef)dimensions,       // dimensions
    (OCArrayRef)dependentVariables, // dependentVariables
    &error);                      // outError
OCRelease(dimensions);
OCRelease(dependentVariables);

if (inversionRecoveryDataset && !error) {
    printf("Successfully created T1 inversion recovery dataset\n");
    printf("Time range: 10 µs to 10 s (6 orders of magnitude)\n");
    printf("T1 relaxation time: %.1f ms\n", T1_seconds * 1000);

    // Export as CSDM file
    bool success = DatasetExport(
        inversionRecoveryDataset,  // ds
        "T1_recovery.csdf",        // json_path
        NULL,                      // binary_dir (auto-determined)
        &error);                   // outError
    if (success) {
        printf("Dataset saved to T1_recovery.csdf\n");
    }
}

// Clean up allocated memory
free(magnetization);
OCRelease(recoveryTimes);
if (inversionRecoveryDataset) OCRelease(inversionRecoveryDataset);
cleanup:

if (error) { printf(“Error: %sn”, OCStringGetCString(error)); OCRelease(error); }

Memory Management

RMNLib uses reference counting for memory management. Follow these rules:

// Functions with "Create" or "Copy" return objects you own
DatasetRef dataset = DatasetCreate();           // retain count = 1, you own it
DatasetRef copy = DatasetCopy(dataset);         // retain count = 1, you own the copy

// Retain an object to keep it alive
OCRetain(dataset);                              // retain count = 2

// Release when done (decrements retain count)
OCRelease(dataset);                             // retain count = 1
OCRelease(dataset);                             // retain count = 0, object is deallocated
OCRelease(copy);                                // Clean up the copy

Best Practice: Release objects immediately when ownership transfers

SIScalarRef increment = SIScalarCreateFromExpression(STR("1.0 s"), &error);
SILinearDimensionRef dimension = SILinearDimensionCreateMinimal(
    kSIQuantityTime, 100, increment, NULL, &error);

OCRelease(increment);  // Release immediately - dimension now owns it

OCArrayRef dimensions = OCArrayCreate();
OCArrayAddValue(dimensions, (OCTypeRef)dimension);

OCRelease(dimension);  // Release immediately - array now owns it

This pattern provides lower memory pressure, clearer ownership semantics, and easier debugging.

Metadata and Error Handling

// Metadata with JSON serialization
OCDictionaryRef metadata = OCDictionaryCreate();
OCDictionarySetValue(metadata, STR("experiment"), STR("T1_measurement"));

cJSON *json = OCMetadataCopyJSON(metadata);
char *jsonString = cJSON_Print(json);

// Error handling - always check return values
DatasetRef dataset = DatasetCreate();
if (!dataset) {
    fprintf(stderr, "Failed to create dataset\n");
    return -1;
}

// Clean up
free(jsonString);
cJSON_Delete(json);
OCRelease(metadata);
OCRelease(dataset);

// Library shutdown for leak detection
RMNLibTypesShutdown();

Indices and tables