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