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