Skip to content

Commit b9c58ce

Browse files
committed
Added the pyphotometry preprocessing steps. There is a function called 'import_ppd' that is needed to import .ppd data. I included it in the description of the processppdphotometry function. Is this correct?
1 parent c49b580 commit b9c58ce

File tree

1 file changed

+139
-2
lines changed

1 file changed

+139
-2
lines changed

src/jdb_to_nwb/convert_photometry.py

Lines changed: 139 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import numpy as np
55
import warnings
66
import scipy.io
7-
from scipy.signal import butter, lfilter, hilbert
7+
from scipy.signal import butter, lfilter, hilbert, filtfilt
88
from scipy.sparse import diags, eye, csc_matrix
99
from scipy.sparse.linalg import spsolve
1010
from sklearn.linear_model import Lasso
@@ -336,6 +336,141 @@ def airPLS(data, lambda_=1e8, max_iterations=50):
336336
weights[-1] = weights[0]
337337
return baseline
338338

339+
def processppdphotometry(ppd_file_path):
340+
"""
341+
Process ppd file from pyPhotometry into a dict
342+
requires the import_ppd function from the data_import.py script
343+
The code needed is below.
344+
345+
def import_ppd(file_path, low_pass=20, high_pass=15.9):
346+
Function to import pyPhotometry binary data files into Python. The high_pass
347+
and low_pass arguments determine the frequency in Hz of highpass and lowpass
348+
filtering applied to the filtered analog signals. To disable highpass or lowpass
349+
filtering set the respective argument to None. Returns a dictionary with the
350+
following items:
351+
'filename' - Data filename
352+
'subject_ID' - Subject ID
353+
'date_time' - Recording start date and time (ISO 8601 format string)
354+
'end_time' - Recording end date and time (ISO 8601 format string)
355+
'mode' - Acquisition mode
356+
'sampling_rate' - Sampling rate (Hz)
357+
'LED_current' - Current for LEDs 1 and 2 (mA)
358+
'version' - Version number of pyPhotometry
359+
'analog_1' - Raw analog signal 1 (volts)
360+
'analog_2' - Raw analog signal 2 (volts)
361+
'analog_3' - Raw analog signal 3 (if present, volts)
362+
'analog_1_filt' - Filtered analog signal 1 (volts)
363+
'analog_2_filt' - Filtered analog signal 2 (volts)
364+
'analog_3_filt' - Filtered analog signal 2 (if present, volts)
365+
'digital_1' - Digital signal 1
366+
'digital_2' - Digital signal 2 (if present)
367+
'pulse_inds_1' - Locations of rising edges on digital input 1 (samples).
368+
'pulse_inds_2' - Locations of rising edges on digital input 2 (samples).
369+
'pulse_times_1' - Times of rising edges on digital input 1 (ms).
370+
'pulse_times_2' - Times of rising edges on digital input 2 (ms).
371+
'time' - Time of each sample relative to start of recording (ms)
372+
373+
with open(file_path, "rb") as f:
374+
header_size = int.from_bytes(f.read(2), "little")
375+
data_header = f.read(header_size)
376+
data = np.frombuffer(f.read(), dtype=np.dtype("<u2"))
377+
# Extract header information
378+
header_dict = json.loads(data_header)
379+
volts_per_division = header_dict["volts_per_division"]
380+
sampling_rate = header_dict["sampling_rate"]
381+
# Extract signals.
382+
analog = data >> 1 # Analog signal is most significant 15 bits.
383+
digital = ((data & 1) == 1).astype(int) # Digital signal is least significant bit.
384+
# Alternating samples are different signals.
385+
if "n_analog_signals" in header_dict.keys():
386+
n_analog_signals = header_dict["n_analog_signals"]
387+
n_digital_signals = header_dict["n_digital_signals"]
388+
else: # Pre version 1.0 data file.
389+
n_analog_signals = 2
390+
n_digital_signals = 2
391+
analog_1 = analog[::n_analog_signals] * volts_per_division[0]
392+
analog_2 = analog[1::n_analog_signals] * volts_per_division[1]
393+
analog_3 = analog[2::n_analog_signals] * volts_per_division[0] if n_analog_signals == 3 else None
394+
digital_1 = digital[::n_analog_signals]
395+
digital_2 = digital[1::n_analog_signals] if n_digital_signals == 2 else None
396+
time = np.arange(analog_1.shape[0]) * 1000 / sampling_rate # Time relative to start of recording (ms).
397+
# Filter signals with specified high and low pass frequencies (Hz).
398+
if low_pass and high_pass:
399+
b, a = butter(2, np.array([high_pass, low_pass]) / (0.5 * sampling_rate), "bandpass")
400+
elif low_pass:
401+
b, a = butter(2, low_pass / (0.5 * sampling_rate), "low")
402+
elif high_pass:
403+
b, a = butter(2, high_pass / (0.5 * sampling_rate), "high")
404+
if low_pass or high_pass:
405+
analog_1_filt = filtfilt(b, a, analog_1)
406+
analog_2_filt = filtfilt(b, a, analog_2)
407+
analog_3_filt = filtfilt(b, a, analog_3) if n_analog_signals == 3 else None
408+
else:
409+
analog_1_filt = analog_2_filt = analog_3_filt = None
410+
# Extract rising edges for digital inputs.
411+
pulse_inds_1 = 1 + np.where(np.diff(digital_1) == 1)[0]
412+
pulse_inds_2 = 1 + np.where(np.diff(digital_2) == 1)[0] if n_digital_signals == 2 else None
413+
pulse_times_1 = pulse_inds_1 * 1000 / sampling_rate
414+
pulse_times_2 = pulse_inds_2 * 1000 / sampling_rate if n_digital_signals == 2 else None
415+
# Return signals + header information as a dictionary.
416+
data_dict = {
417+
"filename": os.path.basename(file_path),
418+
"analog_1": analog_1,
419+
"analog_2": analog_2,
420+
"analog_1_filt": analog_1_filt,
421+
"analog_2_filt": analog_2_filt,
422+
"digital_1": digital_1,
423+
"digital_2": digital_2,
424+
"pulse_inds_1": pulse_inds_1,
425+
"pulse_inds_2": pulse_inds_2,
426+
"pulse_times_1": pulse_times_1,
427+
"pulse_times_2": pulse_times_2,
428+
"time": time,
429+
}
430+
if n_analog_signals == 3:
431+
data_dict.update(
432+
{
433+
"analog_3": analog_3,
434+
"analog_3_filt": analog_3_filt,
435+
}
436+
)
437+
data_dict.update(header_dict)
438+
return data_dict
439+
"""
440+
ppd_data = import_ppd(ppd_file_path)
441+
442+
raw_green = pd.Series(ppd_data['analog_1'])
443+
raw_red = pd.Series(ppd_data ['analog_2'])
444+
raw_405 = pd.Series(ppd_data['analog_3'])
445+
446+
relative_raw_signal = raw_green / raw_405
447+
448+
sampling_rate = ppd_data['sampling_rate']
449+
450+
b,a = butter(2, 10, btype='low', fs=sampling_rate)
451+
GACh_denoised = filtfilt(b,a, raw_green)
452+
rDA3m_denoised = filtfilt(b,a, raw_red)
453+
ratio_denoised = filtfilt(b,a, relative_raw_signal)
454+
denoised_405 = filtfilt(b,a, raw_405)
455+
456+
# high pass at 0.001Hz which removes the drift due to bleaching, but will also remove any physiological variation in the signal on very slow timescales.
457+
b,a = butter(2, 0.001, btype='high', fs=sampling_rate)
458+
GACh_highpass = filtfilt(b,a, GACh_denoised, padtype='even')
459+
rDA3m_highpass = filtfilt(b,a, rDA3m_denoised, padtype='even')
460+
ratio_highpass = filtfilt(b,a, ratio_denoised, padtype='even')
461+
highpass_405 = filtfilt(b,a, denoised_405, padtype='even')
462+
463+
green_zscored = np.divide(np.subtract(GACh_highpass,GACh_highpass.mean()),GACh_highpass.std())
464+
465+
red_zscored = np.divide(np.subtract(rDA3m_highpass,rDA3m_highpass.mean()),rDA3m_highpass.std())
466+
467+
zscored_405 = np.divide(np.subtract(highpass_405,highpass_405.mean()),highpass_405.std())
468+
469+
ratio_zscored = np.divide(np.subtract(ratio_highpass,ratio_highpass.mean()),ratio_highpass.std())
470+
471+
return {"green_zscored": green_zscored, "red_zscored": red_zscored, "ratio_zscored": ratio_zscored, "zscored_405": zscored_405}
472+
pass
473+
339474

340475
def add_photometry_metadata(nwbfile: NWBFile, metadata: dict):
341476
# TODO for Ryan - add photometry metadata to NWB :)
@@ -395,9 +530,11 @@ def add_photometry(nwbfile: NWBFile, metadata: dict):
395530
elif "ppd_file_path" in metadata["photometry"]:
396531
# Process ppd file from pyPhotometry
397532
print("Processing ppd file from pyPhotometry...")
398-
533+
ppd_file_path = metadata["photometry"]["ppd_file_path"]
534+
signals = processppdphotometry(ppd_file_path)
399535
# TODO for Jose - add pyPhotometry processing here!!
400536
# Probably add the processing functions above and just call them here
537+
401538
raise NotImplementedError("pyPhotometry processing is not yet implemented.")
402539

403540
else:

0 commit comments

Comments
 (0)