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", elec.logger);
127 
129  region_mask(intra_elec_msh, elec.mtype[Electrics::intra_grid].regions,
130  elec.mtype[Electrics::intra_grid].regionIDs, true, "gregion_i", elec.logger);
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 after %d iterations with residual %g! Reason: %s.", current_lead.c_str(),
168  static_cast<int>(elec.ellip_solver.lin_solver->niter), elec.ellip_solver.lin_solver->final_residual,
169  elec.ellip_solver.lin_solver->reason_str());
170  EXIT(1);
171  }
172 
173  i2e->backward(*elec.ellip_solver.phie, *elec.ellip_solver.phie_i);
174 
175  // Z must stay in solver (algebraic) ordering to match Im in apply();
176  // phie_i is permuted to canonical only for the IGB write below.
177  Z[lead_idx]->deep_copy(*elec.ellip_solver.phie_i);
178 
179  if(p2c) (*p2c)(*elec.ellip_solver.phie_i, true);
180  write_leadfield_igb(current_lead, elec.ellip_solver.phie_i);
181  }
182  log_msg(0, 0, 0, "Leadfield computation complete.");
183 }
184 
185 void Leadfield::load(Electrics & elec)
186 {
187  collect_lead_names(elec.stimuli);
188 
189  int num_leads = lead_names.size();
190  if(num_leads == 0) {
191  log_msg(0, 5, 0, "Error: 'load' called but no leads defined in config!");
192  EXIT(1);
193  }
194 
195  sf_mesh & imesh = get_mesh(intra_elec_msh);
196  if(Z.empty()) {
197  Z.resize(num_leads, nullptr);
198  for(int i = 0; i < num_leads; i++)
199  SF::init_vector(&Z[i], imesh, 1, sf_vec::algebraic);
200  }
201 
202  std::string dir = lf_dir;
203  if(!dir.empty() && dir.back() != '/') dir += "/";
204 
205  size_t global_nodes = Z[0]->gsize();
206 
208 
209  int err = 0;
210  for(int i = 0; i < num_leads; i++) {
211  std::string name = lead_names[i];
212  std::string fpath = dir + "LF_" + name + ".igb";
213 
214  FILE* f = nullptr;
215  if(get_rank() == 0) {
216  f = fopen(fpath.c_str(), "r");
217  if(!f) {
218  log_msg(0, 5, 0, "Error: Cannot open file %s", fpath.c_str());
219  err = 1;
220  } else {
221  IGBheader igb(f, true);
222  if(igb.type() == 0) {
223  log_msg(0, 5, 0, "Error: Corrupt or unrecognized IGB header in %s", fpath.c_str());
224  fclose(f); f = nullptr; err = 1;
225  } else if((size_t)igb.x() != global_nodes) {
226  log_msg(0, 5, 0, "Error: Node count mismatch in %s! Expected %zu, got %d",
227  fpath.c_str(), global_nodes, igb.x());
228  fclose(f); f = nullptr; err = 1;
229  }
230  }
231  }
232 
233  if(get_global(err, MPI_SUM)) EXIT(1);
234  size_t nread = Z[i]->read_binary<float>(f);
235  if(get_rank() == 0) fclose(f);
236  if(nread != Z[i]->gsize()) {
237  log_msg(0, 5, 0, "Error: Incomplete read from %s! Expected %zu elements, got %zu.",
238  fpath.c_str(), Z[i]->gsize(), nread);
239  EXIT(1);
240  }
241  if(p2c) (*p2c)(*Z[i], false);
242 
243  log_msg(0, 0, 0, " - Loaded %s", fpath.c_str());
244  }
245 }
246 
247 void Leadfield::apply(sf_mat & Ki, sf_vec & vm)
248 {
249  if(Z.empty()) return;
250 
251  if(Im == nullptr)
253 
254  Ki.mult(vm, *Im);
255 
256  // V_L(t) = Z_L . (Ki vm); dot() reduces across ranks.
257  for(size_t i = 0; i < Z.size(); i++) {
258  double ecg = Z[i]->dot(*Im);
259  if(get_rank() == 0)
260  trace_data[i].push_back((float)ecg);
261  }
262 }
263 
265 {
267  log_msg(0, 5, 0, "Error: Leadfield requires active EP physics! Aborting.");
268  EXIT(1);
269  }
270 
272  set_dir(POSTPROC);
273  MPI_Barrier(SF_COMM);
274 
275  if(lf_dir.length() > 0) {
276  load(elec);
277  } else {
278  log_msg(0, 0, 0, "Computing leadfields...");
279  construct(elec);
280  }
281 
282  int num_leads = lead_names.size();
283  if(num_leads == 0) {
284  log_msg(0, 5, 0, "No leads detected. Exiting.");
285  EXIT(1);
286  }
287 
288  if(get_rank() == 0) {
289  trace_data.resize(num_leads);
290  for(auto & vec : trace_data) vec.clear();
291  }
292 
293  sf_vec* vm_step = NULL;
296 
297  FILE* fd = nullptr;
298  int total_frames = 0;
299  float file_dt = 0.0;
300  int stride = 1;
301  int err = 0;
302 
303  set_dir(OUTPUT);
304 
305  if(get_rank() == 0) {
306  FILE_SPEC fs = f_open(vm_file.c_str(), "r");
307  if(fs) {
308  IGBheader head;
309  head.fileptr(fs->fd);
310  head.read();
311  total_frames = head.t();
312  file_dt = head.inc_t();
313 
314  if((size_t)head.x() != vm_step->gsize()) {
315  log_msg(0, 5, 0, "Error: Vm dimension in %s does not match mesh. Aborting.", vm_file.c_str());
316  err = 1;
317  }
318 
319  if(file_dt > 0.0)
320  stride = (int)((ecg_timedt / file_dt) + 0.5);
321  if(stride < 1) stride = 1;
322 
323  log_msg(0, 1, 0, " VM file: dt=%.4f ms, target=%.4f ms, stride=%d",
324  file_dt, ecg_timedt, stride);
325  fd = fs->fd;
326  delete(fs);
327  } else {
328  log_msg(0, 5, 0, "Error: Could not open %s", vm_file.c_str());
329  err = 1;
330  }
331  }
332  if(get_global(err, MPI_SUM)) EXIT(1);
333 
334  set_dir(POSTPROC);
335  MPI_Bcast(&total_frames, 1, MPI_INT, 0, SF_COMM);
336  MPI_Bcast(&file_dt, 1, MPI_FLOAT, 0, SF_COMM);
337  MPI_Bcast(&stride, 1, MPI_INT, 0, SF_COMM);
338 
339  if(total_frames == 0) {
340  delete vm_step;
341  return 0;
342  }
343 
344  log_msg(0, 0, 0, "Computing ECG for %d leads, %d frames...", num_leads, total_frames);
345  sf_mat & Ki = *elec.parab_solver.rhs_parab;
346 
347  for(int t = 0; t < total_frames; t++) {
348  size_t nread = vm_step->read_binary<float>(fd);
349  if(nread != vm_step->gsize()) {
350  log_msg(0, 5, 0, "Error: %s read incomplete data slice from %s!", __func__, vm_file.c_str());
351  EXIT(1);
352  }
353 
354  bool forward = false;
355  if(sc) (*sc)(*vm_step, forward);
356 
357  if(t % stride == 0)
358  apply(Ki, *vm_step);
359  }
360 
361  if(get_rank() == 0 && fd) fclose(fd);
362  delete vm_step;
363 
364  int write_err = 0;
365  if(get_rank() == 0) {
366  for(size_t i = 0; i < lead_names.size(); i++) {
367  std::string fname = "ECG_" + lead_names[i] + ".dat";
368  FILE* f = fopen(fname.c_str(), "w");
369  if(f) {
370  for(size_t t = 0; t < trace_data[i].size(); t++) {
371  double time = t * file_dt * stride;
372  fprintf(f, "%g %g\n", time, trace_data[i][t]);
373  }
374  fclose(f);
375  } else {
376  log_msg(0, 5, 0, "Error: Cannot write %s", fname.c_str());
377  write_err = 1;
378  }
379  }
380  }
381 
382  if(!write_err)
383  log_msg(0, 0, 0, "ECG post-processing complete.");
384  return write_err;
385 }
386 
387 } // namespace opencarp
#define SF_COMM
the default SlimFem MPI communicator
Definition: SF_globals.h:25
opencarp::real_t SF_real
Global scalar type.
Definition: SF_globals.h:30
#define EXIT(A)
finalize the backend, if it runs, and exit
Definition: backend.h:23
#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)
Redistributes vector data from indexing "a" to indexing "b".
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:264
void close_files_and_cleanup()
close file descriptors
Definition: sim_utils.cc:2946
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:127
void region_mask(mesh_t meshspec, SF::vector< RegionSpecs > &regspec, SF::vector< int > &regionIDs, bool mask_elem, const char *reglist, FILE_SPEC logger)
classify elements/points as belonging to a region
Definition: ionics.cc:340
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.
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:435
int set_dir(IO_t dest)
Definition: sim_utils.cc:1619
T get_global(T in, MPI_Op OP, MPI_Comm comm=SF_COMM)
Do a global reduction on a variable.
Definition: basics.h:219
FILE_SPEC f_open(const char *fname, const char *mode)
Open a FILE_SPEC.
Definition: basics.cc:111
@ 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:55
@ 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:2068
int get_rank(MPI_Comm comm=SF_COMM)
Definition: basics.h:270
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:121