Skip to content
shreeshkarjagiPublic

About

Python toolkit for analyzing Medtronic Percept PC/RC local field potential (LFP) data: load JSON session reports, run spectral analysis, and generate basic figures for deep brain stimulation research.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

11 Commits

Folders and files

Repository files navigation

pypercept

Analysis toolkit for Medtronic Percept PC/RC Local Field Potential (LFP) data.

Loads JSON session reports exported from the Percept programmer, runs standard spectral analysis, and generates figures.

Supported data modalities

Modality JSON key What it is Channels Stim
IndefiniteStreaming IndefiniteStreaming Raw time-domain recording 6 (all sensing pairs) OFF
BrainSenseTimeDomain BrainSenseTimeDomain Streaming with optional stim 1–2 ON or OFF
BrainSenseLfp BrainSenseLfp Device-computed LFP power (companion to TimeDomain) 1–2 ON or OFF
Timeline DiagnosticData.LFPTrendLogs Chronic 10-min power averages per hemisphere ON
Events DiagnosticData.LfpFrequencySnapshotEvents Patient-triggered FFT snapshots per hemisphere ON

Install

pip install numpy scipy pandas matplotlib

drop the pypercept/ folder next to your scripts.

Quick start

Load and process a single session

import pypercept as lfp

# simple load --> returns Session object ready for processing
session = lfp.load_session("Report_Session_20251014.json")

# process all channels: notch filter, bandpass, Welch PSD, band powers
result = lfp.process_session(session)

# look at the results
for ch in result.channels:
    print(f"{ch.channel} ({ch.hemisphere})")
    print(f"  beta1 power: {ch.band_powers['beta1']:.4f} µV²/Hz")

Full access to all modalities

# load_session_full gives you the full PerceptSession with all data types
ps = lfp.load_session_full("Report_Session_20251014.json")
print(ps.summary())

# access indefinite streaming channels
for stream in ps.indefinite_streaming:
    print(f"{stream.channel}: {stream.duration_seconds:.1f}s, {stream.n_samples} samples")

# access event snapshots
for event in ps.event_snapshots:
    print(f"{event.event_name} | {event.hemisphere} | {event.datetime[:10]}")
    print(f"  FFT bins: {len(event.fft_bin_data)}, freq range: {event.frequency[0]:.1f}-{event.frequency[-1]:.1f} Hz")

# access timeline (chronic tracking)
for tl in ps.timeline_data:
    df = tl.to_dataframe()
    print(f"{tl.hemisphere}: {len(df)} records")

Batch process a directory

# process all JSON files in a folder
results = lfp.process_directory("./data/", verbose=True)

# convert to a flat dataframe
df = lfp.results_to_dataframe(results)
print(df.head())

# aggregate by session (average across channels)
df_sessions = lfp.aggregate_by_session(results, hemisphere="left")

Signal processing building blocks

import numpy as np

# these work on any 1D array, not just Percept data
data = np.random.randn(5000)  # or your LFP signal
fs = 250

# preprocess: 60 Hz notch + 1-100 Hz bandpass
clean = lfp.preprocess(data, fs)

# PSD via Welch
freqs, psd = lfp.compute_psd(clean, fs)

# extract power in a specific band
beta_power = lfp.extract_band_power(psd, freqs, "beta1")
custom_band = lfp.extract_band_power(psd, freqs, (13, 20))

# all standard bands at once
bands = lfp.extract_all_bands(psd, freqs)
# {'delta': ..., 'theta': ..., 'alpha': ..., 'beta1': ..., 'beta2': ..., 'gamma': ...}

# spectrogram
t, f, Sxx = lfp.compute_spectrogram(clean, fs)

# epoching + artifact rejection
epochs = lfp.epoch(clean, fs, duration=2.0, overlap=0.5)
good_epochs = lfp.reject_bad_epochs(epochs, threshold=500)

Plotting

lfp.set_style()  # set matplotlib defaults

# PSD plot
freqs, psd = lfp.compute_psd(clean, fs)
lfp.plot_psd(freqs, psd, show_bands=True, freq_range=(1, 50))

# session dashboard (PSDs + spectrograms + time series + band powers)
result = lfp.process_session(session, compute_spec=True)
lfp.plot_session_dashboard(result, output_path="dashboard.png")

# longitudinal PSDs across sessions (one subplot per channel, color = date)
results = lfp.process_directory("./data/")
lfp.plot_longitudinal_psd(
    results,
    title="Longitudinal PSDs",
    legend_annotations={"10/24": "10/24 (ECT started)", "11/13": "11/13 (DBS ON)"},
)

# batch dashboards for every session in a folder
lfp.generate_all_dashboards("./data/", "./figures/", compute_spec=True)

Group comparison

df = lfp.results_to_dataframe(results)
df["phase"] = ["pre" if d < "2025-01-01" else "post" for d in df["date"]]

stats = lfp.compare_groups(df, "phase", "pre", "post")
print(stats[["band", "mean_pre", "mean_post", "p_value"]])

Frequency bands

Default band definitions (matching Percept event snapshot bands):

Band Range (Hz)
delta 1–4
theta 4–8
alpha 8–13
beta1 (low beta) 13–20
beta2 (high beta) 20–30
gamma (low gamma) 30–50

Percept JSON structure

The Medtronic programmer exports a single JSON file per session. Key top-level fields this package reads:

SessionDate
IndefiniteStreaming[]           --> raw time-domain, 6 channels
  .Channel, .TimeDomainData[], .SampleRateInHz, .FirstPacketDateTime
  .GlobalSequences, .GlobalPacketSizes, .TicksInMses
BrainSenseTimeDomain[]         --> streaming with stim
  (same fields as IndefiniteStreaming)
BrainSenseLfp[]                --> device-computed LFP power
  .LfpData.Left/.Right         --> .TicksInMs, .LFP, .mA, .Frequency
DiagnosticData
  .LFPTrendLogs                --> chronic power averages
  .LfpFrequencySnapshotEvents  --> patient-triggered FFT snapshots
Groups                         --> therapy group settings
DeviceInformation              --> neurostimulator metadata

Channel naming

Percept uses contact pair names like ZERO_THREE_LEFT or ONE_AND_THREE_RIGHT. The library normalizes these to readable labels:

Device name Label
ZERO_THREE_LEFT L 0-3
ONE_THREE_LEFT L 1-3
ZERO_TWO_LEFT L 0-2
ZERO_THREE_RIGHT R 0-3
ONE_THREE_RIGHT R 1-3
ZERO_TWO_RIGHT R 0-2

Processing defaults

  • Sample rate: 250 Hz (Percept PC/RC native rate)
  • Notch filter: 60 Hz (Q=30)
  • Bandpass: 1–100 Hz, 4th order Butterworth
  • PSD: Welch's method, 256-sample segments, 50% overlap, linear detrend
  • Artifact rejection: ±500 µV threshold on epochs

Sensing channel impedance

session = lfp.load_session("Report_Session_20251014.json")

# auto-detect sensing config from Groups
from pypercept.impedance import sensing_channel_impedance
results = sensing_channel_impedance(session=session)

for side, r in results.items():
    print(f"{side} {r.sensing_pair}: {r.channel_impedance:.0f} ohms")
    print(f"  type: {r.channel_type}, valid: {r.model_valid}")
    if not r.model_valid:
        print(f"  warnings: {r.validation_notes}")

# override sensing config
results = sensing_channel_impedance(session=session, sensing_pair=(0, 2))

Three config types are handled automatically based on whether each level in the sensing pair is a ring (levels 0, 3) or segmented (levels 1, 2 on SenSight leads):

Config type Example Method Exact?
ring + ring 0-3, any 1x4 pair bipolar directly yes
ring + segments 0-2, 1-3 seg-to-seg decomposition + cross-level bipolars validated via z_ring spread
segments + segments 1-2 independent seg-to-seg decomposition per level no cross-level estimation

The decomposition uses an additive contact model (B_ij = z_i + z_j) which is approximate. Results include a model_valid flag and validation_notes when the model doesn't fit (e.g. negative ring impedance or high spread between estimates).

About

Python toolkit for analyzing Medtronic Percept PC/RC local field potential (LFP) data: load JSON session reports, run spectral analysis, and generate basic figures for deep brain stimulation research.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages