├── extract ├── afterPoint.m ├── correct4MissingSamples.m ├── extractLFPMontage.m ├── extractStimAmp.m ├── extractLFP.m └── extractTrendLogs.m ├── concatenateLFP.m ├── plot ├── plotChannels.m ├── plotPwelch.m ├── plotSpectrogram.m ├── plotLFPTrendLogs.asv └── plotLFPTrendLogs.m ├── README.md └── loadJSON.m /extract/afterPoint.m: -------------------------------------------------------------------------------- 1 | function out = afterPoint(str) 2 | %Remove all text before point 3 | %Yohann Thenaisie 20.12.2020 4 | 5 | startIndex = strfind(str,'.'); 6 | out = str(startIndex+1:end); -------------------------------------------------------------------------------- /concatenateLFP.m: -------------------------------------------------------------------------------- 1 | %When a streaming is disrupted, it appears as two recordings. 2 | %Concatenate two LFPs recordings and interleave NaNs to account for the 3 | %disruption. 4 | %Yohann Thenaisie 15.02.2021 5 | 6 | filename1 = '20210505T100520_BrainSenseTimeDomain'; 7 | filename2 = '20210505T101029_BrainSenseTimeDomain'; 8 | 9 | load(filename1) %load recording 1 (chronologically first) 10 | LFP1 = LFP; 11 | 12 | load(filename2) %load recording 2 (chronologically second) 13 | LFP2 = LFP; 14 | 15 | %Create a new matrix and contatenate data from both recordings 16 | fullDuration = LFP2.firstTickInSec - LFP1.firstTickInSec + LFP2.time(end); %s 17 | LFP = LFP1; 18 | LFP.time = 1/LFP.Fs:1/LFP.Fs:fullDuration; 19 | LFP.data = NaN(size(LFP.time, 2), LFP.nChannels); 20 | LFP.data(1:size(LFP1.data, 1), :) = LFP1.data; 21 | LFP.data(end-size(LFP2.data, 1)+1:end, :) = LFP2.data; 22 | 23 | %Plot - there should be a disruption 24 | plotChannels(LFP.data, LFP); 25 | 26 | save([filename1 '_concatenated'], 'LFP') -------------------------------------------------------------------------------- /plot/plotChannels.m: -------------------------------------------------------------------------------- 1 | function channelsFig = plotChannels(data, channelParams) 2 | %channelsFig = plotChannels(LFP.data, LFP) 3 | %Plots data from each channel of LFP data in a subplot 4 | %S is a structure with fields: 5 | %.data, .time, .nChannels, .channel_names, .channel_map, .ylabel 6 | %Yohann Thenaisie 26.10.2018 7 | 8 | channelsFig = figure(); 9 | 10 | ax = gobjects(channelParams.nChannels, 1); 11 | [nColumns, nRows] = size(channelParams.channel_map); 12 | for chId = 1:channelParams.nChannels 13 | channel_pos = find(channelParams.channel_map == chId); 14 | ax(chId) = subplot(nRows, nColumns, channel_pos); 15 | hold on 16 | plot(channelParams.time, data(:, chId)) 17 | title(channelParams.channel_names{chId}) 18 | xlim([channelParams.time(1) channelParams.time(end)]) 19 | grid on 20 | minY = min(data(:, chId)); 21 | maxY = max(data(:, chId)); 22 | if minY ~= maxY 23 | ylim([minY maxY]) 24 | end 25 | end 26 | subplot(nRows, nColumns, channelParams.nChannels-nColumns+1) 27 | xlabel('Time (s)') 28 | ylabel(channelParams.ylabel) 29 | linkaxes(ax, 'x') -------------------------------------------------------------------------------- /plot/plotPwelch.m: -------------------------------------------------------------------------------- 1 | function [Pxx, F] = plotPwelch(data, params, varargin) 2 | % function pwelchFig = plotPwelch(data, LFP, 'log') 3 | %'log' converts PSD to dB 4 | %Plot Pwelch (PSD along frequencies) for each channel 5 | %Yohann Thenaisie 25.20.2018 6 | 7 | %Compute pWelch with 0.1Hz frequency resolution 8 | window = round(5*params.Fs); %default 9 | noverlap = round(window*0.6); %default 10 | freqResolution = 0.1; %Hz 11 | fmin = 1; %Hz 12 | fmax = params.Fs/2; %Hz 13 | 14 | 15 | [Pxx, F] = pwelch(data, window, noverlap, fmin:freqResolution:fmax, params.Fs); 16 | 17 | %log scale PSD and confidence interval 18 | if nargin > 2 && strcmpi(varargin{1}, 'log') 19 | Pxx = 10*log10(Pxx); 20 | params.ylabel = 'PSD (dB/Hz)'; 21 | else 22 | params.ylabel = 'PSD (uV^2/Hz)'; 23 | end 24 | 25 | if params.nChannels == 1 26 | Pxx = Pxx'; 27 | end 28 | 29 | % figure; 30 | ax = gobjects(params.nChannels, 1); 31 | [nColumns, nRows] = size(params.channel_map); 32 | for chId = 1:params.nChannels 33 | channel_pos = find(params.channel_map == chId); 34 | ax(chId) = subplot(nRows, nColumns, channel_pos); 35 | hold on 36 | plot(F, Pxx(:, chId), 'LineWidth', 1) 37 | ylabel(params.ylabel) 38 | xlabel('Frequency (Hz)') 39 | title(params.channel_names(chId)) 40 | end 41 | linkaxes(ax, 'x') 42 | xlim([fmin fmax]) 43 | 44 | end 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PerceptToolbox 2 | MATLAB Toolbox to extract JSON files from Medtronic Percept PC neurostimulator. 3 | 4 | ## Instructions 5 | 6 | The loadJSON script extracts data from the following BrainSense recording modes: Setup, Streaming, Survey, Survey Indefinite Streaming and Timeline. 7 | When running loadJSON, select the JSON files to be extracted. 8 | Set correct4missingSamples as 'true' if LFP data needs to be synchronized to other devices (such as EMG or EEG recordings). 9 | 10 | Once extracted, you may load the raw LFP data and use the following functions: 11 | - plotChannels(LFP.data, LFP) plots raw signal for each channel 12 | - plotSpectrogram(LFP.data, LFP) plots spectrogram for each channel 13 | - plotPwelch(LFP.data, LFP) plots the power spectrum density for each channel 14 | 15 | In case a streaming has been disrupted (ie. a loading screen appeared on the tablet), the two recordings (before and after) disruption can be concatenated with the concatenateLFP script. 16 | 17 | Please cite our article when using our Toolbox: 18 | Towards adaptive deep brain stimulation: clinical and technical notes on a novel commercial device for chronic brain sensing, Thenaisie et al. J Neural Eng. 2021 19 | 20 | ## Updates 21 | - 24.09.2021 Bug fix in extractLFPTrendlogs 22 | - 27.09.2021 Allow direct selection of JSON files, bug fix in extractLFP 23 | - 07.12.2021 Bug fix in file selection 24 | - 01.02.2022 Bug fix in extractLFPTrendlogs 25 | 26 | ## Developped by 27 | Yohann Thenaisie - Lausanne University Hospital 28 | Barth Keulen - Leids Universitair Medisch Centrum 29 | Contact: yohann.thenaisie@chuv.ch 30 | -------------------------------------------------------------------------------- /plot/plotSpectrogram.m: -------------------------------------------------------------------------------- 1 | function spectroFig = plotSpectrogram(data, channelParams, varargin) 2 | %Computes and plot standard spectrogram for each channnel 3 | %'channelParams' is a structure with '.nChannels' and '.channel_map' and 4 | %'.channel_names' fields 5 | %Yohann Thenaisie 05.11.2018 6 | normalizePower = 0; 7 | 8 | %Default spectrogram parameters 9 | ax = gobjects(channelParams.nChannels, 1); 10 | window = round(0.5*channelParams.Fs); %0.5 second 11 | noverlap = round(window/2); 12 | fmin = 1; %Hz 13 | fmax = 125; %Hz 14 | 15 | spectroFig = figure(); 16 | [nColumns, nRows] = size(channelParams.channel_map); 17 | for chId = 1:channelParams.nChannels 18 | electrode_pos = find(channelParams.channel_map == chId); 19 | ax(chId) = subplot(nRows, nColumns, electrode_pos); 20 | hold on 21 | [~, f, t, p] = spectrogram(data(:, chId), window, noverlap, fmin:0.5:fmax, channelParams.Fs, 'yaxis'); 22 | 23 | if normalizePower == 1 24 | power2plot = 10*log10(p./mean(p, 2)); 25 | else 26 | power2plot = 10*log10(p); 27 | end 28 | 29 | imagesc(t, f, power2plot) 30 | ax_temp = gca; 31 | ax_temp.YDir = 'normal'; 32 | xlabel('Time (s)') 33 | ylabel('Frequency (Hz)') 34 | xlim([channelParams.time(1) channelParams.time(end)]) 35 | ylim([fmin fmax]) 36 | c = colorbar; 37 | c.Label.String = 'Power/Frequency (dB/Hz)'; 38 | cmax = max(quantile(power2plot, 0.9)); 39 | cmin = min(quantile(power2plot, 0.1)); 40 | caxis([cmin cmax]) 41 | title(channelParams.channel_names(chId)) 42 | 43 | end 44 | 45 | linkaxes(ax, 'xy') -------------------------------------------------------------------------------- /extract/correct4MissingSamples.m: -------------------------------------------------------------------------------- 1 | function LFP = correct4MissingSamples(LFP, TicksInS, GlobalPacketSizes) 2 | %Replace missing data by NaNs 3 | %Yohann Thenaisie 02.09.2020 4 | 5 | warning('Some samples were lost during this recording') 6 | 7 | %Create time vector of all samples that should have been received theorically 8 | time = TicksInS(1):1/LFP.Fs:TicksInS(end)+(GlobalPacketSizes(end)-1)/LFP.Fs; 9 | time = round(time,3); %round to the ms 10 | 11 | %Create logical vector indicating which samples have been received 12 | isReceived = zeros(size(time, 2), 1); 13 | nPackets = numel(GlobalPacketSizes); 14 | for packetId = 1:nPackets 15 | timeTicksDistance = abs(time - TicksInS(packetId)); 16 | [~, packetIdx] = min(timeTicksDistance); 17 | % %if min function does not locate the first min in vector 18 | % if packetIdx > 1 && (timeTicksDistance(packetIdx-1) == timeTicksDistance(packetIdx)) 19 | % packetIdx = packetIdx - 1; 20 | % end 21 | if isReceived(packetIdx) == 1 22 | packetIdx = packetIdx +1; 23 | end 24 | isReceived(packetIdx:packetIdx+GlobalPacketSizes(packetId)-1) = isReceived(packetIdx:packetIdx+GlobalPacketSizes(packetId)-1)+1; 25 | end 26 | figure; plot(isReceived, '.'); yticks([0 1]); yticklabels({'not received', 'received'}); ylim([-1 10]) 27 | 28 | % %If there are pseudo double-received samples, compensate non-received samples 29 | % doublesIdx = find(isReceived == 2); 30 | % missingIdx = find(isReceived == 0); 31 | % nDoubles = numel(doublesIdx); 32 | % for doubleId = 1:nDoubles 33 | % [~, idxOfidx] = min(abs(missingIdx - doublesIdx(doubleId))); 34 | % isReceived(missingIdx(idxOfidx)) = 1; 35 | % isReceived(doublesIdx(doubleId)) = 1; 36 | % end 37 | 38 | %Introduce NaN in data at the timepoints of discontinuities 39 | data = NaN(size(time, 2), LFP.nChannels); 40 | data(logical(isReceived), :) = LFP.data; 41 | 42 | LFP.data = data; 43 | LFP.time = time; 44 | -------------------------------------------------------------------------------- /extract/extractLFPMontage.m: -------------------------------------------------------------------------------- 1 | function extractLFPMontage(data, params) 2 | %Yohann Thenaisie 20.12.2020 3 | 4 | %Two data formats co-exist (structure or cell of structures) 5 | nRecordings = size(data.LFPMontage, 1); 6 | if ~iscell(data.LFPMontage) 7 | S = cell(nRecordings, 1); 8 | for recId = 1:nRecordings 9 | S{recId} = data.LFPMontage(recId); 10 | end 11 | data.LFPMontage = S; 12 | end 13 | 14 | %Extract LFP montage data 15 | LFPMontage.LFPFrequency = data.LFPMontage{1}.LFPFrequency; 16 | LFPMontage.LFPMagnitude = NaN(size(LFPMontage.LFPFrequency, 1), nRecordings); 17 | for recId = 1:nRecordings 18 | LFPMontage.hemisphere{recId} = afterPoint(data.LFPMontage{recId}.Hemisphere); 19 | LFPMontage.channel_names{recId} = strrep(afterPoint(data.LFPMontage{recId}.SensingElectrodes), '_', ' '); 20 | LFPMontage.LFPMagnitude(:, recId) = data.LFPMontage{recId}.LFPMagnitude; 21 | LFPMontage.ArtifactStatus{recId} = afterPoint(data.LFPMontage{recId}.ArtifactStatus); 22 | end 23 | 24 | %define savename 25 | recNames = data.LfpMontageTimeDomain(1).FirstPacketDateTime; 26 | savename = regexprep(char(recNames), {':', '-'}, {''}); 27 | savename = [savename(1:end-5) '_LFPMontage']; 28 | 29 | %plot LFP Montage 30 | LFPMontage.hemisphere = categorical(LFPMontage.hemisphere); 31 | hemisphereNames = unique(LFPMontage.hemisphere); 32 | nHemispheres = numel(hemisphereNames); 33 | channelsFig = figure; 34 | for hemisphereId = 1:nHemispheres 35 | subplot(nHemispheres, 1, hemisphereId) 36 | plot(LFPMontage.LFPFrequency, LFPMontage.LFPMagnitude(:, LFPMontage.hemisphere == hemisphereNames(hemisphereId))); 37 | xlabel('Frequency (Hz)'); ylabel('Power (uVp/rtHz)'); 38 | legend(LFPMontage.channel_names(LFPMontage.hemisphere == hemisphereNames(hemisphereId))) 39 | title(hemisphereNames(hemisphereId)) 40 | end 41 | savefig(channelsFig, [params.save_pathname filesep savename '_LFPMontage']); 42 | 43 | %save data 44 | save([params.save_pathname filesep savename '.mat'], 'LFPMontage') -------------------------------------------------------------------------------- /extract/extractStimAmp.m: -------------------------------------------------------------------------------- 1 | function extractStimAmp(data, params) 2 | %Yohann Thenaisie 24.09.2020 3 | 4 | %Extract parameters for this recording mode 5 | recordingMode = params.recordingMode; 6 | 7 | %Identify the different recordings 8 | nLines = size(data.(recordingMode), 1); 9 | FirstPacketDateTime = cell(nLines, 1); 10 | for lineId = 1:nLines 11 | FirstPacketDateTime{lineId, 1} = data.(recordingMode)(lineId).FirstPacketDateTime; 12 | end 13 | FirstPacketDateTime = categorical(FirstPacketDateTime); 14 | recNames = unique(FirstPacketDateTime); 15 | nRecs = numel(recNames); 16 | 17 | for recId = 1:nRecs 18 | 19 | commaIdx = regexp(data.(recordingMode)(recId).Channel, ','); 20 | nChannels = numel(commaIdx)+1; 21 | 22 | %Convert structure to arrays 23 | nSamples = size(data.(recordingMode)(recId).LfpData, 1); 24 | TicksInMs = NaN(nSamples, 1); 25 | mA = NaN(nSamples, nChannels); 26 | for sampleId = 1:nSamples 27 | TicksInMs(sampleId) = data.(recordingMode)(recId).LfpData(sampleId).TicksInMs; 28 | mA(sampleId, 1) = data.(recordingMode)(recId).LfpData(sampleId).Left.mA; 29 | mA(sampleId, 2) = data.(recordingMode)(recId).LfpData(sampleId).Right.mA; 30 | end 31 | 32 | %Make time start at 0 and convert to seconds 33 | TicksInS = (TicksInMs - TicksInMs(1))/1000; 34 | 35 | Fs = data.(recordingMode)(recId).SampleRateInHz; 36 | 37 | %Store LFP band power and stimulation amplitude in one structure 38 | stimAmp.data = mA; 39 | stimAmp.time = TicksInS; 40 | stimAmp.Fs = Fs; 41 | stimAmp.ylabel = 'Stimulation amplitude (mA)'; 42 | stimAmp.channel_names = {'Left', 'Right'}; 43 | stimAmp.firstTickInSec = TicksInMs(1)/1000; %first tick time (s) 44 | stimAmp.json = params.fname; 45 | 46 | %save name 47 | savename = regexprep(char(recNames(recId)), {':', '-'}, {''}); 48 | savename = [savename(1:end-5) '_' recordingMode '_stimAmp']; 49 | 50 | %Plot stimulation amplitude 51 | stimAmpFig = figure; plot(stimAmp.time, stimAmp.data, 'Linewidth', 2'); xlabel('Time (s)'); ylabel(stimAmp.ylabel); legend(stimAmp.channel_names); xlim([stimAmp.time(1) stimAmp.time(end)]); grid on 52 | savefig(stimAmpFig, [params.save_pathname filesep savename]); 53 | 54 | %save 55 | save([params.save_pathname filesep savename], 'stimAmp') 56 | disp([savename ' saved']) 57 | 58 | end -------------------------------------------------------------------------------- /plot/plotLFPTrendLogs.asv: -------------------------------------------------------------------------------- 1 | function h = plotLFPTrendLogs(LFPTrendLogs, ActiveGroup) 2 | %Plot LFP band power and stimulation amplitude accross time from LFPTrendLogs 3 | %Bart Keulen and Yohann Thenaisie 05.10.2020 4 | 5 | LFP = LFPTrendLogs.LFP; 6 | stimAmp = LFPTrendLogs.stimAmp; 7 | 8 | h = figure; 9 | ax = gobjects(LFP.nChannels+1, 1); 10 | for channelId = 1:LFP.nChannels 11 | 12 | ax(channelId) = subplot(LFP.nChannels+1,1,channelId); 13 | yyaxis left; plot(LFP.time, LFP.data(:,channelId)); ylabel(LFP.ylabel) 14 | yyaxis right; plot(stimAmp.time,stimAmp.data(:,channelId)); ylabel(stimAmp.ylabel); ylim([0 max(stimAmp.data(:,channelId))+0.5]) 15 | title(LFP.channel_names(channelId)); 16 | 17 | if isfield(LFPTrendLogs, 'events') 18 | hold on 19 | 20 | %discard events that have been marked out of the LFP recording period 21 | events = LFPTrendLogs.events(LFPTrendLogs.events.DateTime > LFPTrendLogs.LFP.time(1) & LFPTrendLogs.events.DateTime < LFPTrendLogs.LFP.time(end), :); 22 | 23 | %Plot all events of each type at once 24 | eventIDs = unique(events.EventID); 25 | nEventIDs = size(eventIDs, 1); 26 | colors = lines(nEventIDs); 27 | for eventId = 1:nEventIDs 28 | event_DateTime = events.DateTime(events.EventID == eventIDs(eventId)); 29 | yyaxis left; plot([event_DateTime event_DateTime], [0 max(LFP.data(:,channelId))], '--', 'Color', colors(eventId, :)); 30 | end 31 | 32 | %Plot legend for events - to be worked on 33 | lgd = legend(unique(events.EventName)); 34 | title(lgd,'Events'); 35 | end 36 | 37 | end 38 | 39 | %Last subplot displays ActiveGroups 40 | ax(LFP.nChannels+1) = subplot(LFP.nChannels+1,1,LFP.nChannels+1); 41 | hold on 42 | ActiveGroup.NewGroupId = categorical(ActiveGroup.NewGroupId); 43 | groupNames = unique(ActiveGroup.NewGroupId); 44 | color = {'r', 'g', 'b', 'y'}; 45 | nGroupChanges = size(ActiveGroup, 1); 46 | for groupChange = 1:nGroupChanges 47 | groupId = ActiveGroup.NewGroupId(groupChange) == groupNames; 48 | if groupChange == nGroupChanges 49 | stopGroup = LFPTrendLogs.LFP.time(end); 50 | else 51 | stopGroup = ActiveGroup.DateTime(groupChange+1); 52 | end 53 | area([ActiveGroup.DateTime(groupChange), stopGroup],[1, 1],'facecolor',color{groupId}, ... 54 | 'facealpha', 0.2,'edgecolor','none'); 55 | end 56 | title('Active Group') 57 | ax1.XAxis.Visible = 'off'; 58 | 59 | xlabel(LFP.xlabel); 60 | linkaxes(ax, 'x') 61 | xlim([min(LFP.time) max(LFP.time)]) 62 | % sgtitle({'LFP Trend Logs', regexprep(LFPTrendLogs.json(1:end-5),'_',' ')}) -------------------------------------------------------------------------------- /plot/plotLFPTrendLogs.m: -------------------------------------------------------------------------------- 1 | function h = plotLFPTrendLogs(LFPTrendLogs, ActiveGroup) 2 | %Plot LFP band power and stimulation amplitude accross time from LFPTrendLogs 3 | %Bart Keulen and Yohann Thenaisie 05.10.2020 4 | 5 | LFP = LFPTrendLogs.LFP; 6 | stimAmp = LFPTrendLogs.stimAmp; 7 | 8 | h = figure; 9 | ax = gobjects(LFP.nChannels+1, 1); 10 | for channelId = 1:LFP.nChannels 11 | 12 | ax(channelId) = subplot(LFP.nChannels+1,1,channelId); 13 | yyaxis left; plot(LFP.time, LFP.data(:,channelId)); ylabel(LFP.ylabel) 14 | yyaxis right; plot(stimAmp.time,stimAmp.data(:,channelId)); ylabel(stimAmp.ylabel); ylim([0 max(stimAmp.data(:,channelId))+0.5]) 15 | title(LFP.channel_names(channelId)); 16 | 17 | if isfield(LFPTrendLogs, 'events') 18 | hold on 19 | 20 | %discard events that have been marked out of the LFP recording period 21 | events = LFPTrendLogs.events(LFPTrendLogs.events.DateTime > LFPTrendLogs.LFP.time(1) & LFPTrendLogs.events.DateTime < LFPTrendLogs.LFP.time(end), :); 22 | 23 | %Plot all events of each type at once 24 | eventIDs = unique(events.EventID); 25 | nEventIDs = size(eventIDs, 1); 26 | colors = lines(nEventIDs); 27 | for eventId = 1:nEventIDs 28 | event_DateTime = events.DateTime(events.EventID == eventIDs(eventId)); 29 | yyaxis left; plot([event_DateTime event_DateTime], [0 max(LFP.data(:,channelId))], '--', 'Color', colors(eventId, :)); 30 | end 31 | 32 | %Plot legend for events - to be worked on 33 | lgd = legend(unique(events.EventName)); 34 | title(lgd,'Events'); 35 | end 36 | 37 | end 38 | 39 | %Last subplot displays ActiveGroups 40 | ax(LFP.nChannels+1) = subplot(LFP.nChannels+1,1,LFP.nChannels+1); 41 | hold on 42 | ActiveGroup.NewGroupId = categorical(ActiveGroup.NewGroupId); 43 | groupNames = unique(ActiveGroup.NewGroupId); 44 | color = {'r', 'g', 'b', 'y'}; 45 | nGroupChanges = size(ActiveGroup, 1); 46 | for groupChange = 1:nGroupChanges 47 | groupId = ActiveGroup.NewGroupId(groupChange) == groupNames; 48 | if groupChange == nGroupChanges 49 | stopGroup = LFPTrendLogs.LFP.time(end); 50 | else 51 | stopGroup = ActiveGroup.DateTime(groupChange+1); 52 | end 53 | area([ActiveGroup.DateTime(groupChange), stopGroup],[1, 1],'facecolor',color{groupId}, ... 54 | 'facealpha', 0.2,'edgecolor','none'); 55 | end 56 | title('Active Group') 57 | ax(LFP.nChannels+1).YAxis.Visible = 'off'; 58 | 59 | xlabel(LFP.xlabel); 60 | linkaxes(ax, 'x') 61 | xlim([min(LFP.time) max(LFP.time)]) 62 | % sgtitle({'LFP Trend Logs', regexprep(LFPTrendLogs.json(1:end-5),'_',' ')}) -------------------------------------------------------------------------------- /loadJSON.m: -------------------------------------------------------------------------------- 1 | %LOADJSON loads JSON files, extracts, saves and plots BrainSense, Setup, 2 | %Survey, Indefinite Streaming and Timeline data in one folder per session 3 | %Yohann Thenaisie 02.09.2020 - Lausanne University Hospital (CHUV) 4 | 5 | %Set pathname to the Percept Toolbox 6 | addpath(genpath('C:\Users\BSI\Dropbox (NeuroRestore)\Percept\PerceptToolbox')) 7 | 8 | %Select JSON files to load 9 | [filenames, data_pathname] = uigetfile('*.json', 'MultiSelect', 'on'); 10 | cd(data_pathname) 11 | 12 | nFiles = size(filenames, 2); 13 | for fileId = 1:nFiles 14 | 15 | %Load JSON file 16 | if iscell(filenames) 17 | fname = filenames{fileId}; 18 | else 19 | fname = filenames(fileId, :); 20 | end 21 | data = jsondecode(fileread(fname)); 22 | 23 | %Create a new folder per JSON file 24 | params.fname = fname; 25 | params.SessionDate = regexprep(data.SessionDate, {':', '-'}, {''}); 26 | params.save_pathname = [data_pathname filesep params.SessionDate(1:end-1)]; 27 | mkdir(params.save_pathname) 28 | params.correct4MissingSamples = false; %set as 'true' if device synchronization is required 29 | params.ProgrammerVersion = data.ProgrammerVersion; 30 | 31 | if isfield(data, 'IndefiniteStreaming') %Survey Indefinite Streaming 32 | 33 | params.recordingMode = 'IndefiniteStreaming'; 34 | params.nChannels = 6; 35 | params.channel_map = [1 2 3 ; 4 5 6]; 36 | 37 | extractLFP(data, params) 38 | 39 | end 40 | 41 | if isfield(data, 'BrainSenseTimeDomain') %Streaming 42 | 43 | params.nChannels = 2; 44 | params.channel_map = 1:params.nChannels; 45 | 46 | params.recordingMode = 'BrainSenseTimeDomain'; 47 | extractLFP(data, params) 48 | params.recordingMode = 'BrainSenseLfp'; 49 | extractStimAmp(data, params) 50 | 51 | end 52 | 53 | if isfield(data, 'SenseChannelTests') %Setup OFF stimulation 54 | 55 | params.recordingMode = 'SenseChannelTests'; 56 | params.nChannels = 6; 57 | params.channel_map = [1 2 3 ; 4 5 6]; 58 | 59 | extractLFP(data, params) 60 | 61 | end 62 | 63 | if isfield(data, 'CalibrationTests') %Setup ON stimulation 64 | 65 | params.recordingMode = 'CalibrationTests'; 66 | params.nChannels = 2; 67 | params.channel_map = [1 2]; 68 | 69 | extractLFP(data, params) 70 | 71 | end 72 | 73 | if isfield(data, 'LFPMontage') %Survey 74 | 75 | %Extract and save LFP Montage PSD 76 | extractLFPMontage(data, params) 77 | 78 | %Extract and save LFP Montage Time Domain 79 | params.recordingMode = 'LfpMontageTimeDomain'; 80 | params.nChannels = 6; 81 | params.channel_map = [1 2 3 ; 4 5 6]; 82 | extractLFP(data, params); 83 | 84 | end 85 | 86 | if ~isempty(data.MostRecentInSessionSignalCheck) %Setup 87 | 88 | SignalCheck = data.MostRecentInSessionSignalCheck; 89 | save([params.save_pathname filesep 'SignalCheck'], 'SignalCheck') 90 | 91 | h = figure; hold on 92 | channel_names = cell(6, 1); 93 | for chId = 1:6 94 | plot(SignalCheck(chId).SignalFrequencies, SignalCheck(chId).SignalPsdValues) 95 | channel_names{chId} = SignalCheck(chId).Channel(19:end); 96 | end 97 | xlabel('Frequency (Hz)') 98 | ylabel('uVp/rtHz') 99 | legend(channel_names) 100 | savefig(h, [params.save_pathname filesep 'SignalCheck']) 101 | disp('SignalCheck saved') 102 | 103 | end 104 | 105 | if isfield(data, 'DiagnosticData') && isfield(data.DiagnosticData, 'LFPTrendLogs') %Timeline and Events 106 | 107 | params.recordingMode = 'LFPTrendLogs'; 108 | extractTrendLogs(data, params) 109 | 110 | end 111 | 112 | end 113 | -------------------------------------------------------------------------------- /extract/extractLFP.m: -------------------------------------------------------------------------------- 1 | function extractLFP(data, params) 2 | %Yohann Thenaisie 04.09.2020 3 | 4 | %Extract parameters for this recording mode 5 | recordingMode = params.recordingMode; 6 | nChannels = params.nChannels; 7 | fname = params.fname; 8 | 9 | %Identify the different recordings 10 | nLines = size(data.(recordingMode), 1); 11 | FirstPacketDateTime = cell(nLines, 1); 12 | for lineId = 1:nLines 13 | FirstPacketDateTime{lineId, 1} = data.(recordingMode)(lineId).FirstPacketDateTime; 14 | end 15 | FirstPacketDateTime = categorical(FirstPacketDateTime); 16 | recNames = unique(FirstPacketDateTime); 17 | nRecs = numel(recNames); 18 | 19 | %Extract LFPs in a new structure for each recording 20 | for recId = 1:nRecs 21 | 22 | datafield = data.(recordingMode)(FirstPacketDateTime == recNames(recId)); 23 | 24 | LFP = struct; 25 | LFP.nChannels = size(datafield, 1); 26 | if LFP.nChannels ~= nChannels 27 | warning(['There are ' num2str(LFP.nChannels) ' instead of the expected ' num2str(nChannels) ' channels']) 28 | end 29 | LFP.channel_names = cell(1, LFP.nChannels); 30 | LFP.data = []; 31 | for chId = 1:LFP.nChannels 32 | LFP.channel_names{chId} = strrep(datafield(chId).Channel, '_', ' '); 33 | LFP.data(:, chId) = datafield(chId).TimeDomainData; 34 | end 35 | LFP.Fs = datafield(chId).SampleRateInHz; 36 | 37 | %Extract size of received packets 38 | GlobalPacketSizes = str2num(datafield(1).GlobalPacketSizes); %#ok 39 | if sum(GlobalPacketSizes) ~= size(LFP.data, 1) && ~strcmpi(recordingMode, 'SenseChannelTests') && ~strcmpi(recordingMode, 'CalibrationTests') 40 | warning([recordingMode ': data length (' num2str(size(LFP.data, 1)) ' samples) differs from the sum of packet sizes (' num2str(sum(GlobalPacketSizes)) ' samples)']) 41 | end 42 | 43 | %Extract timestamps of received packets 44 | TicksInMses = str2num(datafield(1).TicksInMses); %#ok 45 | if ~isempty(TicksInMses) 46 | LFP.firstTickInSec = TicksInMses(1)/1000; %first tick time (s) 47 | end 48 | 49 | if ~isempty(TicksInMses) && params.correct4MissingSamples %TicksInMses is empty for SenseChannelTest 50 | TicksInS = (TicksInMses - TicksInMses(1))/1000; %convert to seconds and initiate at 0 51 | 52 | %If there are more ticks in data packets, ignore extra ticks 53 | nPackets = numel(GlobalPacketSizes); 54 | nTicks = numel(TicksInS); 55 | if nPackets ~= nTicks 56 | warning('GlobalPacketSizes and TicksInMses have different lengths') 57 | 58 | maxPacketId = max([nPackets, nTicks]); 59 | nSamples = size(LFP.data, 1); 60 | 61 | %Plot 62 | figure; 63 | ax(1) = subplot(2, 1, 1); plot(TicksInS, '.'); xlabel('Data packet ID'); ylabel('TicksInS'); xlim([0 max([nPackets nTicks])]) 64 | ax(2) = subplot(2, 1, 2); plot(cumsum(GlobalPacketSizes), '.'); xlabel('Data packet ID'); ylabel('Cumulated sum of samples received'); xlim([0 max([nPackets nTicks])]); 65 | hold on; plot([0 maxPacketId], [nSamples, nSamples], '--') 66 | linkaxes(ax, 'x') 67 | 68 | TicksInS = TicksInS(1:nPackets); 69 | 70 | end 71 | 72 | %Check if some ticks are missing 73 | isDataMissing = logical(TicksInS(end) >= sum(GlobalPacketSizes)/LFP.Fs); 74 | 75 | if isDataMissing 76 | LFP = correct4MissingSamples(LFP, TicksInS, GlobalPacketSizes); 77 | end 78 | 79 | end 80 | 81 | LFP.time = (1:length(LFP.data))/LFP.Fs; % [s] 82 | if LFP.nChannels <= 2 83 | LFP.channel_map = 1:LFP.nChannels; 84 | else 85 | LFP.channel_map = params.channel_map; 86 | end 87 | LFP.xlabel = 'Time (s)'; 88 | LFP.ylabel = 'LFP (uV)'; 89 | LFP.json = fname; 90 | LFP.recordingMode = recordingMode; 91 | 92 | %save name 93 | savename = regexprep(char(recNames(recId)), {':', '-'}, {''}); 94 | savename = [savename(1:end-5) '_' recordingMode]; 95 | 96 | %Plot LFPs and save figure 97 | channelsFig = plotChannels(LFP.data, LFP); 98 | savefig(channelsFig, [params.save_pathname filesep savename '_LFP']); 99 | 100 | %Plot spectrogram and save figure 101 | if ~isempty(TicksInMses) && params.correct4MissingSamples && isDataMissing %cannot compute Fourier transform on NaN 102 | warning('Spectrogram cannot be computed as some samples are missing.') 103 | else 104 | spectroFig = plotSpectrogram(LFP.data, LFP); 105 | savefig(spectroFig, [params.save_pathname filesep savename '_spectrogram']); 106 | end 107 | 108 | %save LFPs 109 | save([params.save_pathname filesep savename '.mat'], 'LFP') 110 | disp([savename ' saved']) 111 | 112 | end -------------------------------------------------------------------------------- /extract/extractTrendLogs.m: -------------------------------------------------------------------------------- 1 | function extractTrendLogs(data, params) 2 | % Bart Keulen 4-10-2020 3 | % Modified by Yohann Thenaisie 05.10.2020 4 | 5 | % Extract parameters for this recording mode 6 | recordingMode = params.recordingMode; 7 | fname = params.fname; 8 | LFP.data = []; 9 | stimAmp.data = []; 10 | 11 | % Extract recordings left and right 12 | hemisphereLocationNames = fieldnames(data.DiagnosticData.LFPTrendLogs); 13 | nHemisphereLocations = numel(hemisphereLocationNames); 14 | 15 | for hemisphereId = 1:nHemisphereLocations 16 | 17 | data_hemisphere = data.DiagnosticData.LFPTrendLogs.(hemisphereLocationNames{hemisphereId}); 18 | 19 | recFields = fieldnames(data_hemisphere); 20 | nRecs = numel(recFields); 21 | allDays = table; 22 | 23 | %Concatenate data accross days 24 | for recId = 1:nRecs 25 | 26 | datafield = struct2table(data_hemisphere.(recFields{recId})); 27 | allDays = [allDays; datafield]; %#ok<*AGROW> 28 | 29 | end 30 | 31 | allDays = sortrows(allDays, 1); 32 | 33 | LFP.data = [LFP.data allDays.LFP]; 34 | stimAmp.data = [stimAmp.data allDays.AmplitudeInMilliAmps]; 35 | 36 | end 37 | 38 | %Extract LFP, stimulation amplitude and date-time information 39 | % DateTime = cellfun(@(x) datetime(regexprep(x(1:end-1),'T',' ')), allDays.DateTime); 40 | nTimepoints = size(allDays, 1); 41 | for recId = 1:nTimepoints 42 | DateTime(recId) = datetime(regexprep(allDays.DateTime{recId}(1:end-1),'T',' ')); 43 | end 44 | 45 | % Store LFP in a structure 46 | LFP.time = DateTime; 47 | LFP.nChannels = nHemisphereLocations; 48 | LFP.channel_names = hemisphereLocationNames; 49 | LFP.xlabel = 'Date Time'; 50 | LFP.ylabel = 'LFP band power'; 51 | 52 | %Store stimAmp in a structure 53 | stimAmp.time = DateTime; 54 | stimAmp.nChannels = nHemisphereLocations; 55 | stimAmp.channel_names = hemisphereLocationNames; 56 | stimAmp.xlabel = 'Date Time'; 57 | stimAmp.ylabel = 'Stimulation amplitude [mA]'; 58 | 59 | %Store all information in one structure 60 | LFPTrendLogs.LFP = LFP; 61 | LFPTrendLogs.stimAmp = stimAmp; 62 | LFPTrendLogs.json = fname; 63 | LFPTrendLogs.recordingMode = recordingMode; 64 | 65 | %If patient has marked events, extract them 66 | if isfield(data.DiagnosticData, 'LfpFrequencySnapshotEvents') 67 | data_events = data.DiagnosticData.LfpFrequencySnapshotEvents; 68 | nEvents = size(data_events, 1); 69 | events = table('Size',[nEvents 6],'VariableTypes',... 70 | {'cell', 'double', 'cell', 'logical', 'logical', 'cell'},... 71 | 'VariableNames',{'DateTime','EventID','EventName','LFP','Cycling', 'LfpFrequencySnapshotEvents'}); 72 | for eventId = 1:nEvents 73 | if iscell(data_events) %depending on software version 74 | thisEvent = struct2table(data_events{eventId}, 'AsArray', true); 75 | else 76 | thisEvent = struct2table(data_events(eventId), 'AsArray', true); 77 | end 78 | events(eventId, 1:5) = thisEvent(:, 1:5); %remove potential 'LfpFrequencySnapshotEvents' 79 | if ismember('LfpFrequencySnapshotEvents', thisEvent.Properties.VariableNames) 80 | for hemisphereId = 1:nHemisphereLocations 81 | PSD.FFTBinData(:, hemisphereId) = thisEvent.LfpFrequencySnapshotEvents.(hemisphereLocationNames{hemisphereId}).FFTBinData; 82 | PSD.channel_names{hemisphereId} = [hemisphereLocationNames{hemisphereId}(23:end), ' ' thisEvent.LfpFrequencySnapshotEvents.(hemisphereLocationNames{hemisphereId}).SenseID(27:end)]; 83 | end 84 | PSD.Frequency = thisEvent.LfpFrequencySnapshotEvents.(hemisphereLocationNames{hemisphereId}).Frequency; 85 | PSD.nChannels = nHemisphereLocations; 86 | events.LfpFrequencySnapshotEvents{eventId} = PSD; 87 | end 88 | end 89 | events.DateTime = cellfun(@(x) datetime(regexprep(x(1:end-1),'T',' ')), events.DateTime); 90 | LFPTrendLogs.events = events; 91 | end 92 | 93 | %Has the stimulation/recording group been changed? 94 | % GroupHistory = struct2table(data.GroupHistory); 95 | % GroupHistory.SessionDate = cellfun(@(x) datetime(regexprep(x(1:end-1),'T',' ')), GroupHistory.SessionDate); 96 | EventLogs = data.DiagnosticData.EventLogs; 97 | nEventLogs = size(EventLogs, 1); 98 | ActiveGroup = []; 99 | for eventId = 1:nEventLogs 100 | if isfield(EventLogs{eventId}, 'NewGroupId') 101 | DateTime = datetime(regexprep(EventLogs{eventId}.DateTime(1:end-1),'T',' ')); 102 | OldGroupId = afterPoint(EventLogs{eventId}.OldGroupId); 103 | NewGroupId = afterPoint(EventLogs{eventId}.NewGroupId); 104 | 105 | %Find the stimulation and sensing settings of this new group 106 | Groups_PPS = data.Groups.Initial; 107 | if iscell(Groups_PPS) 108 | nGroups = size(Groups_PPS, 1); 109 | GroupParams = table('Size',[nGroups 4], 'VariableTypes',... 110 | {'string','logical','struct', 'struct'}, 'VariableNames',... 111 | {'GroupId', 'ActiveGroup', 'ProgramSettings', 'GroupSettings'}); 112 | for groupId = 1:nGroups 113 | GroupParams(groupId, :) = struct2table(Groups_PPS{groupId}); 114 | end 115 | else 116 | if size(Groups_PPS, 1) 117 | GroupParams = struct2table(Groups_PPS, 'AsArray', true); 118 | else 119 | GroupParams = struct2table(Groups_PPS); 120 | end 121 | end 122 | GroupParams.GroupId = cellfun(@(x) afterPoint(x), GroupParams.GroupId, 'UniformOutput', false); 123 | NewProgramSettings = GroupParams.ProgramSettings(NewGroupId == categorical(GroupParams.GroupId)); 124 | 125 | %Create output table 126 | ActiveGroup_temp = cell2table({DateTime, OldGroupId, NewGroupId, NewProgramSettings},... 127 | 'VariableNames',{'DateTime' 'OldGroupId' 'NewGroupId' 'NewProgramSettings'}); 128 | ActiveGroup = [ActiveGroup; ActiveGroup_temp]; 129 | 130 | end 131 | end 132 | 133 | %Define savename 134 | savename = [params.SessionDate '_' recordingMode]; 135 | 136 | %Plot and save LFP trends 137 | channelsFig = plotLFPTrendLogs(LFPTrendLogs, ActiveGroup); 138 | savefig(channelsFig, [params.save_pathname filesep savename]); 139 | 140 | %Save TrendLogs in one file 141 | save([params.save_pathname filesep savename '.mat'], 'LFPTrendLogs', 'ActiveGroup') 142 | disp([savename ' saved']) 143 | 144 | end --------------------------------------------------------------------------------