openCARP
Doxygen code documentation for the open cardiac electrophysiology simulator openCARP
leadfield.cc
Go to the documentation of this file.
1 // SPDX-FileCopyrightText: Copyright (c) NumeriCor GmbH
2 // SPDX-License-Identifier: Apache-2.0
3 
12 #include <unordered_set>
13 
14 #include "leadfield.h"
15 #include "electrics.h"
16 #include "SF_init.h"
17 
18 #ifdef WITH_CALIPER
19 #include "caliper/cali.h"
20 #else
21 #include "caliper_hooks.h"
22 #endif
23 
24 namespace opencarp {
25 
27 {
28  if(param_globals::lf_vmfile && std::string(param_globals::lf_vmfile).length() > 0)
29  vm_file = param_globals::lf_vmfile;
30  else
31  vm_file = param_globals::vofile;
32 
33  ecg_timedt = param_globals::ecg_timedt;
34 
35  if(param_globals::lf_dir)
36  lf_dir = param_globals::lf_dir;
37 }
38 
40 {
41  for(sf_vec* z : Z) delete z;
42  if(Im) delete Im;
43 }
44 
45 void Leadfield::collect_lead_names(const SF::vector<stimulus> & stimuli)
46 {
47  lead_names.clear();
48  std::unordered_set<std::string> seen;
49  for(const stimulus & s : stimuli)
50  if(s.phys.type == LF_I && seen.insert(s.name).second)
51  lead_names.push_back(s.name);
52 }
53 
54 void Leadfield::write_leadfield_igb(const std::string & lead_name, sf_vec * phie_i)
55 {
56  int rank = get_rank();
58  std::string filename = "LF_" + lead_name + ".igb";
59 
60  FILE* f_out = nullptr;
61  int err = 0;
62  if(rank == 0) {
63  f_out = fopen(filename.c_str(), "w");
64  if(!f_out) {
65  log_msg(0, 5, 0, "Error: Cannot write to %s", filename.c_str());
66  err = 1;
67  } else {
68  IGBheader head;
69  head.x(phie_i->gsize());
70  head.y(1);
71  head.z(1);
72  head.t(1);
73  head.dim_t(0.0);
74  head.type(IGB_FLOAT);
75  head.systeme("little_endian");
76  head.unites_x("um");
77  head.unites("mV");
78  head.fileptr(f_out);
79  head.write();
80  }
81  }
82  if(get_global(err, MPI_SUM)) EXIT(1);
83  phie_i->write_binary<float>(f_out);
84 
85  if(rank == 0) {
86  fclose(f_out);
87  log_msg(0, 0, 0, " - Wrote %s (nodes=%zu)", filename.c_str(), phie_i->gsize());
88  }
89 }
90 
91 void Leadfield::apply_unit_curr_lead(const std::string & lead, Electrics & elec)
92 {
93  elec.ellip_solver.phiesrc->set(0.0);
94 
95  bool stim_found = false;
96  for(const stimulus & s : elec.stimuli) {
97  if(s.name == lead && s.phys.type == LF_I) {
98  // Normalise each electrode patch by its own enclosed volume so that
99  // multi-electrode leads (e.g. WCT-referenced precordial leads) keep the
100  // correct relative weighting. Scale a local copy to avoid mutating the
101  // shared stimulus state.
102  stimulus scaled = s;
103  SF_real vol = get_volume_from_nodes(*elec.ellip_solver.mass_e, scaled.electrode.vertices);
104  if(vol > 0.0)
105  scaled.pulse.strength *= 1.e12 / vol;
106  apply_stim_to_vector(scaled, *elec.ellip_solver.phiesrc, true);
107  stim_found = true;
108  }
109  }
110 
111  if(stim_found) {
112  elec.ellip_solver.mass_e->mult(*elec.ellip_solver.phiesrc, *elec.ellip_solver.currtmp);
113  elec.ellip_solver.phiesrc->deep_copy(*elec.ellip_solver.currtmp);
114  }
115 }
116 
117 void Leadfield::construct(Electrics & elec)
118 {
120 
121  if(elec.ellip_solver.phie_mat == nullptr) {
122  elec.ellip_solver.init();
123 
125  region_mask(extra_elec_msh, elec.mtype[Electrics::extra_grid].regions,
126  elec.mtype[Electrics::extra_grid].regionIDs, true, "gregion_e");
127 
129  region_mask(intra_elec_msh, elec.mtype[Electrics::intra_grid].regions,
130  elec.mtype[Electrics::intra_grid].regionIDs, true, "gregion_i");
131 
132  elec.ellip_solver.rebuild_stiffness(elec.mtype, elec.stimuli, elec.logger);
133  elec.ellip_solver.rebuild_mass(elec.logger);
134  }
135 
136  elec.ellip_solver.phie->set(0.0);
137  elec.ellip_solver.phie_i->set(0.0);
138 
139  collect_lead_names(elec.stimuli);
140 
141  int num_leads = lead_names.size();
142  if(num_leads == 0) {
143  log_msg(0, 4, 0, "Warning: No Leadfield stimuli found. Skipping.");
144  return;
145  }
146 
147  sf_mesh & imesh = get_mesh(intra_elec_msh);
148  if(Z.empty()) {
149  Z.resize(num_leads, nullptr);
150  for(int i = 0; i < num_leads; i++)
151  SF::init_vector(&Z[i], imesh, 1, sf_vec::algebraic);
152  }
153 
156 
157  log_msg(0, 0, 0, "Computing %d leadfield matrices...", num_leads);
158 
159  for(int lead_idx = 0; lead_idx < num_leads; lead_idx++) {
160  std::string current_lead = lead_names[lead_idx];
161  log_msg(0, 1, 0, " - Solving lead %d/%d: %s", lead_idx + 1, num_leads, current_lead.c_str());
162 
163  apply_unit_curr_lead(current_lead, elec);
164  (*elec.ellip_solver.lin_solver)(*elec.ellip_solver.phie, *elec.ellip_solver.phiesrc);
165 
166  if(elec.ellip_solver.lin_solver->reason < 0) {
167  log_msg(0, 5, 0, "Error: Leadfield solver failed to converge for lead %s! Reason: %s.", current_lead.c_str(),
168  elec.ellip_solver.lin_solver->reason_str());
169  EXIT(1);
170  }
171 
172  i2e->backward(*elec.ellip_solver.phie, *elec.ellip_solver.phie_i);
173 
174  // Z must stay in solver (algebraic) ordering to match Im in apply();
175  // phie_i is permuted to canonical only for the IGB write below.
176  Z[lead_idx]->deep_copy(*elec.ellip_solver.phie_i);
177 
178  if(p2c) (*p2c)(*elec.ellip_solver.phie_i, true);
179  write_leadfield_igb(current_lead, elec.ellip_solver.phie_i);
180  }
181  log_msg(0, 0, 0, "Leadfield computation complete.");
182 }
183 
184 void Leadfield::load(Electrics & elec)
185 {
186  collect_lead_names(elec.stimuli);
187 
188  int num_leads = lead_names.size();
189  if(num_leads == 0) {
190  log_msg(0, 5, 0, "Error: 'load' called but no leads defined in config!");
191  EXIT(1);
192  }
193 
194  sf_mesh & imesh = get_mesh(intra_elec_msh);
195  if(Z.empty()) {
196  Z.resize(num_leads, nullptr);
197  for(int i = 0; i < num_leads; i++)
198  SF::init_vector(&Z[i], imesh, 1, sf_vec::algebraic);
199  }
200 
201  std::string dir = lf_dir;
202  if(!dir.empty() && dir.back() != '/') dir += "/";
203 
204  size_t global_nodes = Z[0]->gsize();
205 
207 
208  int err = 0;
209  for(int i = 0; i < num_leads; i++) {
210  std::string name = lead_names[i];
211  std::string fpath = dir + "LF_" + name + ".igb";
212 
213  FILE* f = nullptr;
214  if(get_rank() == 0) {
215  f = fopen(fpath.c_str(), "r");
216  if(!f) {
217  log_msg(0, 5, 0, "Error: Cannot open file %s", fpath.c_str());
218  err = 1;
219  } else {
220  IGBheader igb(f, true);
221  if(igb.type() == 0) {
222  log_msg(0, 5, 0, "Error: Corrupt or unrecognized IGB header in %s", fpath.c_str());
223  fclose(f); f = nullptr; err = 1;
224  } else if((size_t)igb.x() != global_nodes) {
225  log_msg(0, 5, 0, "Error: Node count mismatch in %s! Expected %zu, got %d",
226  fpath.c_str(), global_nodes, igb.x());
227  fclose(f); f = nullptr; err = 1;
228  }
229  }
230  }
231 
232  if(get_global(err, MPI_SUM)) EXIT(1);
233  size_t nread = Z[i]->read_binary<float>(f);
234  if(get_rank() == 0) fclose(f);
235  if(nread != Z[i]->gsize()) {
236  log_msg(0, 5, 0, "Error: Incomplete read from %s! Expected %zu elements, got %zu.",
237  fpath.c_str(), Z[i]->gsize(), nread);
238  EXIT(1);
239  }
240  if(p2c) (*p2c)(*Z[i], false);
241 
242  log_msg(0, 0, 0, " - Loaded %s", fpath.c_str());
243  }
244 }
245 
246 void Leadfield::apply(sf_mat & Ki, sf_vec & vm)
247 {
248  if(Z.empty()) return;
249 
250  if(Im == nullptr)
252 
253  Ki.mult(vm, *Im);
254 
255  // V_L(t) = Z_L . (Ki vm); dot() reduces across ranks.
256  for(size_t i = 0; i < Z.size(); i++) {
257  double ecg = Z[i]->dot(*Im);
258  if(get_rank() == 0)
259  trace_data[i].push_back((float)ecg);
260  }
261 }
262 
264 {
266  log_msg(0, 5, 0, "Error: Leadfield requires active EP physics! Aborting.");
267  EXIT(1);
268  }
269 
271  set_dir(POSTPROC);
272  MPI_Barrier(PETSC_COMM_WORLD);
273 
274  if(lf_dir.length() > 0) {
275  load(elec);
276  } else {
277  log_msg(0, 0, 0, "Computing leadfields...");
278  construct(elec);
279  }
280 
281  int num_leads = lead_names.size();
282  if(num_leads == 0) {
283  log_msg(0, 5, 0, "No leads detected. Exiting.");
284  EXIT(1);
285  }
286 
287  if(get_rank() == 0) {
288  trace_data.resize(num_leads);
289  for(auto & vec : trace_data) vec.clear();
290  }
291 
292  sf_vec* vm_step = NULL;
295 
296  FILE* fd = nullptr;
297  int total_frames = 0;
298  float file_dt = 0.0;
299  int stride = 1;
300  int err = 0;
301 
302  set_dir(OUTPUT);
303 
304  if(get_rank() == 0) {
305  FILE_SPEC fs = f_open(vm_file.c_str(), "r");
306  if(fs) {
307  IGBheader head;
308  head.fileptr(fs->fd);
309  head.read();
310  total_frames = head.t();
311  file_dt = head.inc_t();
312 
313  if((size_t)head.x() != vm_step->gsize()) {
314  log_msg(0, 5, 0, "Error: Vm dimension in %s does not match mesh. Aborting.", vm_file.c_str());
315  err = 1;
316  }
317 
318  if(file_dt > 0.0)
319  stride = (int)((ecg_timedt / file_dt) + 0.5);
320  if(stride < 1) stride = 1;
321 
322  log_msg(0, 1, 0, " VM file: dt=%.4f ms, target=%.4f ms, stride=%d",
323  file_dt, ecg_timedt, stride);
324  fd = fs->fd;
325  delete(fs);
326  } else {
327  log_msg(0, 5, 0, "Error: Could not open %s", vm_file.c_str());
328  err = 1;
329  }
330  }
331  if(get_global(err, MPI_SUM)) EXIT(1);
332 
333  set_dir(POSTPROC);
334  MPI_Bcast(&total_frames, 1, MPI_INT, 0, PETSC_COMM_WORLD);
335  MPI_Bcast(&file_dt, 1, MPI_FLOAT, 0, PETSC_COMM_WORLD);
336  MPI_Bcast(&stride, 1, MPI_INT, 0, PETSC_COMM_WORLD);
337 
338  if(total_frames == 0) {
339  delete vm_step;
340  return 0;
341  }
342 
343  log_msg(0, 0, 0, "Computing ECG for %d leads, %d frames...", num_leads, total_frames);
344  sf_mat & Ki = *elec.parab_solver.rhs_parab;
345 
346  for(int t = 0; t < total_frames; t++) {
347  size_t nread = vm_step->read_binary<float>(fd);
348  if(nread != vm_step->gsize()) {
349  log_msg(0, 5, 0, "Error: %s read incomplete data slice from %s!", __func__, vm_file.c_str());
350  EXIT(1);
351  }
352 
353  bool forward = false;
354  if(sc) (*sc)(*vm_step, forward);
355 
356  if(t % stride == 0)
357  apply(Ki, *vm_step);
358  }
359 
360  if(get_rank() == 0 && fd) fclose(fd);
361  delete vm_step;
362 
363  int write_err = 0;
364  if(get_rank() == 0) {
365  for(size_t i = 0; i < lead_names.size(); i++) {
366  std::string fname = "ECG_" + lead_names[i] + ".dat";
367  FILE* f = fopen(fname.c_str(), "w");
368  if(f) {
369  for(size_t t = 0; t < trace_data[i].size(); t++) {
370  double time = t * file_dt * stride;
371  fprintf(f, "%g %g\n", time, trace_data[i][t]);
372  }
373  fclose(f);
374  } else {
375  log_msg(0, 5, 0, "Error: Cannot write %s", fname.c_str());
376  write_err = 1;
377  }
378  }
379  }
380 
381  if(!write_err)
382  log_msg(0, 0, 0, "ECG post-processing complete.");
383  return write_err;
384 }
385 
386 } // namespace opencarp
opencarp::real_t SF_real
Global scalar type.
Definition: SF_globals.h:18
#define CALI_CXX_MARK_FUNCTION
Definition: caliper_hooks.h:8
void dim_t(float a)
Definition: IGBheader.h:341
size_t x(void)
Definition: IGBheader.h:268
int type(void)
Definition: IGBheader.h:280
int systeme(void)
Definition: IGBheader.h:297
size_t t(void)
Definition: IGBheader.h:277
size_t y(void)
Definition: IGBheader.h:271
void unites_x(const char *a)
Definition: IGBheader.h:353
void fileptr(gzFile f)
Definition: IGBheader.cc:329
int write()
Definition: IGBheader.cc:343
size_t z(void)
Definition: IGBheader.h:274
void unites(const char *a)
Definition: IGBheader.h:365
virtual T gsize() const =0
size_t read_binary(FILE *fd)
Container for a PETSc VecScatter.
void backward(abstract_vector< T, S > &in, abstract_vector< T, S > &out, bool add=false)
Backward scattering.
A vector storing arbitrary data.
Definition: SF_vector.h:28
parabolic_solver parab_solver
Solver for the parabolic bidomain equation.
Definition: electrics.h:259
igb_output_manager output_manager
class handling the igb output
Definition: electrics.h:268
int read(bool quiet=false)
Definition: IGBheader.cc:717
void fileptr(FILE *f)
Definition: IGBheader.cc:312
void inc_t(float a)
Definition: IGBheader.h:302
int run(Electrics &elec)
Full pipeline: construct or load Z, stream vm.igb, write ECG_*.dat.
Definition: leadfield.cc:263
void close_files_and_cleanup()
close file descriptors
Definition: sim_utils.cc:2939
sf_mat * rhs_parab
rhs matrix to solve parabolic
Definition: electrics.h:104
Tissue level electrics, main Electrics physics class.
Lead field ECG computation.
void init_vector(SF::abstract_vector< T, S > **vec)
Definition: SF_init.h:110
SF::scattering * get_scattering(const int from, const int to, const SF::SF_nbr nbr, const int dpn)
Get a scattering from the global scatter registry.
SF_real get_volume_from_nodes(sf_mat &mass, SF::vector< mesh_int_t > &local_idx)
Definition: fem_utils.cc:202
sf_mesh & get_mesh(const mesh_t gt)
Get a mesh by specifying the gridID.
Definition: sf_interface.cc:18
SF::scattering * get_permutation(const int mesh_id, const int perm_id, const int dpn)
Get the PETSC to canonical permutation scattering for a given mesh and number of dpn.
void region_mask(mesh_t meshspec, SF::vector< RegionSpecs > &regspec, SF::vector< int > &regionIDs, bool mask_elem, const char *reglist, bool warn_on_default_tags)
classify elements/points as belonging to a region
Definition: ionics.cc:391
SF::meshdata< mesh_int_t, mesh_real_t > sf_mesh
Definition: sf_interface.h:33
void apply_stim_to_vector(const stimulus &s, sf_vec &vec, bool add)
Definition: electrics.cc:438
int set_dir(IO_t dest)
Definition: sim_utils.cc:1615
int get_rank(MPI_Comm comm=PETSC_COMM_WORLD)
Definition: basics.h:269
T get_global(T in, MPI_Op OP, MPI_Comm comm=PETSC_COMM_WORLD)
Do a global reduction on a variable.
Definition: basics.h:218
FILE_SPEC f_open(const char *fname, const char *mode)
Open a FILE_SPEC.
Definition: basics.cc:123
@ POSTPROC
Definition: sim_utils.h:39
@ OUTPUT
Definition: sim_utils.h:39
void set_elec_tissue_properties(MaterialType *mtype, Electrics::grid_t g, FILE_SPEC logger)
Fill the RegionSpec of an electrics grid with the associated inputs from the param parameters.
Definition: electrics.cc:94
void log_msg(FILE_SPEC out, int level, unsigned char flag, const char *fmt,...)
Definition: basics.cc:57
@ extra_elec_msh
Definition: sf_interface.h:46
@ intra_elec_msh
Definition: sf_interface.h:45
bool mesh_is_registered(const mesh_t gt)
check wheter a SF mesh is set
Definition: sf_interface.cc:48
SF::abstract_vector< SF_int, SF_real > sf_vec
Definition: sf_interface.h:35
Basic_physic * get_physics(physic_t p, bool error_if_missing)
Convinience function to get a physics.
Definition: sim_utils.cc:2064
SF::abstract_matrix< SF_int, SF_real > sf_mat
Definition: sf_interface.h:37
#define IGB_FLOAT
Definition: IGBheader.h:44
#define PETSC_TO_CANONICAL
Permute algebraic data from PETSC to canonical ordering.
Definition: sf_interface.h:64
File descriptor struct.
Definition: basics.h:120