|
4 | 4 | import numpy as np |
5 | 5 | import warnings |
6 | 6 | import scipy.io |
7 | | -from scipy.signal import butter, lfilter, hilbert |
| 7 | +from scipy.signal import butter, lfilter, hilbert, filtfilt |
8 | 8 | from scipy.sparse import diags, eye, csc_matrix |
9 | 9 | from scipy.sparse.linalg import spsolve |
10 | 10 | from sklearn.linear_model import Lasso |
@@ -336,6 +336,141 @@ def airPLS(data, lambda_=1e8, max_iterations=50): |
336 | 336 | weights[-1] = weights[0] |
337 | 337 | return baseline |
338 | 338 |
|
| 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 | + |
339 | 474 |
|
340 | 475 | def add_photometry_metadata(nwbfile: NWBFile, metadata: dict): |
341 | 476 | # TODO for Ryan - add photometry metadata to NWB :) |
@@ -395,9 +530,11 @@ def add_photometry(nwbfile: NWBFile, metadata: dict): |
395 | 530 | elif "ppd_file_path" in metadata["photometry"]: |
396 | 531 | # Process ppd file from pyPhotometry |
397 | 532 | print("Processing ppd file from pyPhotometry...") |
398 | | - |
| 533 | + ppd_file_path = metadata["photometry"]["ppd_file_path"] |
| 534 | + signals = processppdphotometry(ppd_file_path) |
399 | 535 | # TODO for Jose - add pyPhotometry processing here!! |
400 | 536 | # Probably add the processing functions above and just call them here |
| 537 | + |
401 | 538 | raise NotImplementedError("pyPhotometry processing is not yet implemented.") |
402 | 539 |
|
403 | 540 | else: |
|
0 commit comments