Skip to content

Commit e59a050

Browse files
NWB export of ExpData
1 parent d0359c0 commit e59a050

File tree

1 file changed

+238
-0
lines changed

1 file changed

+238
-0
lines changed

ephys/classes/NWB/file_export.py

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
import numpy as np
2+
from pynwb import NWBHDF5IO, NWBFile, TimeSeries
3+
from pynwb.icephys import (
4+
CurrentClampStimulusSeries,
5+
CurrentClampSeries,
6+
VoltageClampSeries,
7+
VoltageClampStimulusSeries,
8+
IntracellularElectrode,
9+
)
10+
from uuid import uuid4
11+
from ephys.classes.experiment_objects import ExpData, MetaData, SubjectInfo, DeviceInfo
12+
from ephys.classes.trace import Trace
13+
from ephys.classes.channels import ChannelInformation, Channel
14+
from ephys.classes.current import CurrentClamp, CurrentTrace
15+
from ephys.classes.voltage import VoltageClamp, VoltageTrace
16+
17+
from quantities import Quantity
18+
19+
20+
class NWBExporter:
21+
22+
def __init__(
23+
self,
24+
experiment: ExpData,
25+
lab="Unknown Lab",
26+
institution: str = "Unknown Institution",
27+
session_description: str = "Unknown Session",
28+
device: DeviceInfo | None = None,
29+
) -> None:
30+
self.experiment = experiment
31+
self.lab = lab
32+
self.institution = institution
33+
self.session_description = session_description
34+
35+
date_of_exp = self.experiment.meta_data.get_date_of_experiment()
36+
start_index = np.argmin(date_of_exp)
37+
self.experimenter = list(set(self.experiment.meta_data.get_experimenter()))
38+
self.electrode = list()
39+
self.electrode_groups = list()
40+
self.response = list()
41+
self.stimulus = list()
42+
43+
self.nwb = NWBFile(
44+
session_description="Ephys recording",
45+
identifier=str(uuid4()),
46+
session_start_time=date_of_exp[start_index],
47+
experimenter=self.experimenter,
48+
lab=self.lab,
49+
institution=self.institution,
50+
)
51+
self.device = device or self.nwb.create_device(
52+
name=device.device_name if device else "unknown",
53+
description=device.device_description if device else "unknown device",
54+
)
55+
56+
def add_all_traces(self) -> None:
57+
for trace in self.experiment.protocols:
58+
# TODO: check for same electrodes
59+
self.add_trace(trace)
60+
61+
def add_trace(self, trace: Trace, location: str = "") -> None:
62+
"""
63+
Adds a trace to the NWB file.
64+
"""
65+
for channel_group in np.unique(trace.channel_information.channel_grouping):
66+
self.add_VC_group(trace, location, channel_grouping=channel_group)
67+
68+
def add_electrode(
69+
self, location: str = "", electrode_number: int | None = None
70+
) -> IntracellularElectrode:
71+
electrode = self.nwb.create_icephys_electrode(
72+
name=(
73+
f"Electrode_{electrode_number}"
74+
if electrode_number is not None
75+
else "Electrode"
76+
),
77+
description="Icephys electrode",
78+
location=location,
79+
device=self.device,
80+
)
81+
self.electrode.append(electrode)
82+
return electrode
83+
84+
def _parse_sweep_data(
85+
self,
86+
channel_data: VoltageClamp | CurrentClamp | VoltageTrace | CurrentTrace,
87+
time: Quantity,
88+
electrode: IntracellularElectrode,
89+
sweep_number: int,
90+
channel_description: str = "",
91+
) -> dict:
92+
return {
93+
"data": channel_data.data[sweep_number],
94+
# "unit": channel_data.unit,
95+
"timestamps": time.magnitude[sweep_number],
96+
# "time_unit": time.dimensionality.string,
97+
"electrode": electrode,
98+
# "rate": float(channel_data.sampling_rate.magnitude),
99+
"gain": 1.0,
100+
"resolution": 1.0,
101+
"conversion": 1.0,
102+
"description": channel_description,
103+
"sweep_number": sweep_number,
104+
}
105+
106+
def add_VC_group(
107+
self,
108+
trace: Trace,
109+
location: str = "unknown location",
110+
channel_grouping: int | None = None,
111+
) -> None:
112+
113+
group_indices = np.where(
114+
trace.channel_information.channel_grouping == channel_grouping
115+
)[0]
116+
electrode = self.add_electrode(location=location)
117+
for channel_index in group_indices:
118+
channel: Channel = trace.channel[channel_index]
119+
self.add_channel(
120+
channel=channel,
121+
time=trace.time,
122+
electrode=electrode,
123+
)
124+
125+
def add_channel(
126+
self,
127+
channel: Channel,
128+
time: Quantity,
129+
electrode: IntracellularElectrode,
130+
channel_description: str = "",
131+
) -> None:
132+
for sweep in range(channel.sweep_count):
133+
if isinstance(channel, CurrentClamp):
134+
self.add_current_stimulus_sweep(
135+
channel_data=channel,
136+
time=time,
137+
electrode=electrode,
138+
sweep_number=sweep,
139+
channel_description=channel_description,
140+
)
141+
elif isinstance(channel, VoltageClamp):
142+
self.add_voltage_stimulus_sweep(
143+
channel_data=channel,
144+
time=time,
145+
electrode=electrode,
146+
sweep_number=sweep,
147+
channel_description=channel_description,
148+
)
149+
elif isinstance(channel, CurrentTrace):
150+
self.add_current_response_sweep(
151+
channel_data=channel,
152+
time=time,
153+
electrode=electrode,
154+
sweep_number=sweep,
155+
channel_description=channel_description,
156+
)
157+
elif isinstance(channel, VoltageTrace):
158+
self.add_voltage_response_sweep(
159+
channel_data=channel,
160+
time=time,
161+
electrode=electrode,
162+
sweep_number=sweep,
163+
channel_description=channel_description,
164+
)
165+
166+
def add_current_stimulus_sweep(
167+
self,
168+
channel_data: CurrentClamp,
169+
time: Quantity,
170+
electrode: IntracellularElectrode,
171+
sweep_number: int,
172+
channel_description: str = "Current clamp stimulus",
173+
) -> CurrentClampStimulusSeries:
174+
ex_stim = CurrentClampStimulusSeries(
175+
name=f"CurrentClampStimulus_ch_{channel_data.channel_number}_sweep_{sweep_number}",
176+
**self._parse_sweep_data(
177+
channel_data, time, electrode, sweep_number, channel_description
178+
),
179+
)
180+
self.nwb.add_stimulus(stimulus=ex_stim)
181+
return ex_stim
182+
183+
def add_voltage_stimulus_sweep(
184+
self,
185+
channel_data: VoltageClamp,
186+
time: Quantity,
187+
electrode: IntracellularElectrode,
188+
sweep_number: int,
189+
channel_description: str = "Voltage clamp stimulus",
190+
) -> VoltageClampStimulusSeries:
191+
ex_stim = VoltageClampStimulusSeries(
192+
name=f"VoltageClampStimulus_ch_{channel_data.channel_number}_sweep_{sweep_number}",
193+
**self._parse_sweep_data(
194+
channel_data, time, electrode, sweep_number, channel_description
195+
),
196+
)
197+
self.nwb.add_stimulus(stimulus=ex_stim)
198+
return ex_stim
199+
200+
def add_voltage_response_sweep(
201+
self,
202+
channel_data: VoltageTrace,
203+
time: Quantity,
204+
electrode: IntracellularElectrode,
205+
sweep_number: int,
206+
channel_description: str = "Current clamp response",
207+
) -> VoltageClampSeries:
208+
ex_record = VoltageClampSeries(
209+
name=f"VoltageClampRecording_ch_{channel_data.channel_number}_sweep_{sweep_number}",
210+
**self._parse_sweep_data(
211+
channel_data, time, electrode, sweep_number, channel_description
212+
),
213+
)
214+
self.nwb.add_acquisition(nwbdata=ex_record)
215+
return ex_record
216+
217+
def add_current_response_sweep(
218+
self,
219+
channel_data: CurrentTrace,
220+
time: Quantity,
221+
electrode: IntracellularElectrode,
222+
sweep_number: int,
223+
channel_description: str = "Current clamp response",
224+
) -> CurrentClampSeries:
225+
ex_record = CurrentClampSeries(
226+
name=f"CurrentClampRecording_ch_{channel_data.channel_number}_sweep_{sweep_number}",
227+
**self._parse_sweep_data(
228+
channel_data, time, electrode, sweep_number, channel_description
229+
),
230+
)
231+
self.nwb.add_acquisition(nwbdata=ex_record)
232+
return ex_record
233+
234+
def write_nwb(self, file_path: str) -> None:
235+
if file_path.endswith(".nwb"):
236+
with NWBHDF5IO(file_path, "w") as io:
237+
io.write(self.nwb)
238+
io.close()

0 commit comments

Comments
 (0)