Skip to content

Commit 647572f

Browse files
Bugfix/SM-63 Empty signalmaps (#4)
* load empty signalMaps * Feature/SM-18: Linear connections writer (#5) * openCARP reader saves linear connection regions as separate data structure * faster write function for openCARP data * Feature/SM-45: Export CSV cell and histogram regions (#6) * include histogram and cell region in csv export * Bugfix/SM-86: Free boundaries as one actor (#8) * combine free boundaries into a single actor * not add rf to export openep if ablation == None (#9)
1 parent bd69270 commit 647572f

File tree

5 files changed

+126
-43
lines changed

5 files changed

+126
-43
lines changed

openep/data_structures/arrows.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@ class Arrows:
1313
fibres (np.ndarray): array of shape N_cells x 3
1414
divergence (np.ndarray): array of shape N_cells x 3
1515
linear_connections (np.ndarray): array of shape M x 3 (represents the linear connections between endo and epi)
16+
linear_connection_regions (np.ndarray): array of shape N_cells
1617
"""
1718

1819
# TODO: move divergence arrows into Arrows class
1920
# TODO: remove longitudinal and transversal arrows from Fields class
2021
fibres: np.ndarray = None
2122
divergence: np.ndarray = None
2223
linear_connections: np.ndarray = None
24+
linear_connection_regions: np.ndarray = None
2325

2426
def __repr__(self):
2527
return f"arrows: {tuple(self.__dict__.keys())}"
@@ -41,6 +43,13 @@ def __iter__(self):
4143
def __contains__(self, arrow):
4244
return arrow in self.__dict__.keys()
4345

46+
@property
47+
def linear_connection_regions_names(self):
48+
if self.linear_connection_regions is None:
49+
return []
50+
regions = np.unique(self.linear_connection_regions).astype(str)
51+
return regions.tolist()
52+
4453
def copy(self):
4554
"""Create a deep copy of Arrows"""
4655

openep/data_structures/surface.py

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class Fields:
3636
local_activation_time (np.ndarray): array of shape N_points
3737
impedance (np.ndarray): array of shape N_points
3838
force (np.ndarray): array of shape N_points
39-
region (np.ndarray): array of shape N_cells
39+
cell_region (np.ndarray): array of shape N_cells
4040
longitudinal_fibres (np.ndarray): array of shape N_cells x 3
4141
transverse_fibres (np.ndarray): array of shape N_cells x 3
4242
pacing_site (np.ndarray): array of shape N_points
@@ -54,6 +54,7 @@ class Fields:
5454
pacing_site: np.ndarray = None
5555
conduction_velocity: np.ndarray = None
5656
cv_divergence: np.ndarray = None
57+
histogram: np.ndarray = None
5758

5859
def __repr__(self):
5960
return f"fields: {tuple(self.__dict__.keys())}"
@@ -187,18 +188,22 @@ def extract_surface_data(surface_data):
187188
if isinstance(pacing_site, np.ndarray):
188189
pacing_site = None if pacing_site.size == 0 else pacing_site.astype(int)
189190

190-
try:
191-
conduction_velocity = surface_data['signalMaps']['conduction_velocity_field'].get('value', None)
192-
if isinstance(conduction_velocity, np.ndarray):
193-
conduction_velocity = None if conduction_velocity.size == 0 else conduction_velocity.astype(float)
194-
except KeyError:
195-
conduction_velocity = None
191+
if surface_data.get('signalMaps'):
192+
try:
193+
conduction_velocity = surface_data['signalMaps']['conduction_velocity_field'].get('value', None)
194+
if isinstance(conduction_velocity, np.ndarray):
195+
conduction_velocity = None if conduction_velocity.size == 0 else conduction_velocity.astype(float)
196+
except KeyError:
197+
conduction_velocity = None
196198

197-
try:
198-
cv_divergence = surface_data['signalMaps']['divergence_field'].get('value', None)
199-
if isinstance(cv_divergence, np.ndarray):
200-
cv_divergence = None if cv_divergence.size == 0 else cv_divergence.astype(float)
201-
except KeyError:
199+
try:
200+
cv_divergence = surface_data['signalMaps']['divergence_field'].get('value', None)
201+
if isinstance(cv_divergence, np.ndarray):
202+
cv_divergence = None if cv_divergence.size == 0 else cv_divergence.astype(float)
203+
except KeyError:
204+
cv_divergence = None
205+
else:
206+
conduction_velocity = None
202207
cv_divergence = None
203208

204209
fields = Fields(

openep/draw/draw_routines.py

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,10 @@ def draw_free_boundaries(
6363
width: int = 5,
6464
plotter: pyvista.Plotter = None,
6565
names: List[str] = None,
66+
combine: bool = False
6667
):
6768
"""
68-
Draw the freeboundaries of a mesh.
69+
Draw the free boundaries of a mesh.
6970
7071
Args:
7172
free_boundaries (FreeBoundary): `FreeBoundary` object. Can be generated using
@@ -76,28 +77,47 @@ def draw_free_boundaries(
7677
If None, a new plotting object will be created.
7778
names (List(str)): List of names to associated with the actors. Default is None, in which
7879
case actors will be called 'free_boundary_n', where n is the index of the boundary.
80+
combine (bool): Combines all free boundaries into one Actor (faster load time).
7981
8082
Returns:
8183
plotter (pyvista.Plotter): Plotting object with the free boundaries added.
82-
8384
"""
84-
85+
combined_lines = pyvista.PolyData() if combine else None
8586
plotter = pyvista.Plotter() if plotter is None else plotter
8687
colours = [colour] * free_boundaries.n_boundaries if isinstance(colour, str) else colour
88+
8789
if names is None:
8890
names = [f"free_boundary_{boundary_index:d}" for boundary_index in range(free_boundaries.n_boundaries)]
8991

9092
for boundary_index, boundary in enumerate(free_boundaries.separate_boundaries()):
9193

9294
points = free_boundaries.points[boundary[:, 0]]
9395
points = np.vstack([points, points[:1]]) # we need to close the loop
94-
plotter.add_lines(
95-
points,
96-
color=colours[boundary_index],
97-
width=width,
98-
name=names[boundary_index],
99-
connected=True
100-
)
96+
97+
# store the lines to be added in later
98+
if combine:
99+
lines = pyvista.lines_from_points(points)
100+
combined_lines = combined_lines.merge(lines)
101+
102+
# add each line one-by-one
103+
else:
104+
plotter.add_lines(
105+
points,
106+
color=colours[boundary_index],
107+
width=width,
108+
name=names[boundary_index],
109+
connected=True
110+
)
111+
112+
if combine:
113+
# add the combined lines as a single Actor manually (modified code of add_lines)
114+
actor = pyvista.Actor(mapper=pyvista.DataSetMapper(combined_lines))
115+
actor.prop.line_width = width
116+
actor.prop.show_edges = True
117+
actor.prop.edge_color = colour
118+
actor.prop.color = colour
119+
actor.prop.lighting = False
120+
plotter.add_actor(actor, reset_camera=False, name=names[0], pickable=False)
101121

102122
return plotter
103123

openep/io/readers.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,9 @@ def load_opencarp(
162162
points_data *= scale_points
163163
fibres_data = None if fibres is None else np.loadtxt(fibres)
164164

165-
indices_data, cell_region_data, linear_connection_data = [], [], []
165+
indices_data, cell_region_data = [], []
166+
linear_connection_data, linear_connection_regions = [], []
167+
166168
with open(indices) as elem_file:
167169
data = elem_file.readlines()
168170
for elem in data:
@@ -171,14 +173,18 @@ def load_opencarp(
171173
indices_data.append(list(map(int, parts[1:4])))
172174
cell_region_data.append(int(parts[4]))
173175
elif parts[0] == 'Ln':
174-
linear_connection_data.append(list(map(int, parts[1:4])))
176+
linear_connection_data.append(list(map(int, parts[1:3])))
177+
linear_connection_regions.append(int(parts[3]))
178+
175179
indices_data = np.array(indices_data)
176180
cell_region = np.array(cell_region_data)
177181
linear_connection_data = np.array(linear_connection_data)
182+
linear_connection_regions = np.array(linear_connection_regions)
178183

179184
arrows = Arrows(
180185
fibres=fibres_data,
181-
linear_connections=linear_connection_data
186+
linear_connections=linear_connection_data,
187+
linear_connection_regions=linear_connection_regions
182188
)
183189

184190
fields = Fields(

openep/io/writers.py

Lines changed: 61 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@
5656
import scipy.io
5757
import pathlib
5858

59-
6059
from openep.data_structures.ablation import Ablation
6160
from openep.data_structures.case import Case
6261
from openep.data_structures.surface import Fields
@@ -109,22 +108,41 @@ def export_openCARP(
109108
comments='',
110109
)
111110

112-
# Save elements info
113-
n_lines = case.indices.shape[0]
111+
# Total number of lines
112+
n_triangles = case.indices.shape[0]
113+
n_lin_conns = 0 if case.arrows.linear_connections is None else case.arrows.linear_connections.shape[0]
114+
n_lines = n_triangles + n_lin_conns
115+
116+
# Save triangulation elements info
117+
cell_type = np.full(n_triangles, fill_value="Tr")
114118
cell_region = case.fields.cell_region if case.fields.cell_region is not None else np.zeros(n_triangles, dtype=int)
115119

116-
elem_data = ""
117-
for indices, region in zip(case.indices, cell_region):
118-
elem_data += 'Tr ' + ' '.join(map(str, indices)) + ' ' + str(region) + '\n'
120+
combined_cell_data = [cell_type[:, np.newaxis], case.indices, cell_region[:, np.newaxis]]
121+
combined_cell_data = np.concatenate(combined_cell_data, axis=1, dtype=object)
119122

123+
np.savetxt(
124+
output_path.with_suffix(".elem"),
125+
combined_cell_data,
126+
fmt="%s %d %d %d %d",
127+
header=str(n_lines),
128+
comments='',
129+
)
130+
131+
# Save linear connections info
120132
if case.arrows.linear_connections is not None:
121-
n_lines += case.arrows.linear_connections.shape[0]
122-
for indices in case.arrows.linear_connections:
123-
elem_data += 'Ln ' + ' '.join(map(str, indices)) + '\n'
133+
conn_type = np.full(n_lin_conns, fill_value="Ln")
134+
ln_region = case.arrows.linear_connection_regions if case.arrows.linear_connection_regions is not None else np.zeros(n_lin_conns, dtype=int)
135+
136+
combined_ln_conn_data = [conn_type[:, np.newaxis], case.arrows.linear_connections, ln_region[:, np.newaxis]]
137+
combined_ln_conn_data = np.concatenate(combined_ln_conn_data, axis=1, dtype=object)
124138

125-
with open(output_path.with_suffix('.elem'), 'w') as file:
126-
file.write(f'{n_lines}\n')
127-
file.write(elem_data)
139+
# append to the elem file
140+
with open(output_path.with_suffix(".elem"), 'a') as elem_file:
141+
np.savetxt(
142+
elem_file,
143+
combined_ln_conn_data,
144+
fmt="%s %d %d %d",
145+
)
128146

129147
# Save fibres
130148
if case.arrows.fibres is not None:
@@ -141,7 +159,7 @@ def export_openCARP(
141159

142160
# Ignore all -1 values, as these points do not belong to any pacing site
143161
for site_index in np.unique(case.fields.pacing_site)[1:]:
144-
162+
145163
pacing_site_points = np.nonzero(case.fields.pacing_site == site_index)[0]
146164
n_points = pacing_site_points.size
147165

@@ -240,7 +258,9 @@ def export_openep_mat(
240258
divergence=case.analyse.divergence
241259
)
242260

243-
userdata['rf'] = _export_ablation_data(ablation=case.ablation)
261+
if case.ablation is not None:
262+
userdata['rf'] = _export_ablation_data(ablation=case.ablation)
263+
244264
scipy.io.savemat(
245265
file_name=filename,
246266
mdict={'userdata': userdata},
@@ -276,7 +296,7 @@ def _add_surface_maps(surface_data, **kwargs):
276296

277297

278298
def export_csv(
279-
case: Case,
299+
system,
280300
filename: str,
281301
selections: dict,
282302
):
@@ -286,10 +306,17 @@ def export_csv(
286306
Saves selected field data.
287307
288308
Args:
289-
case (Case): dataset to be exported
309+
system (model.system_manager.System): System with dataset to be exported
290310
filename (str): name of file to be written
291311
selections (dict): the data field name and flag whether to export or not
292312
"""
313+
case = system.case
314+
mesh = system.mesh
315+
316+
point_region = _convert_cell_to_point(
317+
cell_data=case.fields.cell_region,
318+
mesh=mesh
319+
)
293320

294321
available_exports = {
295322
'Bipolar voltage': case.fields.bipolar_voltage,
@@ -298,10 +325,11 @@ def export_csv(
298325
'Force': case.fields.force,
299326
'Impedance': case.fields.impedance,
300327
'Thickness': case.fields.thickness,
301-
'Cell region': case.fields.cell_region,
328+
'Cell region': point_region,
302329
'Pacing site': case.fields.pacing_site,
303330
'Conduction velocity': case.fields.conduction_velocity,
304331
'Divergence': case.fields.cv_divergence,
332+
'Histogram': case.fields.histogram,
305333
}
306334

307335
df = pd.DataFrame()
@@ -555,4 +583,19 @@ def _export_ablation_data(ablation: Ablation):
555583
ablation_data['originaldata']['force']['lateralangle'] = ablation.force.lateral_angle if ablation.force.lateral_angle is not None else empty_float_array
556584
ablation_data['originaldata']['force']['position'] = ablation.force.points if ablation.force.points is not None else empty_float_array
557585

558-
return ablation_data
586+
return ablation_data
587+
588+
589+
def _convert_cell_to_point(cell_data, mesh):
590+
"""Convert cell data to point data by applying the cell value to its vertices"""
591+
point_data = np.zeros(mesh.n_points, dtype=int)
592+
for cell_i in range(mesh.n_cells):
593+
594+
cell_value = cell_data[cell_i]
595+
point_ids = mesh.get_cell(cell_i).point_ids
596+
597+
# Add cell value to point ids
598+
for pid in point_ids:
599+
point_data[pid] = cell_value
600+
601+
return point_data

0 commit comments

Comments
 (0)