Neuroplayer Processor Library

© 2006-2025 Kevan Hashemi, Open Source Instruments Inc.

match-prompts-only chapter-chunk

Contents

Introduction
Generic Characteristics
Baseline Power Calibration
Event Classification
Event Handler
Export Signal
Export Spectrum
Consecutive Power Bands
Arbitrary Power Bands
Total Operating Time
Electromyogram and Electrocardiogram
LabChart Exporter
Spike Counter
Event Detection and Identification

Introduction

[15-JUL-25] Here we present a library of Neuroplayer interval processor scripts. We deploy all processors scripts in the same way: we save them to disk as a text file. Copy and paste them from this page into a text editor and save to disk. We recommend the file extension "tcl". The LWDAQ program provides its own black-and-white text editor in the Tools menu. Use the New Script menu command to open a fresh script window. Cut and paste the processor script into the new window and use Save As to save the script to a new file on disk. Do not include spaces in your file name. Do not save the file in a directory that contains spaces in its name or path. The Neuroplayer is not designed to tolerate spaces in directory or file names. Once you have the script saved to disk, select it using the Pick button in the Processing line of the Neuroplayer window. Enable by checking the Enable box. For each playback interval, the Neuroplayer will execute the script once for each selected channel. Some processors add elements to the processing result string. If so, the result string it creates will be printed in the Neuroplayer text window, prefixed by the archive name and interval start time. Some processors do not modify the results string, but instead produce output files of their own design. How do I deploy an interval processor? How can I save an interval processor to disk using LWDAQ?

Generic Characteristics

[16-JUL-25] This processor illustrates the function of a Neuroplayer interval processor by calculating several characteristics of the interval and appending them as numbers to processing result string. It is a "standard processor" in that the characteristics line it produces conforms to the standard characteristics format. The first number we add is the current channel number. The Neuroplayer will be processing the selected signal channels one after another. The one that it is processing right now is the "current channel number". This number will be the only integer-format number the processor adds to the characteristics line. After that, we calculate the signal reception, which is the fraction of transmitted samples that are being received by the telemetry receiver. The Neuroplayer provides a "loss" fraction, which we subtract from 1.0 to obtain "reception". We add reception to the characteristics string with two digits after the decimal point. We must be sure not to write it as an integer with no digits after the decimal point. We assume the device providing the telemetry signal is an AC-coupled SCT or HMT, and with this assumption, we are able to convert the average value of the signal into a measurement of the device's current battery voltage using the formula presented in the Battery Voltage chapter of the Telemetry Manual. We calculate the amplitude of the signal after bandpass filtering to 2-80 Hz band, and express in microvolts assuming that the input dynamic range of our SCT or HMT is 30 mV. We calculate the range of the signal, which is the maximum minus the minimum, in microvolts as well. We finish with the normalized coastline of the signal, also in microvolts, which is the average absolute change in signal value between one sample and the next. Give me an example of an interval processor for use with the Neuroplayer. How can I write my own interval processor script? Show me an interval processor that calculates reception, battery voltage, standard deviation, range, and coastline of the signal.

# Specify the dynamic range of the SCT or HMT in microvolts.
set dynamic_range 30000

# Specify the frequency range in which we will calculate signal power.
set flo 2.0
set fhi 80.0

# Calculate reception efficiency. The Neuroplayer has already calculated loss
# for us, whic is the fraction of samples that have not been received. To obtain
# loss, the Neuroplayer uses its best guess at the nominal sample rate of the
# transmitter, or a value specified in the channel select by the user, and the
# number of messages in the signal before reconstruction. If the raw signal
# contains a large number of bad messages, the total number of messages in this
# interval could, in principle, be greater than the nominal sample rate, giving
# rise to a negative value for loss. But in such cases, the Neuroplayer sets the
# loss to zero. We format the reception to give one digit after the decimal
# point. The standard characteristics line format rules forbid us from writing
# any characteristic that looks like an integer, other than the current channel
# number.
set reception [format %.1f [expr 100.0 - $info(loss)]]

# Calculate some generic characteristics of the interval. We use a lwdaq library
# routine called "ave_stdev". We pass it the "values" string, which contains all
# the sixteen-bit sample values without their timestamps. The "signals" string
# in the same information array contains the timestamps and sample values
# alternating in a space-delimited list, so we don't use that. We just want the
# values. We get back the average, standard deviation, maximum, minimum, and
# mean absolute deviation of the signal. The mean absolute deviation is the
# coastline divided by the number of samples.
lassign [lwdaq ave_stdev $info(values)] ave stdev max min mad

# Use the average value of the signal to measure the battery voltage. We use the
# known 1.8-V ground voltage of the SCT and HMT amplifiers to convert the
# average, which we assume is the amplifier's zero-voltage output, to battery
# voltage.
set battery [format %.2f [expr 65536*1.0/$ave*1.8]]

# Calculate the power in our chosen frequency band. The two zeros in the call to
# the band-power routine tell the routine not to display the bandpass filtered
# signal and not to replace the current signal "values" string with the filtered
# signal values. Convert this power into microvolts standard deviation.
set power [Neuroplayer_band_power $flo $fhi 0 0]
set amplitude [format %.1f [expr sqrt($power)*$dynamic_range/65535]]

# Calculate the range in microvolts.
set range [format %.1f [expr $dynamic_range*($max-$min)/65535]]

# Convert the mean absolute deviation into microvolts. The result is a normalized
# measure of coastline: it has been divided by the number of samples.
set coastline [format %.1f [expr $dynamic_range*$mad/65535]]

# Add channel number and characteristics to the processing result string. These
# values will be added for each selected telemetry channel, as this processor is
# called for each of them. Note that we have a space at the end of our string so
# that the characteristics of the next channel may be added by this same
# processor.
append result "$info(channel_num) $reception $battery $amplitude $range $coastline "

Baseline Power Calibration

[18-JUL-25] This standard interval processor performs baseline calibration. We can combine this processor with an event classification processor so as to support power-based event classification, as we might when using EMG to assist with sleep scoring. See the Baseline Calibration section of our Event Detection page for a description of the strengths and weaknesses of baseline calibration for event detection. This baseline power calibration algorithm is not effective when applied to EEG recorded from epileptic animals, but it is very effective at calibrating the baseline power of EMG signals for sleep scoring. This processor implements a minimum-seeking calibration of signal power, in which we assume that the minimum interval power is a useful value for baseline power. The processor uses the amplitude of the signal after filtering to a frequency range called the "event band" using the discrete fourier transform of the interval. The calibration power is in units of square analog-to-digital converter counts, or sq-cnt. We convert to microvolts squared using the sensitivity of the recording device. For most AC transmitters, the sensitivity is 0.45 μV/cnt, and for most DC transmitters it is 1.8 μV/cnt. We take the fourier transform after applying the glitch filter, but we don't set the glitch filter with this processor. Be sure to set the glitch threshold to something like 500 before performing calibration. Use this processor with "Reset Baselines on Playback Start" and "Write Baselines to Metadata on Playback Finish" in the Neuroplayer's Calibration panel. It will write the minimun interval power measure for the archive into the archive's metadata. If you set the "Name for All Metadata Reads and Writes" to "BCP3" you will later be able to select its calibration from any others you may have written to the metadata. The script is also available as BCP3.tcl. How do I determine the baseline power of my EEG, EMG, or EKG signal? Show me an interval processor that calibrates the baseline power of my EEG signal. Can the Neuroplayer determine the baseline power of my EEG signal? How can I use the Neuroplayer to score EMG for sleeping and waking?

# Because we are going to use the fourier transform, we must make sure we
# configure the Neuroplayer to calculate the transform even when the
# amplitude versus frequency plot is disabled.
set config(af_calculate) 1

# Define the limits of frequency band in which we will determine the 
# upon which we will determine  baseline power.
set event_lo 2
set event_hi 80.0

# Maximum signal loss we can tolerate and still consider the interval
# an event.
set max_loss 20.0

# Calculate the standard deviation of the signal, as well as the average,
# maximum, minimum, and mean absolute deviation, just because we can. We
# are going to print out the standard deviation of the signal so we can
# compare it to the standard deviation of the signal filtered by the
# discrete fourier transform. 
scan [lwdaq ave_stdev $info(values)] %f%f%f%f%f ave stdev max min mad

# Obtain the event-band power. We take all the components of the discrete
# fourier transform that lie in our event band, add the squares of their
# amplitudes together, divide by two, and take the square root. This gives
# us the amplitude of the signal we would obtain if we took the inverse
# transform of the event-band components alone. If we want to see what
# the event-band filtered signal looks like, we set the first flag after
# the event_hi parameter in the line below to 1. The filtered signal will
# appear in a different color in the value versus time plot. The calculation
# of the inverse transform takes time, so turn off the signal display when
# processing bulk datat: set the flag after event_hi to 0. 
set event_pwr [expr sqrt([Neuroplayer_band_power $event_lo $event_hi 0 0]/2.0)]

# Look for a minimum event power value. If we find one, and it's not 
# a redundant interval with zero power, and furthermore reception is
# adequate, we set the baseline power for this channel equal to the 
# new minimum.
upvar #0 Neuroplayer_info(bp_$info(channel_num)) baseline_pwr
if {($event_pwr < $baseline_pwr) && ($event_pwr > 0.0) && ($info(loss) < $max_loss)} {
	set baseline_pwr [format %.1f $event_pwr]
}

# Our characteristics consist of the baseline power and the interval 
# standard deviation. We have the channel number, the current baseline
# power, the event-band power of this interval, and the standard deviation
# of this interval. If we set the event band range to 0.001 (a value just 
# above zero to eliminate the zero-frequency component) and 1000.0 (or any
# value higher than the half the sample frequency) we expect to get an
# event band power slightly lower than the standard deviation. The event
# band power will not be identical to the standard deviation, because we
# apply a window function to the signal before taking the fourier transform,
# and this window function removes some of the variation in the signal
# by bringing the signal at the start and finish of the interval to the 
# same value, which is the average value of the signal before applying the
# window function.
append result "$info(channel_num)\
	$baseline_pwr\
	[format %.1f $event_pwr]\
	[format %.1f $stdev] "

Event Classification

[15-JUL-25] This is an event classification processor. We apply a processor like this to our recordings in order to generate characteristics for the Event Classifier. When we are building a library, the processor provides metrics for classification as we play a recording. When we are classifying a thousands of hours of recordings, we apply a classification processor to the recording before-hand so as to generate characteristics files that contain the metrics of each recorded signal in each playback interval. This particular event classification processor is Generation 20, Version 4, Revision 1, or ECP20V4R1, but with the event handler portion of the script removed. It calculates six metrics: power, coastline, intermittency, coherence, asymmetry, and spikiness. It has one set of metric adjustments optimised for sample rate 256 SPS and another set of adjustments optimised for 512 SPS. Show me an event classification processor script. How does an event classification processor work? How does the Neuroplayer calculate the metrics used by the Event Classifier? Provide me with an event classification processor that calculates power, coastline, intermittency, coherence, asymmetry and spikiness.

# Define event type names and color codes for the Event Classifier.
set config(classifier_types) "Ictal red \
  Hiss blue \
  Depression cyan \
  Spike orange \
  Grooming darkgreen \
  Artifact lightgreen \
  Baseline gray"
  
# Define metric names for the Event Classifier.
set config(classifier_metrics) "power\
  coastline\
  intermittency\
  coherence\
  asymmetry\
  spikiness"

# Configure the processor diagnostic displays.
set show_intermittency 0
set show_coherence 0
set show_spikiness 0
set metric_diagnostics 0

# The coherence threshold produces the minor peaks and valleys, which we use to
# measure the coherence of the interval.
set coherence_threshold 0.0

# The spikiness_extent is the maximum width of spike we want to find. The
# units are samples, so specify an integer.
set spikiness_extent 2

# We use the baseline power value for this channel as a scaling factor
# for our power metric calculation. We do not set the baseline power
# in this processor, so we must make sure we set it in the Neuroplayer's
# calibration window.
upvar #0 Neuroplayer_info(bp_$info(channel_num)) baseline_pwr

# We want to make sure we don't calculate the fourier transform unless
# the user turns on the spectrum display in the Neuroplayer panel.
set config(af_calculate) 0

# We begin this channel's section of the characteristics line with the 
# channel number.
lappend result $info(channel_num)

# If we have too much signal loss, we ignore this interval.
set max_loss 20.0

# The following code, when enabled, plots the coastline progress, derivative
# of coastline, and a list of derivative values sorted into decreasing order
# so as to show on the value versus time plot the fraction of the coastline 
# that is generated by the 10% points of greatest derivative. This fraction 
# is the area under the first 10% of the sorted derivative plot, divided by 
# the total area under the sorted derivative plot.
if {$show_intermittency} {
  set cp [lwdaq coastline_x_progress $info(values)]
  lwdaq_graph $cp $info(vt_image) -y_only 1 -color 9
  set coastline [lindex $cp end]
  set dcp ""
  set max 0.0
  foreach c $cp {
    if {$dcp == ""} {set cc $c}
    append dcp "[expr abs($c - $cc)] "
    set cc $c
  }
  set dcp [lsort -decreasing -real $dcp]
  lwdaq_graph $dcp $info(vt_image) -y_only 1 -y_min 0 -color 5
}

# Coherence peak and valley display. When enabled, peaks are marked with a
# short downward mark at the top of the display, valleys by a short upward
# mark at the bottom of the display.
if {$show_coherence} {
  scan [lwdaq ave_stdev $info(values)] %f%f%f%f%f ave stdev max min mad
  set range [expr $max-$min]
  set max [lindex $info(values) 0]
  set min $max
  set vv $max
  set state "0 "
  set i 0
  set min_i 0
  set max_i 0
  set state "none"
  set maxima "0 1 "
  set minima "0 -1 "
  foreach v $info(values) {
    if {$v>$max} {
      set max $v
      set max_i $i
    }
    if {$v<$min} {
      set min $v
      set min_i $i
    }
    if {(abs($v-$min)>$coherence_threshold*$range) && ($state!="min")} {
      append minima "$min_i -1.0 $min_i 0 $min_i -1.0 "
      set max $v
      set max_i $i
      set min $v
      set min_i $i
      set state "min"
    } elseif {(abs($v-$max)>$coherence_threshold*$range) && ($state!="max")} {
      append maxima "$max_i +1.0 $max_i 0 $max_i +1.0 "
      set max $v
      set max_i $i
      set min $v
      set min_i $i
      set state "max"
    }
    incr i
    set vv $v
  }
  if {$state=="max"} {
    append minima "$min_i -1.0 $min_i 0 $min_i -1.0 "
  } 
  if {$state=="min"} {
    append maxima "$max_i +1.0 $max_i 0 $max_i +1.0 "
  }
  append minima "$i -1.0"
  append maxima "$i +1.0"
  lwdaq_graph $maxima $info(vt_image) -color 9 \
    -y_max 1.0 -y_min -20 \
    -x_min 0.0 -x_max [expr [llength $info(values)]-1]
  lwdaq_graph $minima $info(vt_image) -color 9 \
    -y_max 20 -y_min -1.0 \
    -x_min 0.0 -x_max [expr [llength $info(values)]-1]
}

# Spikiness signal width disply. When enabled, the width of the signal in
# a region before and after the current time is represented on the
# screen with a top and bottom line.
if {$show_spikiness} {
  set ext $spikiness_extent
  scan [lwdaq ave_stdev $info(values)] %f%f%f%f%f ave stdev max min mad
  set heights ""
  set max_height 0
  for {set i 0} {$i < [llength $info(values)]} {set i [expr $i + $ext]} {
    set top [lindex $info(values) $i]
    set bottom $top
    for {set j [expr $i-$ext]} {$j <= [expr $i+$ext]} {incr j} {
      if {($j >= 0) && ($j < [llength $info(values)])} {
        set v [lindex $info(values) $j]
        if {$v > $top} {set top $v}
        if {$v < $bottom} {set bottom $v}
      }
    }
    if {$i > 0} {
      lwdaq_graph "[expr $i - $ext] $previous_bottom\
          $i $bottom" $info(vt_image) -color 9 \
        -y_max $max -y_min $min \
        -x_min 0.0 -x_max [expr [llength $info(values)]-1]
      lwdaq_graph "[expr $i - $ext] $previous_top\
          $i $top" $info(vt_image) -color 9 \
        -y_max $max -y_min $min \
        -x_min 0.0 -x_max [expr [llength $info(values)]-1]
    }
    set previous_top $top
    set previous_bottom $bottom
    set height [expr $top - $bottom]
    lappend heights $height
    if {$height > $max_height} {set max_height $height}
  }
  lwdaq_graph $heights $info(vt_image) -y_only 1 -color 5 \
    -y_max $max_height -y_min 0 \
    -x_min 0.0 -x_max [expr [llength $heights]-1]
  set heights [lsort -decreasing -integer $heights]
  set median [lindex $heights [expr round([llength $heights]/2.0)]]
  set min_height [lindex $heights end]
  set spikiness [format %.2f [expr 1.0*$max_height/$median]]
  Neuroplayer_print "$spikiness $ext $median $max_height\
    $min_height [llength $heights]" darkgreen
}

# We add a single-letter code that indicates the type of the interval, so
# far as this processor can deduce it. We have L for loss, U for unclassified
# and N for no event.
if {($info(loss)<$max_loss)} {
  lappend result "U"
} else {
  lappend result "L"
}

# We calculate metrics using a library routine. We select which routine with 
# a letter code. After the letter we pass numeric parameters that direct the
# metric calculation. We check the metric result for errors, and if we find
# them, we print the error message to the Neuroplayer text window, and set
# the metric string to all zeros. So that we can see diagnostic printing from
# the metric calculator, we direct the lwdaq library to print in the Neuroplayer
# text window with the lwdaq_config command.
if {($info(loss)<$max_loss)} {
  lwdaq_config -text_name $info(text)
  set metrics [lwdaq_metrics $info(values) "E \
    $coherence_threshold \
    $spikiness_extent \
    $metric_diagnostics"]
  if {[LWDAQ_is_error_result $metrics]} {
    Neuroplayer_print $metrics
    set metrics "0 0 0 0 0 0"
  }
} else {
  set metrics "0 0 0 0 0 0"
}

# We write our baseline power to the characteristics line. This is the signal
# amplitude for which the power metric should be 0.5. Note that different metric 
# calculations may use different measures of amplitude, such as standard deviation
# or mean absolute deviation. Whatever measure the metric calculator uses, when
# this measure is equal to our power center value, the power metric will be 0.5.
lappend result [format %.1f $baseline_pwr]

# Scan the metrics string for the metric values.
scan $metrics %f%f%f%f%f%f \
  power coastline intermittency coherence asymmetry spikiness

# We calculate the metric values from the various interval measures. We vary
# these calculations slightly according to the signal sample rate. We have
# optimized them for 256 SPS and 512 SPS. The metrics are bounded zero to one.
# We use Neuroclassifier_sigmoidal to tranform the measures, which are all
# greater than zero, into bounded metric values. We pass to the sigmoidal
# routine the value of the measure, a centering value, which is the value of the
# measure for which the metric will be one half, and an exponent, which
# determines how sharp the sigmoidal function is at the center. The larger the
# exponent, smaller the range of measure values about the center that will
# appear near one half.
if {$info(sps_$info(channel_num)) == "256"} {
  lappend result [Neuroclassifier_sigmoidal $power $baseline_pwr 1.0]
  lappend result [Neuroclassifier_sigmoidal $coastline 0.05 2.0]
  lappend result [Neuroclassifier_sigmoidal $intermittency 0.4 5.0]
  lappend result [Neuroclassifier_sigmoidal $coherence 0.1 2.0]
  lappend result [Neuroclassifier_sigmoidal $asymmetry 0.5 1.0]
  lappend result [Neuroclassifier_sigmoidal $spikiness 6 1.5]
} else {
  lappend result [Neuroclassifier_sigmoidal $power $baseline_pwr 1.0]
  lappend result [Neuroclassifier_sigmoidal $coastline 0.05 2.0]
  lappend result [Neuroclassifier_sigmoidal $intermittency 0.4 4.0]
  lappend result [Neuroclassifier_sigmoidal $coherence 0.07 2.0]
  lappend result [Neuroclassifier_sigmoidal $asymmetry 0.5 1.0]
  lappend result [Neuroclassifier_sigmoidal $spikiness 12 1.5]
}

Event Handler

[15-JUL-25] This an event handler script is for use with the Neuroplayer's Event Classifier. The handler is compatible with generation-twenty event classification processors, such as ECP20V4R1. The event handler detects and responds to epileptic seizures by asserting one of the logic outputs on an Octal Data Receiver. We include this code at the end of our classification processor. The code defines the Event Classifier handler script that will be called by the Event Classifier after the Neuroplayer has processed an interval, provided that we have enabled event handling. The first time the handler is called, it opens its own configuration and monitoring panel. This particular event handler sends a command to an Octal Data Receiver to turn on a lamp, and another command turn the same lamp off. We use command 0x0104 to set output X1 to HI and 0x0004 to set X1 to LO. When we see "trig_len" consecutive Ictal or Spike intervals, we turn on the lamp for "on_len" intervals, then turn if off for "off_len" intervals, and so on, until we turn the lamp off and more than "en_len" intervals have passed since the initial trigger. If we set "en_prob" to a value less than one, it represents the probability that the trigger will cause a stimulus with light. The stimulus will be either illuminated or not, at random. But it will last the same length of time. We can list as many signal channels as we like in the "id_list" string, separated by spaces. We apply the same configuration paramters to all channels, but each channel gets a separate line of counters in the event handler window. Stimuli and stimuli disabled by the enable probability are recorded to a text file we name in an entry box in the handler script. This text file has the format of a Neurorachiver event list, so you can select it in the Neuroplayer and navigate between the events with the event navigation buttons. The "enable" button in the panel enables the transmission of commands to the Octal Data Receiver. Uncheck this box when testing the handler, or when recording events to the text file from pre-recorded data so that LWDAQ does not attempt to make contact with the LWDAQ hardware. The "verbose" button enables printing more information to the handler text window. Show me an event handler script for the Neuroplayer's Event Classifier. How does an event handler script detect and respond to classified events? Give me an event handler script for the Event Classifier that detects the commencement of an epileptic seizure and initiates a stimulus.

# If the handler window does not exist, but the Event Classifier window
# does exist, and the event handler is enabled, we will define the handler 
# procedure for the Event Classifier. 
if {[winfo exists $info(classifier_window)] \
  && ![winfo exists $info(classifier_window)\.handler] \
  && $config(enable_handler)} {

#
# We set the Neuroplayer's handler_script, which has one part that creates the
# handler window when the window does not exist, and another part that detects a
# seizure and acts upon the detection.
#
set info(handler_script) {
  upvar #0 event_handler_info h
  upvar #0 LWDAQ_config_Receiver rconfig
  upvar #0 Neuroplayer_config nconfig
  
  set w $info(classifier_window)\.handler
  if {![winfo exists $w]} {
    # We may need to print some warnings in the text window later.
    set warnings ""
    
    # Here are the global variables the event handler uses.
    catch {unset h}
    set h(ip) $rconfig(daq_ip_addr)
    set h(socket) $rconfig(daq_driver_socket)
    set h(on) "0004"
    set h(off) "0104"
    set h(verbose) "0"
    set h(enable) "0"
    set h(outfile) "~/Desktop/Handler_Activity.txt"
    set h(trig_len) "5"
    set h(on_len) "30"
    set h(off_len) "31"
    set h(en_len) "60"
    set h(en_prob) "1.0"
    set h(id_list) $nconfig(channel_selector)
    foreach id $h(id_list) {
      set h(state$id) "Rest"
      set h(on_cntr$id) "0"
      set h(en_cntr$id) "0"
    }

    # Open a window for handler.
    toplevel $w
    wm title $w "Event Handler Control Panel"
    
    # Create configuration entries
    set f [frame $w.f1]
    pack $f -side top -fill x
    foreach a {Trig_Len On_Len Off_Len En_Len En_Prob On Off} {
      set b [string tolower $a]
      label $f.l$b -text "$a\:" 
      entry $f.e$b -textvariable event_handler_info($b) -width 4
      pack $f.l$b $f.e$b -side left -expand yes 
    }
    
    # Create more configuration entries.
    foreach id $h(id_list) {
      if {![string is integer -strict $id]} {
        append warnings "WARNING: Bad channel number \"$id\" found in Neuroplayer's\
          channel select string.\n"
        append warnings "SUGGESTION: List the channels you want to analyze in the\
          select string and reset this handler.\n"
        continue
      }
      set f [frame $w.fid$id]
      pack $f -side top -fill x
      label $f.id$id -text "$id\:" -width 3
      label $f.state$id -textvariable event_handler_info(state$id) -fg blue -width 12
      pack $f.id$id  $f.state$id -side left -expand yes
      foreach a {On_Cntr En_Cntr} {
        set b [string tolower $a]
        label $f.l$b$id -text "$a\:"
        entry $f.e$b$id -textvariable event_handler_info($b$id) -width 10
        pack $f.l$b$id $f.e$b$id -side left -expand yes
      }
    }

    # Create output file browser.
    set f [frame $w.f3]
    pack $f -side top -fill x
    label $f.lo -text "Outfile:" 
    entry $f.eo -textvariable  event_handler_info(outfile) -width 60
    pack $f.lo $f.eo -side left -expand yes
    label $f.ip -text "IP:" 
    entry $f.eip -textvariable event_handler_info(ip) -width 12
    pack $f.ip $f.eip -side left -expand yes 
    label $f.socket -text "Socket:" 
    entry $f.esocket -textvariable event_handler_info(socket) -width 2
    pack $f.socket $f.esocket -side left -expand yes 
    checkbutton $f.e -text "Enable" -variable event_handler_info(enable) 
    pack $f.e -side left -expand yes
    checkbutton $f.v -text "Verbose" -variable event_handler_info(verbose) 
    pack $f.v -side left -expand yes
    
    # Create text widget.
    set h(t) [LWDAQ_text_widget $w 70 10 1 1]
    LWDAQ_print $h(t) "NOTE: Close this window to reset the handler." purple
    LWDAQ_print $h(t) $warnings
  }
  
  if {$h(verbose)} {
    LWDAQ_print $h(t) "Handling channel $id in $info(play_file_tail) at $pt s, $type." 
  }
  if {[lsearch $h(id_list) $id] >= 0} {
    if {$h(state$id) == "Rest"} {
      if {($type == "Ictal") || ($type == "Spike")} {
        incr h(on_cntr$id)
        if {$h(on_cntr$id) >= $h(trig_len)} {
          set h(on_cntr$id) 0
          set h(en_cntr$id) 0
          if {rand() < $h(en_prob)} {
            if {$h(verbose)} {
              LWDAQ_print $h(t) "Attempting to open a socket to $h(ip)\..."
              LWDAQ_update
            }
            if {$h(enable)} {
              set sock [LWDAQ_socket_open $h(ip)]    
              LWDAQ_set_driver_mux $sock $h(socket)    
              LWDAQ_transmit_command_hex $sock $h(on)
              LWDAQ_wait_for_driver $sock
              LWDAQ_socket_close $sock
            }
            set h(state$id) "Enable_On"
            LWDAQ_print $h(t) "$info(play_file_tail) $pt $id $h(state$id)" brown
            LWDAQ_print $h(outfile) "$info(play_file_tail) $pt $id $h(state$id)" brown
          } {
            set h(state$id) "Disable"
            LWDAQ_print $h(t) "$info(play_file_tail) $pt $id $h(state$id)" brown
            LWDAQ_print $h(outfile) "$info(play_file_tail) $pt $id $h(state$id)" brown
          }
        }
      } {
        set h(on_cntr$id) 0
        set h(en_cntr$id) 0
      }
    } elseif {$h(state$id) == "Disable"} {
      incr h(en_cntr$id)
      if {$h(en_cntr$id) >= $h(en_len)} {
        set h(state$id) "Rest"
        set h(on_cntr$id) 0
        set h(en_cntr$id) 0        
      }
    } elseif {$h(state$id) == "Enable_On"} {
      incr h(en_cntr$id)
      incr h(on_cntr$id) 
      if {$h(on_cntr$id) >= $h(on_len)} {
        if {$h(verbose)} {
          LWDAQ_print $h(t) "Attempting to open a socket to $h(ip)\..."
          LWDAQ_update
        }
        if {$h(enable)} {
          set sock [LWDAQ_socket_open $h(ip)]    
          LWDAQ_set_driver_mux $sock $h(socket)    
          LWDAQ_transmit_command_hex $sock $h(off)
          LWDAQ_wait_for_driver $sock
          LWDAQ_socket_close $sock
        }
        set h(on_cntr$id) 0
        set h(state$id) "Enable_Off"
        LWDAQ_print $h(t) "$info(play_file_tail) $pt $id $h(state$id)" brown
        LWDAQ_print $h(outfile) "$info(play_file_tail) $pt $id $h(state$id)" brown
      }
    } elseif {$h(state$id) == "Enable_Off"} {
      incr h(en_cntr$id)
      if {$h(en_cntr$id) >= $h(en_len)} {
        set h(state$id) "Rest"
        set h(on_cntr$id) 0
        set h(en_cntr$id) 0        
      } else {
        incr h(on_cntr$id) 
        if {$h(on_cntr$id) >= $h(off_len)} {
          if {$h(verbose)} {
            LWDAQ_print $h(t) "Attempting to open a socket to $h(ip)\..."
            LWDAQ_update
          }
          if {$h(enable)} {
            set sock [LWDAQ_socket_open $h(ip)]    
            LWDAQ_set_driver_mux $sock $h(socket)    
            LWDAQ_transmit_command_hex $sock $h(on)
            LWDAQ_wait_for_driver $sock
            LWDAQ_socket_close $sock
          }
          set h(on_cntr$id) 0
          set h(state$id) "Enable_On"
          LWDAQ_print $h(t) "$info(play_file_tail) $pt $id $h(state$id)" brown
          LWDAQ_print $h(outfile) "$info(play_file_tail) $pt $id $h(state$id)" brown
        }
      }
    } else {
      LWDAQ_print $h(t) "ERROR: Unrecognised state \"$h(state$id)\"."
      set h(state$id) "Rest"
      set h(on_cntr$id) 0
      set h(en_cntr$id) 0
    }
  }
}
# End of definition of event handler script.
}

Export Signal

[15-JUL-25] This is a non-standard interval processor. It provides a low-pass filtered version of the signal in a text file for us to plot. It also, as a side-effect of the filtering, plots the filtered signal in the Neuroplayer window, and it replaces the original signal with the filtered signal in the info(values) variable. Once it has filtered the signal, it writes every filtered signal value to a separate line in an output file. The output file is named after the current channel number, so we will get a separate file for each selected channel. If we want to high-pass or low-pass filter the signal, or even band-pass filter the signal, all we need do is adjust the two frequency parameters in our call to the band-power routine. For example, to band-pass 10-40 Hz, we would replace the "0.0 80.0 1 1" with "10.0 40.0 1 1". If we don't want to apply any filtering before exporting, we can comment out the band-power command line. An interval processor script that band-pass filters the signal and saves to disk. How can I filter my telemetry signal and save the filtered sample values to disk? Give me an example of a non-standard interval processor.

set fn [file join [file dirname $config(play_file)] "E$info(channel_num)\.txt"]
Neuroplayer_band_power 0.0 80.0 1 1
set export_string ""
foreach value $info(values) {
  append export_string "[format %.1f $value]\n"
}
set f [open $fn a]
puts -nonewline $f $export_string
close $f

Export Spectrum

[14-JUL-25] This is a non-standard interval processor. It exports the amplitude components of a signal's discrete Fourier transform to disk. It is an example of a processor that produces no characteristics file. It does not use the result string, so it produces no characteristics line. Instead, it creates its own output file. The Fourier transform of an interval contains both amplitude and phase components, but this routine drops the phase components and writes only the amplitude components to disk. The name of the file it writes to consists of the letter "S" followed by the telemetry channel number and a "txt" extension. Instead of appending each new spectrum to its output file, each run through this script re-writes the spectrum file. Show me an interval processor that exports the spectrum of a signal. How can I obtain the Fourier transform of a telemetry signal? Give me a processor script that writes the amplitude spectrum of a signal to disk in text format.

set fn [file join [file dirname $config(processor_file)] "S$info(channel_num)\.txt"]
set export_string ""
set frequency 0
foreach {amplitude phase} $info(spectrum) {
  append export_string "$frequency $amplitude\n"
  set frequency [expr $frequency + $info(f_step)]
}
set f [open $fn w]
puts -nonewline $f $export_string
close $f

Consecutive Power Bands

[14-JUL-25] This is a standard interval processor. It calculates the power of a signal in each of a sequence of consecutive, adjacent frequency bands. It records the power in each band to the processing results string. The processor uses the Neuroplayer_band_power routine to obtain the power in each of one or more bands as specified by a list of boudaries between the bands. The processor assumes the first band starts just above 0.0 Hz. The DC components of the spectrum is excluded from the first band. The first band ends with the first frequency value we specify. We specify the remaining bands with the frequency of the boundaries between the bands. The final frequency is the top end of the final band. This is an example of a processor that will produce the characteristics files used by a Power Band Average interval analysis program such as PBAV4.tcl. We apply this processor to our recording to produce characteristics files with one-secoond resolution and later run the PBA program to take combine the one-second power measurements and calculate the average band power for longer intervals. Show me a processor script that calculates the power in consecutive frequency bands. How can I get the Neuroplayer to calculate the power in consecutive frequency bands? Show me how to use the Neuroplayer_band_power routine in a processor script.

append result "$info(channel_num) "
set f_lo 0
foreach f_hi {1 20 40 160} {
  set power [Neuroplayer_band_power [expr $f_lo + 0.01] $f_hi 0]
  append result "[format %.2f [expr 0.001 * $power]] "
  set f_lo $f_hi
}

Arbitrary Power Bands

[14-JUL-25] This is a standard interval processor. It is similar to the Consecutive Power Bands processor, but allows us to define arbitrary bands through the specification of both the bottom and top frequency of each band. Like the Consecutive Power Bands processor, this Arbitrary Power Bands processor can be used to produce the characteristics files that are needed by the Power Band Average (PBAV4.tcl) analysis program. First we apply this processor to our recording, then we apply the PBA program to calculate average power over longer intervals of time. Show me a processor script that calculates the power in a sequence of arbitrary frequency bands. Show me how to use the Neuroplayer_band_power routine in a processor script. Show me how to produce the spectrograph of a telemetry signal with an interval processor script.

append result "$info(channel_num) [format %.2f [expr 100.0 - $info(loss)]] "
foreach {lo hi} {1 4 5 8 9 12 13 40 100 160} {
  set bp [expr 0.001 * [Neuroplayer_band_power $lo $hi 0]]
  append result "[format %.2f $bp] "
}

Total Operating Time

[14-JUL-25] Suppose we want to know how many hours a particular transmitter spent operating during the course of an experiment in which we turned it on and off many times. This standard interval processor will accumulate the time the transmitter was running. In order to perform the accumulation, the processor creates its own global array variable, in which it stores the accumulated operating time of each telemetry channel. Can the Neuroplayer tell me how many hours a transmitter has been running for? Show me a processor script that adds up all the time a transmitter has been turned on. How can I tell how long my transmitter lasted before its battery ran out?

set min_reception 20
upvar #0 Neuroarchiver_ol($info(channel_num)) ol
if {![info exists ol]} {set ol 0}
if {$info(loss) < 100-$min_reception} {
  set ol [expr $ol + 1.0*$config(play_interval)/3600]
}
append result "$info(channel_num) [format %.1f $ol] "

Electromyogram and Electrocardiogram

[15-JUL-25] This is a standard interval processor that writes to the result string the current channel number, the heart rate present in the signal, and background signal power. The processor assumes there are spikes in the signal that are an electrocadiogram (ECG) heartbeat signal. To obtain the heart rate, the processor does not use a Fourier transform, but rather finds all the heartbeat spikes in the interval and deduces the heart rate from their spacing. After that, it measures the power of the signal between the spikes, which we assume to be a measure of an electromyogram (EMG) signal. This processor is useful when our EMG signal is corrupted with EKG and we want to extract both heartbeat and just the EMG power. The script is also available as EKG1V2.tcl. How can I measure EMG power and heartrate when the EMG and ECG signals are mixed together? Is it possible to measure EMG power when ECG is present? Show me a processor that measures EMG and ECG.

# Here we have the frequency range over which we want to measure EMG
# power in the inter-spike regions, as well as the inter-spike margin
# size we allow to elimninate the spikes from the EMG calculation.
# The units of the margin size are fractions of an EKG period. The 
# units of frequency are Hertz. We assume the set of spikes found by
#  the processor is the set of EKG pulses in the interval. The gain is
# in microvolts per count.
set f_lo 10.0
set f_hi 160.0
set inter_fraction 0.25
set gain 0.4

# Configure the processor diagnostic displays.
set show_spikes 1

# The feature_extent is the maximum width of a spike or burst we want
# to count. The units are samples, so specify an integer. The feature
# threshold is height of a spike or burst we want to in units of mean
# absolute step size.
set feature_threshold 1
set feature_extent 40

# The burst_num_spikes is the minimum number of spikes for a cluster
# of spikes to be classified as a burst rather than a spike.
set burst_num_spikes 3

# We calculate the spike detection threshold and extent from the 
# feature threshold and extent.
set spike_threshold [expr $feature_threshold / 4.0]
set spike_extent [expr $feature_extent / 4]

# We begin this channel's section of the characteristics line with the 
# channel number.
append result "$info(channel_num) "

# If we have too much signal loss, we ignore this interval.
set max_loss 20.0

# Obtain a list of smaller spikes, from which we are going to select a list
# of larger spikes and spike bursts. The spike-finder runs in a compiled library
# routine so it executes more quickly.
set small_spikes [lwdaq spikes_x $info(values) $spike_threshold $spike_extent]

# Select solitary spikes and burst spikes. We print their existence, location, and
# size to the text window if show_spikes is set.
set spikes [list]
set bursts [list]
foreach {index height} $small_spikes {
	set spike "[format %.0f $index] [format %.1f $height]"
 	if {$height >= $feature_threshold} {
		set solitary $burst_num_spikes
		foreach {i h} $small_spikes {
			if {($i != $index) && (abs($i-$index)<$feature_extent)} {
				set solitary [expr $solitary - 1]
			}
		}
		if {$solitary >= 1} {
			if {([llength $spikes] == 0) || ([lindex $spikes end 0]<$index-$feature_extent)} {
				lappend spikes $spike
			}
		} {
			set unique 1
			foreach b $bursts {
				scan $b %f%f i h
				if {($i != $index) && (abs($i-$index)<$feature_extent)} {
					set unique 0
				}
			}
			if {$unique} {
				lappend bursts $spike
			}
		}
	}
}

# We have the number of spikes. We must format it as a real number
# so that it won't look like a channel number in the characteristics
# string.
set num_spikes [format %.1f [llength $spikes]]
set num_bursts [format %.1f [llength $bursts]]

# Calculate heart rate. We assume we have all the heart pulses accounted
# for in our spikes list. We present them as a sequence of steps to a
# straight line fit and so get the average spike period in units of 
# sample periods.
set xy ""
set index 0
foreach spike $spikes {
	append xy "$index [lindex $spike 0] " 
	incr index
}
set fit [lwdaq straight_line_fit $xy]
set period [lindex $fit 0]
set f_h [expr $info(frequency) / $period]

# Show the spikes and bursts with vertical markers if asked. The 
# Spikes are marked with a line at the top of the signal display
# and the bursts are marked with a line at the bottom. 
if {$show_spikes && [winfo exists $info(window)]} {
	if {[llength $spikes] > 0} {
		set spike_marks "0 -1 "
		foreach s $spikes {
			set i [lindex $s 0]
			set h [lindex $s 1]
			append spike_marks "$i -1.0 $i 0 $i -1.0 "
		}
		append spike_marks "[llength $info(values)] -1"
		lwdaq_graph $spike_marks $info(vt_image) -color 3 \
			-y_max 0 -y_min -10.0 \
			-x_min 0.0 -x_max [expr [llength $info(values)]-1]
	}
	if {[llength $bursts] > 0} {
		set burst_marks "0 1 "
		foreach s $bursts {
			set i [lindex $s 0]
			set h [lindex $s 1]
			append burst_marks "$i 1 $i 0 $i 1 "
		}
		append burst_marks "[llength $info(values)] 1"
		lwdaq_graph $burst_marks $info(vt_image) -color 13 \
			-y_max 10 -y_min 0 \
			-x_min 0.0 -x_max [expr [llength $info(values)]-1]
	}
}

# Go through the signal, looking between the spikes and calculating
# non-spike power.
set pwr [Neuroplayer_band_power $f_lo $f_hi 0 1]
set sum_sqrs 0.0
for {set i 1} {$i < [llength $spikes]} {incr i} {
	set first [lindex $spikes [expr $i-1] 0]
	set left_edge [expr round($first+($period)*$inter_fraction)]
	set right_edge [expr round($first+($period)*(1-$inter_fraction))]
	set stats [lwdaq ave_stdev [lrange $info(values) $left_edge $right_edge]]
	set stdev [lindex $stats 1]
	set sum_sqrs [expr $sum_sqrs + $stdev*$stdev]
}
if {$i > 1} {
	set amplitude [expr $gain*sqrt($sum_sqrs/($i-1))]
} {
	set amplitude 0.0
}

# Write the heart rate and inter-spike power to the result string.
append result "[format %.2f $f_h] [format %.3f $amplitude] "

LabChart Exporter

[15-JUL-25] Export signal values to text file suitable for reading by LabChart, following an example LabChart input text file supplied by Sebastian Bauer. Each active channel (when channel select is *) or each selected channel (when channel select is a list of channel numbers) receives a file En.txt, where n is the channel number. All values from the reconstructed signal are appended as a pair of separate real numbers on their own line of the file. The first number is time from the beginning of the export, or the absolute time. The second number is the voltage at the input of the EEG amplifier, after scaling by a constant. We select absolute or file time with the absolute_time flag, and milliseconds instead of seconds as the unit with the time_in_ms flag. We can have the sample value in millivolts, or we can have it in microvolts if we set the value_in_uV flag. The script also produces a simple processing result, giving the channel number and the number of values exported. This script is also available for download as LabChart_Exporter_V4.tcl. How can I translate NDF into LabChart format? Show me an interval processor script that exports signals to LabChart format. Will the Neuroplayer provide signals in LabChart format?

# We must tell the processor the sample rate, so that it will be sure to reconstruct
# the imperfect sample sequence received from a transmitter into a reliable signal
# with a fixed number of samples per second.
set sample_rate 512

# Most subcutaneous transmitters have input dynamic range equal to 1/100 of their
# battery voltage, because their amplifiers have a gain of 100. With a lithium
# primary cell, we have dynamic range 27 mV.
set range_mV 27.000
set mV_per_count [expr $range_mV/65536]

# We can print commas instead of decimal points for the European user. We can
# express the time in milliseconds or seconds. The sample value we will convert
# into millivolts, unless the value_in_uV flag true. We can express time as an
# absolute value with respect to UNIX time zero, or as a value relative to the
# start of the archive. Set these constants to 0 for false and 1 for true. 
set use_commas 0
set time_in_ms 0
set value_in_uV 0
set absolute_time 0

# Set the glitch threshold to zero to turn off the glitch filter, and to some
# value like 2000 to get rid of huge, single-point glitches and leave large
# artifacts intact.
set config(glitch_threshold) 2000

# Define the file name for export. We will name it En, where "n" is the 
# channel number.
set fn [file join [file dirname $config(play_file)] "E$info(channel_num)\.txt"]

# declare global start time variable for timekeeping.
global LabChart_Exporter_start_time

# Initialize this file if it does not exist. We are able to fill in the Interval
# field, which is the length of time between samples, and the DateTime and Range
# fields. But Lab Chart gives no information on how to construct the TimeFormat
# field, so we leave it blank.
if {![file exists $fn]} {
	set f [open $fn w]
	if {$time_in_ms} {
		puts $f "Interval= [expr 1000.0/$sample_rate]"
	} else {
		puts $f "Interval= [expr 1.0/$sample_rate]"
	
	}
	set ndf_metadata [LWDAQ_ndf_string_read $config(play_file)]
	if {[regexp {Created: ([^\n]*)} $ndf_metadata match date]} {
		puts $f "DateTime= $date"
	} else {
		puts $f "DateTime= Unknown"
	}
	puts $f "TimeFormat= "
	puts $f "ChannelTitle= $info(channel_num)"
	if {$value_in_uV} {
		puts $f "Range= [format %.1f [expr 1000.0*$range_mV]]"
	} else {
		puts $f "Range= $range_mV"
	}
	close $f
	set LabChart_Exporter_start_time \
		[Neuroplayer_clock_convert $info(datetime_play_time)]
} else {
	if {![info exists LabChart_Exporter_start_time]} {
		Neuroplayer_print "WARNING: File $fn exists, but no record of\
			export start time, setting to zero."
		set LabChart_Exporter_start_time 0
	}
}

# Go through the current interval and add samples to the file. Note that we
# fix the number of decimal points for both time and sample value with the 
# format command.
set export_string ""
if {$absolute_time} {
	set start_time [Neuroplayer_clock_convert $info(datetime_start_time)]
} else {
	set start_time [expr [Neuroplayer_clock_convert $info(datetime_start_time)]\
		- $LabChart_Exporter_start_time]
}
set sample_time [expr $config(play_time) + $start_time]
foreach {timestamp value} $info(signal) {
	if {$time_in_ms} {
		set st [format %.3f [expr $sample_time * 1000.0]]
	} else {
		set st [format %.6f $sample_time]
	}
	if {$value_in_uV} {
		set v [format %.1f [expr $value*$mV_per_count*1000]]
	} else {
		set v [format %.4f [expr $value*$mV_per_count]]
	}
	append export_string "$st $v\n"
	set sample_time [expr $sample_time + 1.0/$sample_rate]
}
set f [open $fn a]
if {$use_commas} {
	puts -nonewline $f [string map {. ,} $export_string]
} else {
	puts -nonewline $f $export_string
}
close $f
append result "$info(channel_num) [expr [llength $export_string]/2] "

Spike Counter

[18-JUL-25] This standard interval processor counts spikes and performs baseline power calibration. It also has an optional export function that writes the current signal to a text file on disk. It is an update version of the interval processor we used in our paper Circadian and Brain State Modulation of Network Hyperexcitability in Alzheimer's Disease. The processor produces characteristics containing the channel number, the signal loss as percentage of the expected number of messages, the number of spikes found in the interval, the number of spike bursts found in the interval, the delta-band power in thousands of square counts, or ksqcnt, and the theta-band power in ksqcnt. To obtain root mean square amplitude, take the square root of the power and multiply by the sensitivity of the transmitter. The sensitivity of most AC-coupled transmitters is 0.45 μV/cnt, and for most DC-coupled transmitters, it is 1.8 μV/cnt. The spike count is expressed as a real number in the result string, even though it is an integer. The only integers allowed in standard charachteristics lines are the channel numbers, so we deliberately turn the spike count into a real number with one digit after the decimal point. In addition to creating the characteristics line, the processor exports each active channel to a separate text file in the same directory as the processor script. This export takes place only when the Save option is checked in the Neuroplayer's processing line and a dedicated export flag is set in the script itself. This script is also available for download as SCPP4V1.tcl. How can I count spikes in my EEG or EKG signal? Show me an interval processor that performs event detection, power calibration, and signal export all at the same time. Show me an interval processor that performs several functions at once. Can the Neuroplayer detect and count short spikes automatically?

# Turn on the export with 1, disable with 0.
set export 0

# Export signal to text file in same directory as processor script. The file will
# be named En.txt where n is the channel number. We export the signa only if the
# save processing option is checked.
if {$config(save_processing) && $export} {
	set fn [file join [file dirname $config(processor_file)] "E$info(channel_num)\.txt"]
	set export_string ""
	foreach {timestamp value} $info(signal) {append export_string "$value\n"}
	set f [open $fn a]
	puts -nonewline $f $export_string
	close $f
}

# Configure the processor diagnostic displays.
set show_spikiness 0
set show_theta 0
set show_delta 0

# Define the theta and reference bands.
set theta_lo 4.0
set theta_hi 12.0
set delta_lo 0.1
set delta_hi 3.9

# The feature_extent is the maximum width of a spike or burst we want
# to count. The units are samples, so specify an integer. The feature
# threshold is height of a spike or burst we want to in units of mean
# absolute step size.
set feature_threshold 20
set feature_extent 40

# The burst_num_spikes is the minimum number of spikes for a cluster
# of spikes to be classified as a burst rather than a spike.
set burst_num_spikes 3

# We calculate the spike detection threshold and extent from the 
# feature threshold and extent.
set spike_threshold [expr $feature_threshold / 4.0]
set spike_extent [expr $feature_extent / 4]

# We begin this channel's section of the characteristics line with the 
# channel number.
append result "$info(channel_num) "

# If we have too much signal loss, we ignore this interval.
set max_loss 20.0

# We need to calculate the fourier transform so we can get the theta metric.
set config(af_calculate) 1

# The following code calculates the theta and delta power.
set theta_pwr [expr 0.001*[Neuroplayer_band_power $theta_lo $theta_hi $show_theta 0]]
set delta_pwr [expr 0.001*[Neuroplayer_band_power $delta_lo $delta_hi $show_delta 0]]

# Obtain a list of smaller spikes, from which we are going to select a list
# of larger spikes and spike bursts. The spike-finder runs in a compiled library
# routine so it executes more quickly.
set small_spikes [lwdaq spikes_x $info(values) $spike_threshold $spike_extent]

# Select solitary spikes and burst spikes. We print their existence, location, and
# size to the text window if show_spikiness is set.
set spikes [list]
set bursts [list]
foreach {index height} $small_spikes {
	set spike "[format %.0f $index] [format %.1f $height]"
 	if {$height >= $feature_threshold} {
		set solitary $burst_num_spikes
		foreach {i h} $small_spikes {
			if {($i != $index) && (abs($i-$index)<$feature_extent)} {
				set solitary [expr $solitary - 1]
			}
		}
		if {$solitary >= 1} {
			if {([llength $spikes] == 0) || ([lindex $spikes end 0]<$index-$feature_extent)} {
				lappend spikes $spike
				if {$show_spikiness && [winfo exists $info(window)]} {
					Neuroplayer_print "Spike: $spike $info(channel_num)" orange
				}
			}
		} {
			set unique 1
			foreach b $bursts {
				scan $b %f%f i h
				if {($i != $index) && (abs($i-$index)<$feature_extent)} {
					set unique 0
				}
			}
			if {$unique} {
				lappend bursts $spike
				if {$show_spikiness && [winfo exists $info(window)]} {
					Neuroplayer_print "Burst: $spike $info(channel_num)" darkgreen
				}
			}
		}
	}
}

# We have the number of spikes. We must format it as a real number
# so that it won't look like a channel number in the characteristics
# string.
set num_spikes [format %.1f [llength $spikes]]
set num_bursts [format %.1f [llength $bursts]]

# Show the spikes and bursts with vertical markers if asked. The 
# Spikes are marked with a line at the top of the signal display
# and the bursts are marked with a line at the bottom. 
if {$show_spikiness && [winfo exists $info(window)]} {
	if {[llength $spikes] > 0} {
		set spike_marks "0 -1 "
		foreach s $spikes {
			set i [lindex $s 0]
			set h [lindex $s 1]
			append spike_marks "$i -1.0 $i 0 $i -1.0 "
		}
		append spike_marks "[llength $info(values)] -1"
		lwdaq_graph $spike_marks $info(vt_image) -color 3 \
			-y_max 0 -y_min -10.0 \
			-x_min 0.0 -x_max [expr [llength $info(values)]-1]
	}
	if {[llength $bursts] > 0} {
		set burst_marks "0 1 "
		foreach s $bursts {
			set i [lindex $s 0]
			set h [lindex $s 1]
			append burst_marks "$i 1 $i 0 $i 1 "
		}
		append burst_marks "[llength $info(values)] 1"
		lwdaq_graph $burst_marks $info(vt_image) -color 13 \
			-y_max 10 -y_min 0 \
			-x_min 0.0 -x_max [expr [llength $info(values)]-1]
	}
}

# Write the loss, the number of spikes, delta power, and theta power to 
# characteristics string.
append result "[format %.1f $info(loss)]\
	$num_spikes\
	$num_bursts\
	[format %.3f $delta_pwr]\
	[format %.3f $theta_pwr] "

Event Detection and Identification

[18-JUL-25] This standard processor is a predecessor to today's Event Classifier. The processor measures power in various frequency bands. It appends to the characteristics line the channel number, reception efficiency, a primitive and not particularly effective measurement of spikiness, a baseline power, and an event identification. It performs its own baseline power calibration. The processor supports a dozen event types, including "unknown", "loss", and "normal" type. Even if there is no event, the processor will include a non-event comment. This script is also available for download as EDIP2.tcl. Show me a processor that includes event detection and identification without using the Event Classifier program. I do not want to use the Event Classifier, show me an example interval processor that I can modify to create my own event classification system. Is it possible to classify events using the Neuroarchiver, but without using the Event Classifier?

# With find_events 0, the processor does not add a comment string.
set find_events 1

# When we're checking event identification, it's sometimes conventient
# to print out only the event type, in which case we set full to 0.
set full 1

# We can get more insight into how the event identification is working
# by having the Neuroplayer plot the event-band signal on the screen.
set show 0

# We begin this channel's section of the characteristics line with the 
# channel number.
append result "$info(channel_num) "

# We add the reception efficiency.
if $full {append result "[format %.1f [expr 100.0 - $info(loss)]] "}

# Here we define the limits of the low, high, and event bands. The
# event band should include the other two bands.
set lf_lo 4.0
set lf_hi 16.0
set hf_lo 60.0
set hf_hi 160.0
set event_lo 4.0
set event_hi 160.0

# The transient band extends from just above zero frequency to just below
# the event band. When we see excessive transient power, we ignore the 
# playback interval during event detection.
set transient_pwr [expr 0.001*[Neuroplayer_band_power 0.1 [expr $lf_lo - 0.1]]]
set transient_uv [format %.1f [expr 4.0*sqrt($transient_pwr*1000)*0.4]]

# We choose our event band to include all activity that might contribute
# to the events we are looking for, and to exclude both transient noise and
# very high frequency quantization noise. In order to calculate the spikiness
# of a signal, which we do below, we must look at the sample values of the
# signal itself rather than its spectrum. If we look at the unfiltered signal,
# we will find our calculation of spikiness is disturbed by transients. Here
# we instruct the band power routine to overwrite the info(values) string with
# the inverse transform of the event band spectrum.
set event_pwr [expr 0.001*[Neuroplayer_band_power $event_lo $event_hi $show 1]]

# The spikiness of a signal is the ratio of its standard deviation without any
# values removed to its standard deviation after a two-sigma cut. A two-sigma
# cut is one that rejects all points that are more than two standard deviations
# from the mean. Here we use the event-band filtered signal to obtain the
# spikiness. For a pure train of sharp spikes, the spikiness will approach a
# value of 2.0. For a solitary spike, such as generated by a bad message, the
# spikiness will rise above 4.0.
scan [lwdaq ave_stdev $info(values)] %f%f%f%f ave stdev max min
set filtered ""
foreach v $info(values) {
	if {(abs($v-$ave) <= 2.0*$stdev)} {
		append filtered "$v "
	}
}
scan [lwdaq ave_stdev $filtered] %f%f%f%f fave fstdev fmax fmin
if {$fstdev > 0} {
	set spikiness [expr $stdev/$fstdev]
} {
	set spikiness 1.0
}

# Go through the low-frequency portion of the spectrum calculate the
# total signal power and find the peak amplitude. The frequency of
# this peak is the fundamental frequency of our event. The square of
# its amplitude is the fundamental power. We choose the low-frequency
# band to include the first harmonic of eating, noise spikes, and
# seizure spikes.
set f_peak 0.0
set a_peak 0.0
set f 0
set lf_pwr 0.0
set fundamental_pwr 0.0
foreach {a p} $info(spectrum) {
	if {$f >= $lf_lo} {
		if {$a > $a_peak} {
			set f_peak $f
			set a_peak $a
			set fundamental_pwr [expr 0.001*$a*$a]
		}
		set lf_pwr [expr $lf_pwr + 0.001*$a*$a]
	}
	set f [expr $f + $info(f_step)]
	if {$f >= $lf_hi} {break}
}

# Obtain the high-frequency power. We choose this band to include the
# power we see in high-frequency bursts, but to exclued the harmonics of
# eating and other rhythms.
set hf_pwr [expr 0.001*[Neuroplayer_band_power $hf_lo $hf_hi]]

# We record in the result line all the characteristics of the playback
# interval we need for event identification.
if $full {foreach a "transient_pwr event_pwr lf_pwr hf_pwr fundamental_pwr " {
		append result "[format %.1f [set $a]] "
	}
	append result "[format %.2f $spikiness] "
}

# This processor is self-calibrating. It compares the event power to
# a baseline power that it stores in a global variable. The baseline
# is a measure of the event-band power in the absence of any events, 
# and so we use the minimum observed power as the source of our baseline
# power value. We increase the baseline power by a small fraction every
# interval, in order to accommodate an increase in the sensitivity of
# the electrodes.
upvar #0 Neuroplayer_info(bp_$info(channel_num)) baseline_pwr
if {[info exists baseline_pwr]} {
	set baseline_pwr [expr 1.0001*$baseline_pwr]
} {
	set baseline_pwr 10.0
}
if {($event_pwr < $baseline_pwr) && ($event_pwr > 0.0) && ($info(loss) < 20.0)} {
	set baseline_pwr $event_pwr
}

# We add the baseline power to the characteristics. If we calculate
# the baseline power later, when perusing an archive, we could miss
# the defining low-power interval that sets the baseline.
if $full {append result "[format %.1f $baseline_pwr] "}

if {$find_events} {
	# Reception must be good or we declare this interval to be a loss.
	if {$info(loss) < 20.0} {
		# If the event power is a certain multiple of the baseline power
		# we declare this interval to be an event, and we attempt to identify
		# the cause of the event.
		if {($event_pwr > 5.0*$baseline_pwr)} {
			# We classify the event as a transient when the transient
			# band power is very much greater than the event power. In
			# such cases, the transient power harmonics fill the event band
			# and thwart our event identification. Rather than produce 
			# undreliable identifications, we mark the interval as a 
			# transient.
			if {($transient_pwr > 100.0*$baseline_pwr)} {
				append result "Transient "
			
			# When the signal is very spikey, this is because we have a
			# bad message in the signal, causing an enormous spike that
			# cannot be real.
			} elseif {($spikiness > 3.0)} {
				append result "Corruption "
				
			# Eating causes artifacts that we will identify in several
			# stages. The most obvious eating artifact is a sequence of
			# wave-bursts at between 4 Hz and 10 Hz, with an accompanying
			# fundamental harmonic at the sequence frequency. The event
			# is more spikey than rumble or a continuous high-frequency
			# burst, but less spikey than a sequence of spikes.
			} elseif {($spikiness > 1.20) \
				&& ($spikiness < 1.70) \
				&& ($fundamental_pwr > 0.2*$event_pwr) \
 				&& ($hf_pwr > 0.2*$event_pwr)} {
				append result "Eating "
				
			# If we have eliminated an obvious eating event, and the signal
			# is spikey with a significant proportion of high-frequency power,
			# it is a spike sequence.
			} elseif {($spikiness > 1.5) \
				&& ($hf_pwr > 0.3*$event_pwr)} {
				append result "Spikes "
			
			# If the event is not spikey and has significant high-frequency
			# content, it is a high-frequency burst.
			} elseif {($spikiness < 1.3) \
				&& ($hf_pwr > 0.3*$event_pwr)} {
				append result "Burst "
				
			# If the event is moderately spikey, with moderate high-frequency
			# power, and moderate fundamental power, it is most likely a weak
			# eating event.
			} elseif {($spikiness > 1.3) && ($hf_pwr > 0.2*$event_pwr)} {
				append result "Eating "
			
			# If we have strong high-frequency content at this state in our
			# this must be a burst event.
			} elseif {($hf_pwr > 0.5*$event_pwr)} {
				append result "Burst "
			
			# The signal has low spikiness and low high-frequency content.
			# It must be a low-frequency rhythm in the signal.
			} elseif {($spikiness < 1.3)} {
				append result "Rhythm "

			# If we have not yet identified the event, it may be a combination
			# of events that we cannot distinguish. We mark it unknown.
			} else {
				append result "Unknown "
			}
		} {
			# We mark a non-event interval as normal.
			append result "Normal "
		}
	} {
		append result "Loss "
	}
}