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
- 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:
Copy
# 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:
Copy
# 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:
Copy
# 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:Security Best Practice:Store API keys in environment variables:
Copy
# In Julia
using NimbusSDK
# One-time setup (installs core and authenticates)
NimbusSDK.install_core("your-api-key-here")
# Verify installation
println("✓ Core installed successfully")
Copy
# In your shell (.bashrc, .zshrc, etc.)
export NIMBUS_API_KEY="your-api-key-here"
Copy
# In Julia
api_key = ENV["NIMBUS_API_KEY"]
NimbusSDK.install_core(api_key)
Project Structure
Organize your Julia BCI project:Copy
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
Copy
# 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
Copy
# 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
Copy
# 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
Copy
# 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
Copy
# 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
Copy
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
Copy
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
Slow First Inference
Slow First Inference
Cause: Julia’s JIT compilationSolution: Run warmup inferences
Copy
# 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
Dimension Mismatch
Dimension Mismatch
Cause: Feature count doesn’t match modelSolution: Verify dimensions
Copy
println("Model expects: $(get_n_features(model)) features")
println("Data has: $(size(data.features, 1)) features")
Low Confidence
Low Confidence
Cause: Poor preprocessing or model calibrationSolution: Run diagnostics
Copy
report = diagnose_preprocessing(data)
println("Quality: $(report.quality_score)")
for warning in report.warnings
println(" • $warning")
end
Next Steps
Julia SDK Reference
Complete API documentation
Code Examples
Working Julia examples
Model Training
Train custom models on your data
Preprocessing
Integrate with MNE, EEGLAB, etc.
Support
Need help?- Email: hello@nimbusbci.com
- Documentation: Browse comprehensive guides
- GitHub: Report issues and contribute