Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
760156d
changes to identify ECG files via the point XML files
ExternalGuest Aug 30, 2023
d303acb
Modified files to handle the new situation with the ECG Export files …
ExternalGuest Aug 31, 2023
8b7492a
handle the new file format
ExternalGuest Aug 31, 2023
e1ba0ac
Changed `read_electrodePositionsOnAnnotation.m` file to respect diffe…
ExternalGuest Sep 1, 2023
a26b9d7
Compatibility issues with EP Workbench - edited batchConvert.m to add…
ExternalGuest Sep 9, 2023
280df0d
Added the function addForceData.m
ExternalGuest Nov 20, 2023
14937ee
Adjustments to file versions (StJude name has been removed).
nicklinton Nov 29, 2023
f9b28fc
fix for unipole naming - see https://openep.discourse.group/t/carto-n…
ExternalGuest Feb 12, 2024
26dc8dd
small fix to deal with situations where there is a mixture of electro…
ExternalGuest Feb 14, 2024
3d5712f
Changes to handle new data format; updates to comparestructure.m
ExternalGuest Aug 31, 2024
ec6dc9a
added addLocationPoints.m file
ExternalGuest Aug 31, 2024
b3ba055
fixed bug in setMesh
ExternalGuest Sep 1, 2024
66fc047
small change to batchConvert.m
ExternalGuest Dec 24, 2024
5a05831
further changes to Precision code
ExternalGuest Jan 21, 2025
f830725
no message
ExternalGuest Jun 23, 2025
a285d39
Added new importprecision_openep.m file to make it easier to import a…
ExternalGuest Jun 24, 2025
c7e3a4f
comment changed in importprecision_openep.m
ExternalGuest Jun 25, 2025
216531c
Fixed bug in batchConvert.m
ExternalGuest Jun 25, 2025
a6a223f
various fixes for importing data
ExternalGuest Jun 25, 2025
8315362
no message
ExternalGuest Sep 1, 2025
eef5262
significant update to ensite files - almost but not quite there.
ExternalGuest Sep 24, 2025
b33ac9c
working version
ExternalGuest Sep 25, 2025
c90e244
Added the nameFolds.m function
ExternalGuest Sep 25, 2025
7666dcc
Added routines to define the surface normals at the mapping points an…
ExternalGuest Oct 2, 2025
19d4b0f
minor change to spacing
ExternalGuest Oct 2, 2025
9cfa289
Working on building the robust EnsiteX importer
ExternalGuest Dec 11, 2025
699cb7f
updates to importensitex_openep.m - working as far as line 780
ExternalGuest Dec 12, 2025
a1446da
Further work on importensitex_openep; updated loadensitex_dxldata to …
ExternalGuest Dec 13, 2025
f7038e8
further changes
ExternalGuest Dec 13, 2025
21f4df0
no message
ExternalGuest Dec 13, 2025
bda9061
correct unipole name parsing
ExternalGuest Dec 13, 2025
f3ebac0
updated changes
ExternalGuest Dec 18, 2025
054a737
Added test function for loadensitex_dxldata.m, and added a parameter/…
ExternalGuest Dec 21, 2025
f7b5112
Added comment to drawMap file referring to the points for interpolati…
ExternalGuest Jan 25, 2026
1ef8582
fixes for area
ExternalGuest Mar 14, 2026
fc1e10a
Fix EnSiteX waveform parsing and CLI selection
KestutisMa Jun 29, 2026
08eba9e
Add mapping input and full-case validation
KestutisMa Jun 29, 2026
38d9a16
Support validated Study3 EnSiteX full-case imports
KestutisMa Jun 29, 2026
fef1e09
Add header-based EnSiteX export inspection
KestutisMa Jul 2, 2026
d7f8544
Use semantic EnSiteX file roles during import
KestutisMa Jul 2, 2026
6fcafce
Add multi-dataset OpenEP case containers
KestutisMa Jul 2, 2026
798ed62
Validate EnSiteX unipolar EGM layouts
KestutisMa Jul 3, 2026
0ef844a
Merge newfilestructure CARTO importer work
KestutisMa Jul 3, 2026
5fbc4ae
Make CARTO archive extraction size-aware
KestutisMa Jul 3, 2026
1ed3168
Make CARTO imports batch-testable
KestutisMa Jul 3, 2026
1a39566
Use native unzip for large CARTO archives
KestutisMa Jul 3, 2026
e16da34
Add opt-in full CARTO importer regression
KestutisMa Jul 3, 2026
f8c42e1
Fix EnSite full-case point count assertion
KestutisMa Jul 3, 2026
6d9f0d7
Add headless mapping conversion contract
KestutisMa Jul 5, 2026
67a4dc3
Document ARO mapping importer integration
KestutisMa Jul 5, 2026
069c022
Test production mapping conversion workflows
KestutisMa Jul 5, 2026
d7647db
Add conversion progress reporting and demo workflow
KestutisMa Jul 6, 2026
9dbe4bd
Rename integration docs for TRE workflows
KestutisMa Jul 7, 2026
2400877
Document TRE conversion status contract
KestutisMa Jul 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# OpenEP

OpenEP supports the import and analysis of electroanatomic mapping data in Matlab

For headless CARTO and EnSiteX conversion, validation, status reporting and
TRE integration, see [docs/tre_importer_integration.md](docs/tre_importer_integration.md).
17 changes: 17 additions & 0 deletions addForceData.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
function userdata = addForceData(userdata)

time = zeros(size(userdata.rf.originaldata.ablparams.time));
force = zeros(size(userdata.rf.originaldata.ablparams.time));
axialangle = zeros(size(userdata.rf.originaldata.ablparams.time));
lateralangle = zeros(size(userdata.rf.originaldata.ablparams.time));
position = zeros(size(userdata.rf.originaldata.ablparams.time));

forcedata.time = time;
forcedata.force = force;
forcedata.axialangle = axialangle;
forcedata.lateralangle = lateralangle;
forcedata.position = position;

userdata.rf.originaldata.force = forcedata;

end
55 changes: 55 additions & 0 deletions addLocationPoints.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
function userdata = addLocationPoints(userdata, X, tags, names)
% ADDLOCATIONPOINTS Manually add location points to data
%
% Usage:
% userdata = addLocationPoints(userdata, X, tags, names)
% Where:
% userdata - the OpenEP data structure
% X - locations
% tags - tags
% names - names
%
% addLocationPoints does not accept parameter value pairs
%
% ADDLOCATIONPOINTS Manually add location points to data
%
% Author: Steven Williams (2024)
% Modifications -
%
% Info on Code Testing:
% ---------------------------------------------------------------
% % copy and paste co-ordinates from a CSV file
% tags = cell(size(X,1), 1);
% tags(:) = {'ablation'};
% names = cell(size(X,1), 1);
% names(:) = {'abl'};
% userdata = addLocationPoints(userdata, X, tags, names)
% ---------------------------------------------------------------
%
% ---------------------------------------------------------------
% code
% ---------------------------------------------------------------

numPointsToAdd = size(X, 1);

for i = 1:numPointsToAdd
userdata.electric.tags{end+1} = tags{i};
userdata.electric.names{end+1} = [names{i} num2str(i)];
userdata.electric.egmX(end+1,:) = X(i,:);
userdata.electric.egm(end+1,:) = NaN;
userdata.electric.egmRef(end+1,:) = NaN(1,1000);
userdata.electric.annotations.woi(end+1,:) = [NaN NaN];
userdata.electric.annotations.referenceAnnot(end+1,:) = NaN;
userdata.electric.annotations.mapAnnot(end+1,:) = NaN;
userdata.electric.voltages.bipolar(end+1,:) = NaN;

tr = getMesh(userdata, 'type', 'triangulation');
surfX = findclosestvertex(tr, X(i,:), true);
userdata.electric.egmSurfX(end+1,:) = userdata.surface.triRep.X(surfX,:);
userdata.electric.LATs(end+1,:) = NaN;
userdata.electric.electrodeNames_uni(end+1,:) = NaN;
userdata.electric.include(end+1) = false;
userdata.electric.discarded(end+1) = true;


end
21 changes: 20 additions & 1 deletion batchConvert.m
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,30 @@ function batchConvert(inputDir, outputDir)
% Store comment about what we have done
userdata.notes{end+1} = [date ': data set converted using batchConvert.m'];

% If force data does not exist, add it please
if isfield(userdata, 'rf')
if isempty(userdata.rf)
userdata = addForceData(userdata);
end
if ~isfield(userdata.rf.originaldata, 'force')
userdata = addForceData(userdata);
disp(' ... fake force data added')
userdata.notes{end+1} = [date ': fake forcedata added during batchConvert.m'];
end
else
userdata.rf = [];
end

% If force data does not exist, add it please
if isempty(userdata.surface.uni_imp_frc)
userdata.surface.uni_imp_frc = NaN(size(userdata.surface.triRep.X));
end

% We save as -v7 because it's faster to load in OpenEP-py than -v7.3,
% and the saved file is significantly smaller compared to -v6 files.
outputFile = [outputDir filesep() allFiles{i}];
disp(['saving file: ' outputFile])
save(outputFile, 'userdata', '-v7');
save(outputFile, 'userdata', '-v7.3');

end

Expand Down
37 changes: 37 additions & 0 deletions calculatePeak2PeakVoltage.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
function V = calculatePeak2PeakVoltage( egms, refAnnot, woi )
% CALCULATEPEAK2PEAKVOLTAGE Calcualtes peak to peak voltages
%
% Usage:
% V = calculatePeak2PeakVoltage( egms, refAnnot, woi )
% Where:
% egms - the electrograms
% refAnnot - the reference annotation, in samples
% woi - the window of interest, relative to the refAnnot, in samples
% V - the output voltages
%
% CALCULATEPEAK2PEAKVOLTAGE does not accept parameter-value pairs
%
% CALCULATEPEAK2PEAKVOLTAGE Detailed description goes here
%
% Author: Steven Williams (2025)
% Modifications -
%
% Info on Code Testing:
% ---------------------------------------------------------------
% test code
% ---------------------------------------------------------------
%
% ---------------------------------------------------------------
% code
% ---------------------------------------------------------------

nEgm = size(egms,1);
V = NaN(nEgm,1);
for iEgm = 1:nEgm

sampleRange = refAnnot(iEgm)+woi(iEgm,1):refAnnot(iEgm)+woi(iEgm,2);
thisEgm = egms(iEgm, sampleRange);
V(iEgm) = max(thisEgm) - min(thisEgm);

end
end
5 changes: 3 additions & 2 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,13 @@ This section documents changes which have been merged into develop and will form
- importcarto_mem.m
- no longer has to check the electrode name for each point as this is
taken from read_ecgfile_v4
- ability to read latest Carto data (new format without redundancy)


### Fixed
- Fixed issue with multiple colour bars
- Fixed issue #70 for setting tolerance when identifying in/out points in the mesh
- Geodesic distance calculator now configured for both Ubuntu and MacOSX platforms
- Tidied importcarto_mem default directories
- Sped up xml reading
- Sped up read_meshfile
- Sped up read_meshfile
- Fixed bug in getEarliestActivationSite/getLatestActivationSite when handling datasets written by EP Workbench
219 changes: 111 additions & 108 deletions comparestructure.m
Original file line number Diff line number Diff line change
@@ -1,108 +1,111 @@
function [result, commentary] = comparestructure(a,b)
% COMPARESTRUCTURE.
% Usage:
% [result, commentary] = comparestructure(a,b)
% Inputs:
% a,b - input structures to compare
% Outputs:
% result - logical
% commentary - a cellarray of text messages explaining the differences
%
% Author: Nick Linton (2023)
% Modifications -

% Info on Code Testing:
% ---------------------
% test code
% ---------------------

% ---------------------------------------------------------------
% code
% ---------------------------------------------------------------


local_addmsg(0, 'reset');

if isequal(a,b)
result = true;
commentary = '';
return
else
result = false;
local_compare(a,b,0)
commentary = local_addmsg(0, 'retrieve');
end
end

function local_compare(a,b,level)
fieldnamesA = sort(fieldnames(a));
fieldnamesB = sort(fieldnames(b));
fieldNamesAll = unique([fieldnamesA;fieldnamesB]);

badNamesA = false(size(fieldNamesAll));
badNamesB = false(size(fieldNamesAll));
if ~isequal(fieldnamesA, fieldnamesB)
for i = 1:numel(fieldNamesAll)
if ~any(matches(fieldnamesB, fieldNamesAll{i}))
badNamesA(i) = true;
local_addmsg(['.' fieldnamesA{i} ' is not in the second structure']);
end
if ~any(matches(fieldnamesA, fieldNamesAll{i}))
badNamesB(i) = true;
local_addmsg(['.' fieldnamesA{i} ' is not in the second structure']);
end
end
end

level = level + 1;
% now go through the fieldNames that are shared.
sharedFieldNames = fieldNamesAll(~badNamesA & ~badNamesB);
for i = 1:numel(sharedFieldNames)
f = sharedFieldNames{i};
if ~strcmp(class(a.(f)),class(b.(f)))
local_addmsg(level, ['.' f ' does not have the same class']);
else
switch class(a.(f))
case 'struct'
local_addmsg(level, ['.' f ]);
local_compare(a.(f),b.(f),level)
otherwise
aData = a.(f);
bData = b.(f);
isSizeOK = isequal(size(a.(f)),size(b.(f)));
isNumelOK = isequal(numel(a.(f)),numel(b.(f)));
isContentOK = false;
if isNumelOK
isContentOK = isequaln(a.(f)(:),b.(f)(:));
end
if isSizeOK && isContentOK
% do nothing - all good
local_addmsg(level, ['.' f ' - correct']);
elseif ~isContentOK
local_addmsg(level, ['.' f ' - &&&ERROR&&& has different CONTENT']);
else
local_addmsg(level, ['.' f ' - &&&ERROR&&& has different DIMENSIONS but similar content']);
end
end
end
end
end

function msgAll = local_addmsg(level, msg)
persistent msgStore

verbose = true;
switch msg
case 'reset'
msgStore = {};
case 'retrieve'
msgAll = msgStore;
otherwise
indent = repmat(' ',1,level);
if verbose
disp([indent,msg])
end
msgStore = [msgStore; [indent,msg]];
end
msgAll = msgStore;
end
function [result, commentary] = comparestructure(a,b)
% COMPARESTRUCTURE.
% Usage:
% b = myfunction(a)
% Inputs:
% a - input
% Outputs:
% b - output
%
% MYFUNCTION detailed description.
%
% Author: Nick Linton (2021)
% Modifications -

% Info on Code Testing:
% ---------------------
% test code
% ---------------------

% ---------------------------------------------------------------
% code
% ---------------------------------------------------------------


local_addmsg(0, 'reset');

if isequal(a,b)
result = true;
commentary = '';
return
else
result = false;
local_compare(a,b,0)
commentary = local_addmsg(0, 'retrieve');
end
end

function local_compare(a,b,level)
fieldnamesA = sort(fieldnames(a));
fieldnamesB = sort(fieldnames(b));
fieldNamesAll = unique([fieldnamesA;fieldnamesB]);

badNamesA = false(size(fieldNamesAll));
badNamesB = false(size(fieldNamesAll));
if ~isequal(fieldnamesA, fieldnamesB)
for i = 1:numel(fieldNamesAll)
if ~any(matches(fieldnamesB, fieldNamesAll{i}))
badNamesA(i) = true;
local_addmsg(level, ['.' fieldNamesAll{i} ' is not in the second structure']);
end
if ~any(matches(fieldnamesA, fieldNamesAll{i}))
badNamesB(i) = true;
local_addmsg(level, ['.' fieldNamesAll{i} ' is not in the first structure']);
end
end
else
local_addmsg(level, 'the order of the fields are different');
end

%level = level + 1;
% now go through the fieldNames that are shared.
sharedFieldNames = fieldNamesAll(~badNamesA & ~badNamesB);
for i = 1:numel(sharedFieldNames)
f = sharedFieldNames{i};
if ~strcmp(class(a.(f)),class(b.(f)))
local_addmsg(level, ['.' f ' does not have the same class']);
else
switch class(a.(f))
case 'struct'
local_addmsg(level, ['.' f ]);
local_compare(a.(f),b.(f),level+1)
otherwise
aData = a.(f);
bData = b.(f);
isSizeOK = isequal(size(a.(f)),size(b.(f)));
isNumelOK = isequal(numel(a.(f)),numel(b.(f)));
isContentOK = false;
if isNumelOK
isContentOK = isequaln(a.(f)(:),b.(f)(:));
end
if isSizeOK && isContentOK
% do nothing - all good
local_addmsg(level, ['.' f ' - correct']);
elseif ~isContentOK
local_addmsg(level, ['.' f ' - &&&ERROR&&& has different CONTENT']);
else
local_addmsg(level, ['.' f ' - &&&ERROR&&& has different DIMENSIONS but similar content']);
end
end
end
end
end

function msgAll = local_addmsg(level, msg)
persistent msgStore

verbose = true;
switch msg
case 'reset'
msgStore = {};
case 'retrieve'
msgAll = msgStore;
otherwise
indent = repmat(' ',1,level);
if verbose
disp([indent,msg])
end
msgStore = [msgStore; [indent,msg]];
end
msgAll = msgStore;
end
Loading
Loading