spikertools 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spikertools/__init__.py +7 -0
- spikertools/core.py +149 -0
- spikertools/models.py +638 -0
- spikertools/plots.py +646 -0
- spikertools-0.1.0.dist-info/METADATA +232 -0
- spikertools-0.1.0.dist-info/RECORD +8 -0
- spikertools-0.1.0.dist-info/WHEEL +5 -0
- spikertools-0.1.0.dist-info/top_level.txt +1 -0
spikertools/__init__.py
ADDED
spikertools/core.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# spikertools/core.py
|
|
2
|
+
|
|
3
|
+
from spikertools.models import Event, Neuron, Channel, Session, Events, Channels
|
|
4
|
+
from spikertools.plots import Plots
|
|
5
|
+
import numpy as np
|
|
6
|
+
from scipy.io import wavfile
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
|
|
11
|
+
class Session:
|
|
12
|
+
def __init__(self, wav_file_path, events_file_path=None):
|
|
13
|
+
self.wav_file = wav_file_path
|
|
14
|
+
self.sample_rate, self.data = wavfile.read(wav_file_path)
|
|
15
|
+
print(f"Loaded WAV file: {wav_file_path}")
|
|
16
|
+
print(f"Sample rate: {self.sample_rate} Hz")
|
|
17
|
+
print(f"Data length: {len(self.data)} samples")
|
|
18
|
+
|
|
19
|
+
if events_file_path is None:
|
|
20
|
+
# Infer events file path
|
|
21
|
+
events_file = wav_file_path.replace('.wav', '-events.txt')
|
|
22
|
+
else:
|
|
23
|
+
events_file = events_file_path
|
|
24
|
+
|
|
25
|
+
print(f"Looking for events file: {events_file}")
|
|
26
|
+
|
|
27
|
+
# Initialize events and neurons before loading
|
|
28
|
+
self.events = Events([]) # Changed from list to Events
|
|
29
|
+
self.neurons = []
|
|
30
|
+
|
|
31
|
+
# Initialize the Plots class
|
|
32
|
+
self.plots = Plots(self)
|
|
33
|
+
|
|
34
|
+
if os.path.exists(events_file):
|
|
35
|
+
self._load_events(events_file)
|
|
36
|
+
print(f"Loaded {len(self.events)} events and {len(self.neurons)} neurons.")
|
|
37
|
+
else:
|
|
38
|
+
print("No events file found.")
|
|
39
|
+
|
|
40
|
+
# Initialize other attributes
|
|
41
|
+
self.channels = self._initialize_channels()
|
|
42
|
+
self.datetime = self._extract_datetime()
|
|
43
|
+
|
|
44
|
+
def _load_events(self, events_file):
|
|
45
|
+
with open(events_file, 'r') as f:
|
|
46
|
+
for line in f:
|
|
47
|
+
line = line.strip()
|
|
48
|
+
if not line or line.startswith('#'):
|
|
49
|
+
continue
|
|
50
|
+
parts = line.split(',')
|
|
51
|
+
if len(parts) != 2:
|
|
52
|
+
continue
|
|
53
|
+
name, timestamp = parts
|
|
54
|
+
name = name.strip()
|
|
55
|
+
try:
|
|
56
|
+
timestamp = float(timestamp.strip())
|
|
57
|
+
except ValueError:
|
|
58
|
+
print(f"Invalid timestamp: {timestamp} in line: {line}")
|
|
59
|
+
continue
|
|
60
|
+
self._add_event(name, timestamp)
|
|
61
|
+
|
|
62
|
+
def _add_event(self, name, timestamp):
|
|
63
|
+
# Normalize the event name by stripping leading/trailing spaces
|
|
64
|
+
name = name.strip()
|
|
65
|
+
|
|
66
|
+
# Assign colors to events and neurons
|
|
67
|
+
color = 'k' # Default color is black
|
|
68
|
+
color_map = {
|
|
69
|
+
'1': 'r', '2': 'g', '3': 'b', '4': 'c', '5': 'm',
|
|
70
|
+
'Open': 'orange', 'Close': 'purple',
|
|
71
|
+
}
|
|
72
|
+
if name in color_map:
|
|
73
|
+
color = color_map[name]
|
|
74
|
+
|
|
75
|
+
# Check if event already exists
|
|
76
|
+
if self.events.has_event(name):
|
|
77
|
+
self.events[name].timestamps.append(timestamp)
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
# Handle threshold events for neurons
|
|
81
|
+
if 'thresh' in name.lower():
|
|
82
|
+
# Use regex to parse threshold events
|
|
83
|
+
match = re.match(r'_(neuron\d+)thresh(\w+)-(\d+)', name, re.IGNORECASE)
|
|
84
|
+
if match:
|
|
85
|
+
neuron_id, thresh_type_partial, thresh_value_str = match.groups()
|
|
86
|
+
thresh_value = -int(thresh_value_str) # Assuming thresholds are negative
|
|
87
|
+
|
|
88
|
+
# Find the neuron that corresponds to this threshold
|
|
89
|
+
target_neuron = None
|
|
90
|
+
for neuron in self.neurons:
|
|
91
|
+
if neuron_id in neuron.name:
|
|
92
|
+
target_neuron = neuron
|
|
93
|
+
break
|
|
94
|
+
|
|
95
|
+
if target_neuron:
|
|
96
|
+
if 'hig' in thresh_type_partial.lower():
|
|
97
|
+
target_neuron.thresh_high = thresh_value
|
|
98
|
+
#print(f"Set high threshold for {target_neuron.name} to {thresh_value}")
|
|
99
|
+
elif 'low' in thresh_type_partial.lower():
|
|
100
|
+
target_neuron.thresh_low = thresh_value
|
|
101
|
+
#print(f"Set low threshold for {target_neuron.name} to {thresh_value}")
|
|
102
|
+
else:
|
|
103
|
+
print(f"Unknown threshold type in event name: {name}")
|
|
104
|
+
else:
|
|
105
|
+
print(f"Neuron '{neuron_id}' not found for threshold event: {name}")
|
|
106
|
+
return # Threshold event processed; exit the method
|
|
107
|
+
|
|
108
|
+
# Check if neuron already exists for spike events
|
|
109
|
+
if name.startswith('_'):
|
|
110
|
+
for neuron in self.neurons:
|
|
111
|
+
if neuron.name == name:
|
|
112
|
+
neuron.timestamps.append(timestamp)
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
# If not, create a new neuron
|
|
116
|
+
neuron = Neuron(name, timestamps=[timestamp], color=color)
|
|
117
|
+
self.neurons.append(neuron)
|
|
118
|
+
#print(f"Added neuron: '{name}' with initial spike at timestamp: {timestamp}")
|
|
119
|
+
else:
|
|
120
|
+
# If not a neuron, treat as a regular event
|
|
121
|
+
event = Event(name, timestamps=[timestamp], color=color)
|
|
122
|
+
self.events.append(event)
|
|
123
|
+
#print(f"Added event: '{name}' at timestamp: {timestamp}")
|
|
124
|
+
|
|
125
|
+
def _initialize_channels(self):
|
|
126
|
+
# Initialize channels based on the data
|
|
127
|
+
channels = Channels([]) # Initialize empty Channels container (this uses models.Channels)
|
|
128
|
+
if self.data.ndim == 1:
|
|
129
|
+
channel = Channel(self.data, sample_rate=self.sample_rate, number=0) # This will now use models.Channel
|
|
130
|
+
channels.append(channel)
|
|
131
|
+
else:
|
|
132
|
+
for i in range(self.data.shape[1]):
|
|
133
|
+
channel_data = self.data[:, i]
|
|
134
|
+
channel = Channel(channel_data, sample_rate=self.sample_rate, number=i) # This will now use models.Channel
|
|
135
|
+
channels.append(channel)
|
|
136
|
+
return channels
|
|
137
|
+
|
|
138
|
+
def _extract_datetime(self):
|
|
139
|
+
# Extract datetime from the filename
|
|
140
|
+
basename = os.path.basename(self.wav_file)
|
|
141
|
+
match = re.search(r'(\d{4}-\d{2}-\d{2})_(\d{2}\.\d{2}\.\d{2})', basename)
|
|
142
|
+
if match:
|
|
143
|
+
date_str, time_str = match.groups()
|
|
144
|
+
date_parts = [int(part) for part in date_str.split('-')]
|
|
145
|
+
time_parts = [int(part) for part in time_str.split('.')]
|
|
146
|
+
return datetime(*date_parts, *time_parts)
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
|