Skip to main content

Development Guide

This guide covers the development workflow for building BCI applications with NimbusSDK in Julia, from local setup to production deployment.

Development Environment Setup

Prerequisites

Required:
  • Julia 1.9+ (1.10+ recommended for best performance)
  • Git for version control
  • API Key from Nimbus BCI
  • EEG Hardware or sample data for testing
Recommended:
  • VSCode with Julia extension
  • Jupyter for interactive development
  • Preprocessing pipeline (CSP, bandpower, ERP extraction)

Installation

1

Install Julia

Download and install Julia from julialang.org:
# Verify installation
julia --version
# Expected: julia version 1.10.0 (or later)
2

Create Project Environment

Set up an isolated Julia environment for your BCI project:
# Create project directory
mkdir my-bci-project
cd my-bci-project

# Initialize Julia project
julia --project=. -e 'using Pkg; Pkg.instantiate()'
3

Install NimbusSDK

Add NimbusSDK to your project:
# Start Julia REPL
julia --project=.

# Install NimbusSDK from Julia General Registry
using Pkg
Pkg.add("NimbusSDK")

# Verify installation
using NimbusSDK
println("✓ NimbusSDK installed successfully")
4

Configure API Access

Set up your API credentials:
# In Julia
using NimbusSDK

# One-time setup (installs core and authenticates)
NimbusSDK.install_core("your-api-key-here")

# Verify installation
println("✓ Core installed successfully")
Security Best Practice:Store API keys in environment variables:
# In your shell (.bashrc, .zshrc, etc.)
export NIMBUS_API_KEY="your-api-key-here"
# In Julia
api_key = ENV["NIMBUS_API_KEY"]
NimbusSDK.install_core(api_key)

Project Structure

Organize your Julia BCI project:
my-bci-project/
├── Project.toml          # Julia dependencies
├── Manifest.toml         # Locked dependency versions
├── src/
│   ├── MyBCIProject.jl   # Main module
│   ├── preprocessing.jl   # Feature extraction
│   ├── training.jl        # Model training
│   ├── inference.jl       # Real-time inference
│   └── utils.jl           # Utility functions
├── data/
│   ├── raw/               # Raw EEG recordings
│   ├── processed/         # Preprocessed features
│   └── models/            # Trained models
├── test/
│   ├── runtests.jl        # Test suite
│   └── test_inference.jl  # Inference tests
├── examples/
│   └── motor_imagery_demo.jl
└── README.md

Development Workflow

1. Data Preparation

Preprocessing Pipeline

# src/preprocessing.jl
using DSP, LinearAlgebra, Statistics

"""
    extract_csp_features(eeg_data, csp_filters) -> features

Extract CSP features for motor imagery BCI.
"""
function extract_csp_features(eeg_data::Matrix{Float64}, csp_filters::Matrix{Float64})
    # eeg_data: [n_channels × n_samples]
    # csp_filters: [n_components × n_channels]
    
    # Apply spatial filters
    spatial_filtered = csp_filters * eeg_data
    
    # Compute log-variance features
    features = log.(var(spatial_filtered, dims=2) .+ 1e-10)
    
    return vec(features)
end

"""
    extract_erp_features(eeg_epoch, time_windows) -> features

Extract ERP amplitude features for P300 BCI.
"""
function extract_erp_features(eeg_epoch::Matrix{Float64}, time_windows::Vector)
    # eeg_epoch: [n_channels × n_samples]
    # time_windows: [(start_sample, end_sample), ...]
    
    features = Float64[]
    
    for (start_idx, end_idx) in time_windows
        window_data = eeg_epoch[:, start_idx:end_idx]
        # Mean amplitude in window for each channel
        append!(features, mean(window_data, dims=2))
    end
    
    return features
end

"""
    bandpass_filter(data, low_freq, high_freq, fs) -> filtered_data

Apply bandpass filter to EEG data.
"""
function bandpass_filter(data::Matrix{Float64}, low_freq::Float64, high_freq::Float64, fs::Float64)
    # Design Butterworth bandpass filter
    responsetype = Bandpass(low_freq, high_freq; fs=fs)
    designmethod = Butterworth(4)  # 4th order
    
    # Apply filter to each channel
    filtered = similar(data)
    for ch in 1:size(data, 1)
        filtered[ch, :] = filtfilt(digitalfilter(responsetype, designmethod), data[ch, :])
    end
    
    return filtered
end

2. Model Training

Training Workflow

# src/training.jl
using NimbusSDK
using Statistics

"""
    train_motor_imagery_model(data_file::String) -> trained_model

Complete training workflow for motor imagery BCI.
"""
function train_motor_imagery_model(data_file::String)
    println("="^60)
    println("Motor Imagery Model Training")
    println("="^60)
    
    # 1. Load and preprocess data
    println("\n1. Loading data...")
    raw_data = load_eeg_data(data_file)
    
    # 2. Extract features (CSP)
    println("2. Extracting CSP features...")
    features, labels = extract_mi_features(raw_data)
    # features: [n_features × n_samples × n_trials]
    # labels: [n_trials]
    
    # 3. Create BCIData
    metadata = BCIMetadata(
        sampling_rate = 250.0,
        paradigm = :motor_imagery,
        feature_type = :csp,
        n_features = size(features, 1),
        n_classes = length(unique(labels)),
        chunk_size = nothing  # Batch mode
    )
    
    training_data = BCIData(features, metadata, labels)
    
    # 4. Validate data
    println("3. Validating data...")
    report = diagnose_preprocessing(training_data)
    println("   Quality score: $(round(report.quality_score * 100, digits=1))%")
    
    if report.quality_score < 0.6
        @warn "Low quality score - check preprocessing" score=report.quality_score
    end
    
    # 5. Split into train/test
    println("4. Splitting train/test...")
    train_data, test_data = split_data(training_data, test_ratio=0.2)
    
    # 6. Train model
    println("5. Training RxLDA model...")
    trained_model = train_model(
        RxLDAModel,
        train_data;
        iterations = 50,
        showprogress = true,
        name = "my_motor_imagery_model",
        description = "Trained on $(size(features, 3)) trials"
    )
    
    # 7. Evaluate
    println("\n6. Evaluating on test set...")
    test_results = predict_batch(trained_model, test_data)
    
    accuracy = sum(test_results.predictions .== test_data.labels) / length(test_results.predictions)
    mean_conf = mean(test_results.confidences)
    
    println("\n" * "="^60)
    println("Training Results")
    println("="^60)
    println("Test Accuracy: $(round(accuracy * 100, digits=1))%")
    println("Mean Confidence: $(round(mean_conf, digits=3))")
    
    # 8. Save model
    save_path = "data/models/motor_imagery_$(today()).jld2"
    save_model(trained_model, save_path)
    println("\n✓ Model saved to: $save_path")
    
    return trained_model
end

"""
    split_data(data::BCIData, test_ratio::Float64) -> (train, test)

Split BCIData into training and test sets.
"""
function split_data(data::BCIData, test_ratio::Float64=0.2)
    n_trials = size(data.features, 3)
    n_test = Int(floor(n_trials * test_ratio))
    n_train = n_trials - n_test
    
    # Random shuffle
    indices = randperm(n_trials)
    train_idx = indices[1:n_train]
    test_idx = indices[n_train+1:end]
    
    # Split features and labels
    train_features = data.features[:, :, train_idx]
    test_features = data.features[:, :, test_idx]
    
    train_labels = isnothing(data.labels) ? nothing : data.labels[train_idx]
    test_labels = isnothing(data.labels) ? nothing : data.labels[test_idx]
    
    train_data = BCIData(train_features, data.metadata, train_labels)
    test_data = BCIData(test_features, data.metadata, test_labels)
    
    return train_data, test_data
end

3. Real-Time Inference

Streaming Application

# src/inference.jl
using NimbusSDK
using Statistics

"""
    run_realtime_bci(model_path::String; duration_seconds::Int=60)

Run real-time BCI application.
"""
function run_realtime_bci(model_path::String; duration_seconds::Int=60)
    println("="^60)
    println("Real-Time BCI Application")
    println("="^60)
    
    # 1. Authenticate
    NimbusSDK.install_core(ENV["NIMBUS_API_KEY"])
    
    # 2. Load model
    println("\nLoading model...")
    model = load_model(RxLDAModel, model_path)
    println("✓ Model loaded: $(model.metadata.name)")
    
    # 3. Configure streaming
    metadata = BCIMetadata(
        sampling_rate = 250.0,
        paradigm = :motor_imagery,
        feature_type = :csp,
        n_features = 16,
        n_classes = 4,
        chunk_size = 250  # 1-second chunks
    )
    
    # 4. Initialize session
    println("Initializing streaming session...")
    session = init_streaming(model, metadata)
    println("✓ Session initialized")
    
    # 5. Warmup
    println("\nWarmup (JIT compilation)...")
    dummy_chunk = randn(16, 250)
    for _ in 1:10
        process_chunk(session, dummy_chunk)
    end
    println("✓ Warmup complete")
    
    # 6. Real-time loop
    println("\n" * "="^60)
    println("Starting real-time processing ($(duration_seconds)s)")
    println("="^60)
    
    chunk_count = 0
    start_time = time()
    latencies = Float64[]
    confidences = Float64[]
    
    while (time() - start_time) < duration_seconds
        # Acquire EEG chunk (replace with your acquisition code)
        raw_chunk = acquire_eeg_chunk()  # Your hardware interface
        
        # Extract features
        feature_chunk = extract_csp_features(raw_chunk, model.csp_filters)
        
        # Time inference
        t_start = time()
        result = process_chunk(session, feature_chunk)
        latency_ms = (time() - t_start) * 1000
        
        chunk_count += 1
        push!(latencies, latency_ms)
        push!(confidences, result.confidence)
        
        # Execute BCI command
        if result.confidence > 0.75
            execute_bci_command(result.prediction)
        end
        
        # Report every 10 chunks
        if chunk_count % 10 == 0
            println("\nChunk $chunk_count:")
            println("  Mean latency: $(round(mean(latencies[max(1, end-9):end]), digits=1)) ms")
            println("  Mean confidence: $(round(mean(confidences[max(1, end-9):end]), digits=3))")
        end
    end
    
    # 7. Summary
    println("\n" * "="^60)
    println("Session Complete")
    println("="^60)
    println("Total chunks: $chunk_count")
    println("Mean latency: $(round(mean(latencies), digits=1)) ms")
    println("Mean confidence: $(round(mean(confidences), digits=3))")
end

"""
    execute_bci_command(prediction::Int)

Execute BCI command based on prediction.
"""
function execute_bci_command(prediction::Int)
    commands = ["Left", "Right", "Forward", "Stop"]
    println("→ Command: $(commands[prediction])")
    
    # Your application logic here
    # e.g., control wheelchair, game, cursor, etc.
end

4. Testing

Test Suite

# test/runtests.jl
using Test
using NimbusSDK

@testset "NimbusSDK Tests" begin
    
    @testset "Data Validation" begin
        # Test BCIData creation
        features = randn(16, 250, 10)
        metadata = BCIMetadata(
            sampling_rate = 250.0,
            n_features = 16,
            n_classes = 4
        )
        labels = rand(1:4, 10)
        
        data = BCIData(features, metadata, labels)
        @test size(data.features) == (16, 250, 10)
        @test length(data.labels) == 10
    end
    
    @testset "Model Training" begin
        # Test RxLDA training
        features = randn(8, 100, 50)
        labels = rand(1:2, 50)
        metadata = BCIMetadata(
            sampling_rate = 250.0,
            n_features = 8,
            n_classes = 2
        )
        
        data = BCIData(features, metadata, labels)
        model = train_model(RxLDAModel, data; iterations=10)
        
        @test model isa RxLDAModel
        @test get_n_features(model) == 8
        @test get_n_classes(model) == 2
    end
    
    @testset "Batch Inference" begin
        # Test prediction
        features = randn(8, 100, 20)
        metadata = BCIMetadata(
            sampling_rate = 250.0,
            n_features = 8,
            n_classes = 2
        )
        
        data = BCIData(features, metadata)
        model = train_model(RxLDAModel, BCIData(randn(8, 100, 30), metadata, rand(1:2, 30)); iterations=10)
        
        results = predict_batch(model, data)
        
        @test length(results.predictions) == 20
        @test length(results.confidences) == 20
        @test all(0 .<= results.confidences .<= 1)
    end
    
    @testset "Streaming Inference" begin
        # Test streaming
        model = train_model(RxLDAModel, BCIData(randn(8, 100, 30), metadata, rand(1:2, 30)); iterations=10)
        
        metadata = BCIMetadata(
            sampling_rate = 250.0,
            n_features = 8,
            n_classes = 2,
            chunk_size = 100
        )
        
        session = init_streaming(model, metadata)
        
        chunk = randn(8, 100)
        result = process_chunk(session, chunk)
        
        @test result.prediction in 1:2
        @test 0 <= result.confidence <= 1
    end
end

println("\n✓ All tests passed!")

Best Practices

Code Organization

Module Structure

# src/MyBCIProject.jl
module MyBCIProject

using NimbusSDK
using Statistics

include("preprocessing.jl")
include("training.jl")
include("inference.jl")
include("utils.jl")

export train_motor_imagery_model,
       run_realtime_bci,
       extract_csp_features

end  # module

Error Handling

function safe_inference(model, data::BCIData)
    try
        results = predict_batch(model, data)
        return results
    catch e
        if e isa DimensionMismatch
            @error "Feature dimension mismatch" expected=get_n_features(model) actual=size(data.features, 1)
        elseif e isa ArgumentError
            @error "Invalid argument" exception=e
        else
            @error "Inference failed" exception=e
        end
        return nothing
    end
end

Performance Monitoring

using Statistics

struct PerformanceMonitor
    latencies::Vector{Float64}
    confidences::Vector{Float64}
end

PerformanceMonitor() = PerformanceMonitor(Float64[], Float64[])

function record!(monitor::PerformanceMonitor, latency_ms::Float64, confidence::Float64)
    push!(monitor.latencies, latency_ms)
    push!(monitor.confidences, confidence)
end

function report(monitor::PerformanceMonitor)
    println("Performance Report:")
    println("  Mean latency: $(round(mean(monitor.latencies), digits=1)) ms")
    println("  95th percentile: $(round(quantile(monitor.latencies, 0.95), digits=1)) ms")
    println("  Mean confidence: $(round(mean(monitor.confidences), digits=3))")
end

Debugging Tips

Common Issues

Cause: Julia’s JIT compilationSolution: Run warmup inferences
# Warmup
dummy_data = randn(16, 250, 1)
for _ in 1:10
    predict_batch(model, BCIData(dummy_data, metadata))
end
# Now real inference will be fast
Cause: Feature count doesn’t match modelSolution: Verify dimensions
println("Model expects: $(get_n_features(model)) features")
println("Data has: $(size(data.features, 1)) features")
Cause: Poor preprocessing or model calibrationSolution: Run diagnostics
report = diagnose_preprocessing(data)
println("Quality: $(report.quality_score)")
for warning in report.warnings
    println("  • $warning")
end

Next Steps

Support

Need help?
  • Email: hello@nimbusbci.com
  • Documentation: Browse comprehensive guides
  • GitHub: Report issues and contribute
Happy coding! 🚀 Build amazing BCI applications with NimbusSDK.