Skip to main content

Real-time Inference Setup

Setting up NimbusSDK for real-time BCI applications requires proper configuration to achieve 10-25ms latency while maintaining accuracy. This guide covers essential setup steps and best practices for optimal real-time performance.

Quick Start

Minimal setup for real-time streaming:
using NimbusSDK

# 1. Authenticate
NimbusSDK.install_core("your-api-key")

# 2. Load model
model = load_model(RxLDAModel, "motor_imagery_4class_v1")

# 3. Configure streaming metadata
metadata = BCIMetadata(
    sampling_rate = 250.0,
    paradigm = :motor_imagery,
    feature_type = :csp,
    n_features = 16,
    n_classes = 4,
    chunk_size = 250  # 1 second chunks at 250 Hz
)

# 4. Initialize streaming session
session = init_streaming(model, metadata)

# 5. Process chunks in real-time
for chunk in eeg_stream
    result = process_chunk(session, chunk)
    execute_command(result.prediction, result.confidence)
end

Chunk Size Selection

The chunk_size parameter determines latency and accuracy tradeoff:

Fast Response (0.5 second chunks)

metadata = BCIMetadata(
    sampling_rate = 250.0,
    chunk_size = 125,  # 0.5s at 250 Hz
    # ... other params
)
  • Latency: ~10-15ms per chunk
  • Update rate: Every 0.5 seconds
  • Use case: Fast paced games, quick reactions
  • Trade-off: Less data per prediction, potentially lower confidence
metadata = BCIMetadata(
    sampling_rate = 250.0,
    chunk_size = 250,  # 1s at 250 Hz
    # ... other params
)
  • Latency: ~15-20ms per chunk
  • Update rate: Every second
  • Use case: Most BCI applications
  • Trade-off: Good balance of speed and accuracy

Accurate (2 second chunks)

metadata = BCIMetadata(
    sampling_rate = 250.0,
    chunk_size = 500,  # 2s at 250 Hz
    # ... other params
)
  • Latency: ~20-25ms per chunk
  • Update rate: Every 2 seconds
  • Use case: High-stakes decisions, medical applications
  • Trade-off: Higher confidence but slower updates

Model Selection for Real-Time

RxLDA vs RxGMM

# RxLDA: Faster, recommended for real-time
rxlda_model = load_model(RxLDAModel, "motor_imagery_4class_v1")
# Typical latency: 10-20ms
# Use when: Speed is priority, classes are well-separated

# RxGMM: More flexible, slightly slower
rxgmm_model = load_model(RxGMMModel, "motor_imagery_4class_gmm_v1")
# Typical latency: 15-25ms
# Use when: Need flexibility for overlapping classes
For most real-time applications, Bayesian LDA (RxLDA) is recommended due to its faster inference while maintaining good accuracy.

Aggregation Strategies

Choose how to combine multiple chunks for final decisions:
# High-confidence chunks contribute more
final_result = finalize_trial(session; method=:weighted_vote)
  • Best for: Most applications
  • Advantage: Balances all chunks by confidence
  • Typical accuracy: Best overall performance

Majority Vote

# Each chunk has equal weight
final_result = finalize_trial(session; method=:majority_vote)
  • Best for: When all chunks have similar quality
  • Advantage: Simple, robust to outliers
  • Typical accuracy: Good with consistent signal

Latest Chunk Only

# Use only the most recent chunk
final_result = finalize_trial(session; method=:latest)
  • Best for: Ultra-responsive applications
  • Advantage: Fastest response to state changes
  • Typical accuracy: Lower, but most reactive

Performance Optimization

JIT Compilation Warmup

Julia’s JIT compiler makes the first inference slower. Warm up before real use:
using NimbusSDK

# Load model
model = load_model(RxLDAModel, "motor_imagery_4class_v1")
session = init_streaming(model, metadata)

# Warmup: Run a few dummy inferences
println("Warming up...")
dummy_chunk = randn(16, 250)  # Same shape as real data
for i in 1:10
    process_chunk(session, dummy_chunk)
end
println("✓ Ready for real-time processing")

# Now process real data (much faster)
for chunk in eeg_stream
    result = process_chunk(session, chunk)  # Fast after warmup
end

Memory Pre-allocation

Avoid allocations in the real-time loop:
using NimbusSDK

# Pre-allocate buffers
chunk_buffer = zeros(Float64, 16, 250)
results_buffer = Vector{StreamingResult}(undef, 100)

# Real-time loop with minimal allocation
for (i, raw_chunk) in enumerate(eeg_stream)
    # Reuse pre-allocated buffer
    chunk_buffer .= raw_chunk
    
    # Process (minimal allocation)
    result = process_chunk(session, chunk_buffer)
    results_buffer[i] = result
    
    # Handle result
    handle_bci_command(result)
end

Confidence Thresholding

Only act on high-confidence predictions:
# Configure threshold based on application
confidence_threshold = 0.75  # Adjust based on needs

for chunk in eeg_stream
    result = process_chunk(session, chunk)
    
    # Only execute if confidence exceeds threshold
    if result.confidence > confidence_threshold
        execute_command(result.prediction)
    else
        # Optional: provide feedback about low confidence
        display_uncertain_state()
    end
end

Real-Time Monitoring

Performance Tracking

Monitor system performance during operation:
using NimbusSDK
using Statistics

# Initialize tracker
latencies = Float64[]
confidences = Float64[]

for (i, chunk) in enumerate(eeg_stream)
    # Time the inference
    t_start = time()
    result = process_chunk(session, chunk)
    t_end = time()
    
    latency_ms = (t_end - t_start) * 1000
    push!(latencies, latency_ms)
    push!(confidences, result.confidence)
    
    # Report every 50 chunks
    if i % 50 == 0
        println("\n=== Performance Report ===")
        println("Mean latency: $(round(mean(latencies[end-49:end]), digits=1)) ms")
        println("Max latency: $(round(maximum(latencies[end-49:end]), digits=1)) ms")
        println("Mean confidence: $(round(mean(confidences[end-49:end]), digits=3))")
    end
end

Quality Monitoring

Track signal quality in real-time:
using NimbusSDK

# Track confidence trends
confidence_window = CircularBuffer{Float64}(20)

for chunk in eeg_stream
    result = process_chunk(session, chunk)
    
    # Add to rolling window
    push!(confidence_window, result.confidence)
    
    # Check for degrading quality
    if length(confidence_window) >= 20
        recent_conf = mean(confidence_window)
        
        if recent_conf < 0.6
            @warn "Signal quality degrading"
            println("Recent mean confidence: $(round(recent_conf, digits=2))")
            println("Recommendation: Check electrode contact")
        end
    end
end

Application-Specific Setups

Motor Imagery Game Control

using NimbusSDK

# Setup for gaming
model = load_model(RxLDAModel, "motor_imagery_4class_v1")

# Fast chunks for responsive gameplay
metadata = BCIMetadata(
    sampling_rate = 250.0,
    chunk_size = 125,  # 0.5s for quick response
    paradigm = :motor_imagery,
    feature_type = :csp,
    n_features = 16,
    n_classes = 4
)

session = init_streaming(model, metadata)

# Game control loop
for chunk in game_eeg_stream
    result = process_chunk(session, chunk)
    
    # Lower threshold for responsive gameplay
    if result.confidence > 0.65
        update_game_state(result.prediction)
    end
end

P300 Speller

using NimbusSDK

# P300 detection setup
model = load_model(RxLDAModel, "p300_binary_v1")

# Post-stimulus window
metadata = BCIMetadata(
    sampling_rate = 250.0,
    chunk_size = 200,  # 0.8s post-stimulus
    paradigm = :p300,
    feature_type = :erp,
    n_features = 12,
    n_classes = 2  # target vs non-target
)

session = init_streaming(model, metadata)

# Process stimulus presentations
for (stimulus, chunk) in p300_stream
    result = process_chunk(session, chunk)
    
    # Higher threshold for spelling accuracy
    if result.prediction == 1 && result.confidence > 0.85
        selected_character = stimulus
        println("Selected: $selected_character")
    end
end

Wheelchair Control (Safety-Critical)

using NimbusSDK

# Safety-critical setup
model = load_model(RxLDAModel, "motor_imagery_4class_v1")

# Longer chunks for higher confidence
metadata = BCIMetadata(
    sampling_rate = 250.0,
    chunk_size = 500,  # 2s for safety
    paradigm = :motor_imagery,
    feature_type = :csp,
    n_features = 16,
    n_classes = 4
)

session = init_streaming(model, metadata)

# Safety-critical control loop
for chunk in wheelchair_stream
    result = process_chunk(session, chunk)
    
    # Very high threshold for safety
    if result.confidence > 0.90
        execute_wheelchair_command(result.prediction)
    else
        # Require confirmation for low confidence
        request_manual_confirmation(result.prediction, result.confidence)
    end
end

Error Handling

Robust Real-Time Loop

using NimbusSDK

session = init_streaming(model, metadata)
error_count = 0
max_errors = 10

while is_bci_active()
    try
        chunk = get_next_chunk()
        
        # Process with timeout protection
        result = process_chunk(session, chunk)
        
        # Reset error counter on success
        error_count = 0
        
        # Handle result
        handle_bci_result(result)
        
    catch e
        error_count += 1
        @error "Chunk processing failed" exception=e error_count
        
        if error_count >= max_errors
            @error "Too many consecutive errors - stopping"
            break
        end
        
        # Brief pause before retry
        sleep(0.1)
    end
end

System Requirements

Minimum Requirements

  • CPU: Modern multi-core processor (Intel i5 or equivalent)
  • RAM: 4GB available
  • Julia: Version 1.9 or later
  • OS: Windows, macOS, or Linux
  • CPU: Intel i7 or equivalent (or better)
  • RAM: 8GB available
  • Julia: Version 1.10+
  • Storage: SSD for faster model loading
  • OS: Linux for best real-time performance

Latency Expectations

HardwareRxLDA LatencyRxGMM Latency
i5, 8GB RAM15-25ms20-30ms
i7, 16GB RAM10-20ms15-25ms
High-end workstation10-15ms12-20ms
These are per-chunk latencies. Total system latency includes signal acquisition and any application-specific processing.

Troubleshooting

High Latency Issues

# Profile to find bottlenecks
latencies = []

for chunk in test_stream[1:100]
    t_start = time()
    result = process_chunk(session, chunk)
    latency = (time() - t_start) * 1000
    
    push!(latencies, latency)
end

println("Statistics:")
println("  Mean: $(round(mean(latencies), digits=1)) ms")
println("  Median: $(round(median(latencies), digits=1)) ms")
println("  95th percentile: $(round(quantile(latencies, 0.95), digits=1)) ms")
println("  Max: $(round(maximum(latencies), digits=1)) ms")

# First chunk is usually slower (JIT compilation)
println("\nExcluding first chunk:")
println("  Mean: $(round(mean(latencies[2:end]), digits=1)) ms")
Common Solutions:
  1. Run warmup inferences before real use
  2. Use RxLDA instead of RxGMM for speed
  3. Reduce chunk_size if acceptable
  4. Check system resource usage
  5. Close other applications

Low Confidence Issues

# Diagnose signal quality
using NimbusSDK

# Collect diagnostic trial
diagnostic_data = collect_diagnostic_trial()

# Check preprocessing quality
report = diagnose_preprocessing(diagnostic_data)

println("Preprocessing quality: $(round(report.quality_score * 100, digits=1))%")

if !isempty(report.warnings)
    println("\nWarnings:")
    for warning in report.warnings
        println("  • $warning")
    end
end

if !isempty(report.recommendations)
    println("\nRecommendations:")
    for rec in report.recommendations
        println("  • $rec")
    end
end

Next Steps


Tip: Start with standard 1-second chunks and adjust based on your application’s speed/accuracy requirements.