Error Handling
SPIRES reports errors through return codes on every API function. Consistent error checking is essential for writing robust programs, especially when configurations come from user input or automated optimization.
Status Codes
All SPIRES functions that can fail return a spires_status value:
typedef enum {
SPIRES_OK = 0, /* Success */
SPIRES_ERR_INVALID_ARG = 1, /* Invalid argument */
SPIRES_ERR_ALLOC = 2, /* Memory allocation failure */
SPIRES_ERR_INTERNAL = 3, /* Internal error */
} spires_status;SPIRES_OK (0)
The operation completed successfully. All output parameters are valid and the reservoir state has been updated as documented.
SPIRES_ERR_INVALID_ARG (1)
One or more arguments are invalid. Common causes:
NULLpointer passed where a valid pointer is requirednum_neuronsis zero or negativespectral_radiusis zero or negativeconnectivityis outside the rangeseries_lengthis zerolambdais negative in ridge regressionlearning_rateis zero or negative in online training- Unknown enum value for
neuron_typeorconnectivity_type - Buffer size is too small for
spires_read_reservoir_state()
When this error is returned, the reservoir state is unchanged (the function had no side effects). Fix the invalid argument and retry.
SPIRES_ERR_ALLOC (2)
A memory allocation (malloc, calloc, or internal allocator) failed. This typically means the system is out of memory. Common causes:
- Requesting a very large reservoir (e.g.,
num_neurons = 100000) - Very long history lengths for fractional neurons
- Insufficient system memory
- Memory fragmentation
When this error is returned, the reservoir state is unchanged and no memory has been leaked. The function cleans up any partial allocations before returning.
SPIRES_ERR_INTERNAL (3)
An unexpected internal error occurred. This may indicate:
- A LAPACK routine failed during training (e.g., the matrix is not positive definite despite regularization)
- A numerical overflow or NaN was detected
- An assertion failure in the library’s internal checks
This error should be rare. If you encounter it, check:
- That your data does not contain NaN or infinity values.
- That the regularization parameter is not too small (try or larger).
- That the spectral radius is reasonable (typically ).
Checking Return Values
Every SPIRES function call should have its return value checked. The recommended pattern is:
spires_status s = spires_some_function(/* ... */);
if (s != SPIRES_OK) {
/* Handle the error */
}Minimal Error Handling
For simple programs and prototypes, printing the error and exiting is sufficient:
spires_reservoir *r = NULL;
spires_status s = spires_reservoir_create(&cfg, &r);
if (s != SPIRES_OK) {
fprintf(stderr, "Failed to create reservoir: error %d\n", s);
return 1;
}Structured Error Handling
For production code, handle each error type specifically:
spires_status s = spires_reservoir_create(&cfg, &r);
switch (s) {
case SPIRES_OK:
break; /* success */
case SPIRES_ERR_INVALID_ARG:
fprintf(stderr, "Invalid configuration. Check parameters.\n");
return 1;
case SPIRES_ERR_ALLOC:
fprintf(stderr, "Out of memory. Reduce reservoir size.\n");
return 1;
case SPIRES_ERR_INTERNAL:
fprintf(stderr, "Internal error. Check data for NaN/Inf.\n");
return 1;
}Error Handling with Cleanup
When multiple resources are allocated, errors must be handled carefully to avoid leaks. A common pattern uses goto for cleanup:
int run_experiment(const spires_reservoir_config *cfg,
const double *input, const double *target,
size_t len, double lambda) {
int ret = -1;
spires_reservoir *r = NULL;
double *predictions = NULL;
spires_status s = spires_reservoir_create(cfg, &r);
if (s != SPIRES_OK) {
fprintf(stderr, "Create failed: %d\n", s);
goto cleanup;
}
s = spires_train_ridge(r, input, target, len, lambda);
if (s != SPIRES_OK) {
fprintf(stderr, "Training failed: %d\n", s);
goto cleanup;
}
predictions = spires_run(r, input, len);
if (!predictions) {
fprintf(stderr, "Inference failed\n");
goto cleanup;
}
/* Use predictions */
ret = 0;
cleanup:
free(predictions); /* safe: free(NULL) is a no-op */
spires_reservoir_destroy(r); /* safe: destroy(NULL) is a no-op */
return ret;
}This pattern ensures that all resources are freed on every code path, whether the function succeeds or encounters an error at any stage.
Recovery Strategies
Retry with Adjusted Parameters
Some errors can be recovered from by adjusting parameters:
/* If training fails, try with stronger regularization */
double lambda = 1e-8;
spires_status s;
for (int attempt = 0; attempt < 5; attempt++) {
s = spires_train_ridge(r, input, target, len, lambda);
if (s == SPIRES_OK) break;
if (s == SPIRES_ERR_INTERNAL) {
lambda *= 10.0; /* increase regularization */
fprintf(stderr, "Training failed, retrying with lambda=%.1e\n", lambda);
spires_reservoir_reset(r); /* reset state before retry */
} else {
break; /* non-recoverable error */
}
}Fallback Configuration
If the desired configuration fails, fall back to a simpler one:
/* Try fractional reservoir first */
cfg.neuron_type = SPIRES_NEURON_FLIF_GL;
spires_status s = spires_reservoir_create(&cfg, &r);
if (s == SPIRES_ERR_ALLOC) {
/* Not enough memory for fractional history buffers */
/* Fall back to simpler model */
fprintf(stderr, "Falling back to LIF_DISCRETE\n");
cfg.neuron_type = SPIRES_NEURON_LIF_DISCRETE;
cfg.neuron_params = NULL;
s = spires_reservoir_create(&cfg, &r);
}Graceful Degradation in Optimization
During automated optimization, some configurations will fail. The optimizer handles this internally by assigning the worst possible score to failed configurations. However, if you are writing your own evaluation loop, handle failures gracefully:
double evaluate_config(const spires_reservoir_config *cfg,
const double *input, const double *target,
size_t len, double lambda) {
spires_reservoir *r = NULL;
spires_status s = spires_reservoir_create(cfg, &r);
if (s != SPIRES_OK) return -1.0; /* worst score */
s = spires_train_ridge(r, input, target, len, lambda);
if (s != SPIRES_OK) {
spires_reservoir_destroy(r);
return -1.0;
}
double *pred = spires_run(r, input, len);
if (!pred) {
spires_reservoir_destroy(r);
return -1.0;
}
double score = compute_metric(pred, target, len);
free(pred);
spires_reservoir_destroy(r);
return score;
}Functions That Return Pointers
Some SPIRES functions return pointers instead of status codes. For these functions, a NULL return indicates failure:
| Function | Return on Success | Return on Failure |
|---|---|---|
spires_run() | double * (allocated array) | NULL |
spires_copy_reservoir_state() | double * (allocated array) | NULL |
spires_compute_output() | double * (allocated array) | NULL |
Always check for NULL:
double *pred = spires_run(r, input, len);
if (pred == NULL) {
fprintf(stderr, "spires_run failed\n");
/* handle error */
}Debugging Tips
Data Validation
Before passing data to SPIRES, validate it:
/* Check for NaN and Inf in input data */
for (size_t i = 0; i < len * num_inputs; i++) {
if (isnan(input[i]) || isinf(input[i])) {
fprintf(stderr, "Invalid value at input[%zu]: %f\n", i, input[i]);
return SPIRES_ERR_INVALID_ARG;
}
}Assertion-Heavy Development
During development, use assertions liberally:
#include <assert.h>
spires_reservoir *r = NULL;
spires_status s = spires_reservoir_create(&cfg, &r);
assert(s == SPIRES_OK && "Reservoir creation must succeed in tests");
assert(r != NULL);Compile with -DNDEBUG in production to disable assertions.
Address Sanitizer
Build your program with AddressSanitizer to detect memory errors:
gcc -fsanitize=address -g -o my_program my_program.c \
-lspires -lopenblas -llapacke -lm -fopenmpThis detects use-after-free, buffer overflows, and memory leaks at runtime with a moderate (~2x) performance penalty.