• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

openmc-dev / openmc / 35496191655

20 Sep 2026 07:11AM UTC coverage: 81.484% (+0.007%) from 81.477%
35496191655

Pull #4139

github

web-flow
Merge 1690933f6 into afa7a14ac
Pull Request #4139: Fix lost particles after virtual surface crossings in complex regions

19973 of 29002 branches covered (68.87%)

Branch coverage included in aggregate %.

5 of 5 new or added lines in 1 file covered. (100.0%)

592 existing lines in 10 files now uncovered.

62444 of 72143 relevant lines covered (86.56%)

49753632.27 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

69.55
/src/plot.cpp
1
#include "openmc/plot.h"
2

3
#include <algorithm>
4
#include <cmath>
5
#include <cstdio>
6
#include <fstream>
7
#include <sstream>
8

9
#include "openmc/tensor.h"
10
#include <fmt/core.h>
11
#include <fmt/ostream.h>
12
#ifdef USE_LIBPNG
13
#include <png.h>
14
#endif
15

16
#include "openmc/cell.h"
17
#include "openmc/constants.h"
18
#include "openmc/container_util.h"
19
#include "openmc/dagmc.h"
20
#include "openmc/error.h"
21
#include "openmc/file_utils.h"
22
#include "openmc/geometry.h"
23
#include "openmc/hdf5_interface.h"
24
#include "openmc/material.h"
25
#include "openmc/mesh.h"
26
#include "openmc/message_passing.h"
27
#include "openmc/openmp_interface.h"
28
#include "openmc/output.h"
29
#include "openmc/particle.h"
30
#include "openmc/progress_bar.h"
31
#include "openmc/random_lcg.h"
32
#include "openmc/settings.h"
33
#include "openmc/simulation.h"
34
#include "openmc/string_utils.h"
35
#include "openmc/tallies/filter.h"
36

37
namespace openmc {
38

39
//==============================================================================
40
// Constants
41
//==============================================================================
42

43
constexpr int PLOT_LEVEL_LOWEST {-1}; //!< lower bound on plot universe level
44
constexpr int32_t NOT_FOUND {-2};
45
constexpr int32_t OVERLAP {-3};
46

47
IdData::IdData(size_t h_res, size_t v_res, bool /*include_filter*/)
4,873 ✔
48
  : data_({v_res, h_res, 3}, NOT_FOUND)
4,873 ✔
49
{}
4,873 ✔
50

51
void IdData::set_value(size_t y, size_t x, const Particle& p, int level,
35,403,192 ✔
52
  Filter* /*filter*/, FilterMatch* /*match*/)
53
{
54
  // set cell data
55
  if (p.n_coord() <= level) {
35,403,192 !
56
    data_(y, x, 0) = NOT_FOUND;
×
57
    data_(y, x, 1) = NOT_FOUND;
×
58
  } else {
59
    data_(y, x, 0) = model::cells.at(p.coord(level).cell())->id_;
35,403,192 !
60
    data_(y, x, 1) = level == p.n_coord() - 1
35,403,192 ✔
61
                       ? p.cell_instance()
35,403,192 !
62
                       : cell_instance_at_level(p, level);
×
63
  }
64

65
  // set material data
66
  Cell* c = model::cells.at(p.lowest_coord().cell()).get();
35,403,192 ✔
67
  if (p.material() == MATERIAL_VOID) {
35,403,192 ✔
68
    data_(y, x, 2) = MATERIAL_VOID;
27,301,736 ✔
69
  } else if (c->type_ == Fill::MATERIAL) {
8,101,456 !
70
    Material* m = model::materials.at(p.material()).get();
8,101,456 ✔
71
    data_(y, x, 2) = m->id_;
8,101,456 ✔
72
  }
73
}
35,403,192 ✔
74

75
void IdData::set_overlap(size_t y, size_t x, int /*overlap_idx*/)
28,248 ✔
76
{
77
  for (size_t k = 0; k < data_.shape(2); ++k)
225,984 !
78
    data_(y, x, k) = OVERLAP;
84,744 ✔
79
}
28,248 ✔
80

81
PropertyData::PropertyData(size_t h_res, size_t v_res, bool /*include_filter*/)
×
82
  : data_({v_res, h_res, 2}, NOT_FOUND)
×
83
{}
×
84

85
void PropertyData::set_value(size_t y, size_t x, const Particle& p, int level,
×
86
  Filter* /*filter*/, FilterMatch* /*match*/)
87
{
88
  Cell* c = model::cells.at(p.lowest_coord().cell()).get();
×
89
  data_(y, x, 0) = (p.sqrtkT() * p.sqrtkT()) / K_BOLTZMANN;
×
90
  data_(y, x, 1) = c->density(p.cell_instance());
×
91
}
×
92

93
void PropertyData::set_overlap(size_t y, size_t x, int /*overlap_idx*/)
×
94
{
95
  data_(y, x) = OVERLAP;
×
96
}
×
97

98
//==============================================================================
99
// RasterData implementation
100
//==============================================================================
101

102
RasterData::RasterData(size_t h_res, size_t v_res, bool include_filter)
938 ✔
103
  : id_data_({v_res, h_res, include_filter ? 4u : 3u}, NOT_FOUND),
1,854 ✔
104
    property_data_({v_res, h_res, 2}, static_cast<double>(NOT_FOUND)),
938 ✔
105
    include_filter_(include_filter)
938 ✔
106
{}
938 ✔
107

108
void RasterData::set_value(size_t y, size_t x, const Particle& p, int level,
3,772,679 ✔
109
  Filter* filter, FilterMatch* match)
110
{
111
  // set cell data
112
  if (p.n_coord() <= level) {
3,772,679 !
113
    id_data_(y, x, 0) = NOT_FOUND;
×
114
    id_data_(y, x, 1) = NOT_FOUND;
×
115
  } else {
116
    id_data_(y, x, 0) = model::cells.at(p.coord(level).cell())->id_;
3,772,679 !
117
    id_data_(y, x, 1) = level == p.n_coord() - 1
3,772,679 ✔
118
                          ? p.cell_instance()
3,772,679 !
119
                          : cell_instance_at_level(p, level);
×
120
  }
121

122
  // set material data
123
  Cell* c = model::cells.at(p.lowest_coord().cell()).get();
3,772,679 ✔
124
  if (p.material() == MATERIAL_VOID) {
3,772,679 ✔
125
    id_data_(y, x, 2) = MATERIAL_VOID;
2,573,050 ✔
126
  } else if (c->type_ == Fill::MATERIAL) {
1,199,629 !
127
    Material* m = model::materials.at(p.material()).get();
1,199,629 ✔
128
    id_data_(y, x, 2) = m->id_;
1,199,629 ✔
129
  }
130

131
  // set filter index (only if filter is being used)
132
  if (include_filter_ && filter) {
3,772,679 !
133
    filter->get_all_bins(p, TallyEstimator::COLLISION, *match);
55,000 ✔
134
    if (match->bins_.empty()) {
55,000 !
135
      id_data_(y, x, 3) = -1;
×
136
    } else {
137
      id_data_(y, x, 3) = match->bins_[0];
55,000 ✔
138
    }
139
    match->bins_.clear();
55,000 !
140
    match->weights_.clear();
55,000 !
141
  }
142

143
  // set temperature (in K)
144
  property_data_(y, x, 0) = (p.sqrtkT() * p.sqrtkT()) / K_BOLTZMANN;
3,772,679 ✔
145

146
  // set density (g/cm³)
147
  if (c->type_ != Fill::UNIVERSE && p.material() != MATERIAL_VOID) {
3,772,679 !
148
    property_data_(y, x, 1) = c->density(p.cell_instance());
1,199,629 ✔
149
  }
150
}
3,772,679 ✔
151

152
void RasterData::set_overlap(size_t y, size_t x, int overlap_idx)
365,794 ✔
153
{
154
  // Set cell, instance, and material to OVERLAP, but preserve filter bin for
155
  // tally plotting. Cell encodes the overlap index as a negative number so that
156
  // it can be used to look up overlap information in the plotter.
157
  id_data_(y, x, 0) = OVERLAP - overlap_idx - 1;
365,794 ✔
158
  id_data_(y, x, 1) = OVERLAP;
365,794 ✔
159
  id_data_(y, x, 2) = OVERLAP;
365,794 ✔
160

161
  property_data_(y, x, 0) = OVERLAP;
365,794 ✔
162
  property_data_(y, x, 1) = OVERLAP;
365,794 ✔
163
}
365,794 ✔
164

165
//==============================================================================
166
// Global variables
167
//==============================================================================
168

169
namespace model {
170

171
std::unordered_map<int, int> plot_map;
172
vector<std::unique_ptr<PlottableInterface>> plots;
173
uint64_t plotter_seed = 1;
174

175
} // namespace model
176

177
//==============================================================================
178
// RUN_PLOT controls the logic for making one or many plots
179
//==============================================================================
180

181
extern "C" int openmc_plot_geometry()
121 ✔
182
{
183

184
  for (auto& pl : model::plots) {
407 ✔
185
    write_message(5, "Processing plot {}: {}...", pl->id(), pl->path_plot());
286 ✔
186
    pl->create_output();
286 ✔
187
  }
188

189
  return 0;
121 ✔
190
}
191

192
void PlottableInterface::write_image(const ImageData& data) const
231 ✔
193
{
194
#ifdef USE_LIBPNG
195
  output_png(path_plot(), data);
231 ✔
196
#else
197
  output_ppm(path_plot(), data);
198
#endif
199
}
231 ✔
200

201
void Plot::create_output() const
198 ✔
202
{
203
  if (PlotType::slice == type_) {
198 ✔
204
    // create 2D image
205
    ImageData image = create_image();
143 ✔
206
    write_image(image);
143 ✔
207
  } else if (PlotType::voxel == type_) {
198 !
208
    // create voxel file for 3D viewing
209
    create_voxel();
55 ✔
210
  }
211
}
198 ✔
212

213
void Plot::print_info() const
154 ✔
214
{
215
  // Plot type
216
  if (PlotType::slice == type_) {
154 ✔
217
    fmt::print("Plot Type: Slice\n");
121 ✔
218
  } else if (PlotType::voxel == type_) {
33 !
219
    fmt::print("Plot Type: Voxel\n");
33 ✔
220
  }
221

222
  // Plot parameters
223
  fmt::print("Origin: {} {} {}\n", origin_[0], origin_[1], origin_[2]);
154 ✔
224

225
  if (PlotType::slice == type_) {
154 ✔
226
    fmt::print("Width: {:4} {:4}\n", width_[0], width_[1]);
121 ✔
227
  } else if (PlotType::voxel == type_) {
33 !
228
    fmt::print("Width: {:4} {:4} {:4}\n", width_[0], width_[1], width_[2]);
33 ✔
229
  }
230

231
  if (PlotColorBy::cells == color_by_) {
154 ✔
232
    fmt::print("Coloring: Cells\n");
88 ✔
233
  } else if (PlotColorBy::mats == color_by_) {
66 !
234
    fmt::print("Coloring: Materials\n");
66 ✔
235
  }
236

237
  if (PlotType::slice == type_) {
154 ✔
238
    switch (basis_) {
121 !
239
    case PlotBasis::xy:
77 ✔
240
      fmt::print("Basis: XY\n");
77 ✔
241
      break;
77 ✔
242
    case PlotBasis::xz:
22 ✔
243
      fmt::print("Basis: XZ\n");
22 ✔
244
      break;
22 ✔
245
    case PlotBasis::yz:
22 ✔
246
      fmt::print("Basis: YZ\n");
22 ✔
247
      break;
22 ✔
248
    }
249
    fmt::print("Pixels: {} {}\n", pixels()[0], pixels()[1]);
121 ✔
250
  } else if (PlotType::voxel == type_) {
33 !
251
    fmt::print("Voxels: {} {} {}\n", pixels()[0], pixels()[1], pixels()[2]);
33 ✔
252
  }
253
}
154 ✔
254

255
void read_plots_xml()
1,413 ✔
256
{
257
  // Check if plots.xml exists; this is only necessary when the plot runmode is
258
  // initiated. Otherwise, we want to read plots.xml because it may be called
259
  // later via the API. In that case, its ok for a plots.xml to not exist
260
  std::string filename = settings::path_input + "plots.xml";
1,413 ✔
261
  if (!file_exists(filename) && settings::run_mode == RunMode::PLOTTING) {
1,413 !
UNCOV
262
    fatal_error(fmt::format("Plots XML file '{}' does not exist!", filename));
×
263
  }
264

265
  write_message("Reading plot XML file...", 5);
1,413 ✔
266

267
  // Parse plots.xml file
268
  pugi::xml_document doc;
1,413 ✔
269
  doc.load_file(filename.c_str());
1,413 ✔
270

271
  pugi::xml_node root = doc.document_element();
1,413 ✔
272

273
  read_plots_xml(root);
1,413 ✔
274
}
1,413 ✔
275

276
void read_plots_xml(pugi::xml_node root)
1,910 ✔
277
{
278
  for (auto node : root.children("plot")) {
2,891 ✔
279
    std::string plot_desc = "<auto>";
990 ✔
280
    if (check_for_node(node, "id")) {
990 !
281
      plot_desc = get_node_value(node, "id", true);
990 ✔
282
    }
283

284
    if (check_for_node(node, "type")) {
990 !
285
      std::string type_str = get_node_value(node, "type", true);
990 ✔
286
      if (type_str == "slice") {
990 ✔
287
        model::plots.emplace_back(
838 ✔
288
          std::make_unique<Plot>(node, Plot::PlotType::slice));
1,685 ✔
289
      } else if (type_str == "voxel") {
143 ✔
290
        model::plots.emplace_back(
55 ✔
291
          std::make_unique<Plot>(node, Plot::PlotType::voxel));
110 ✔
292
      } else if (type_str == "wireframe_raytrace") {
88 ✔
293
        model::plots.emplace_back(
55 ✔
294
          std::make_unique<WireframeRayTracePlot>(node));
110 ✔
295
      } else if (type_str == "solid_raytrace") {
33 !
296
        model::plots.emplace_back(std::make_unique<SolidRayTracePlot>(node));
33 ✔
297
      } else {
UNCOV
298
        fatal_error(fmt::format(
×
299
          "Unsupported plot type '{}' in plot {}", type_str, plot_desc));
300
      }
301
      model::plot_map[model::plots.back()->id()] = model::plots.size() - 1;
981 ✔
302
    } else {
981 ✔
UNCOV
303
      fatal_error(fmt::format("Must specify plot type in plot {}", plot_desc));
×
304
    }
305
  }
981 ✔
306
}
1,901 ✔
307

308
void free_memory_plot()
9,699 ✔
309
{
310
  model::plots.clear();
9,699 ✔
311
  model::plot_map.clear();
9,699 ✔
312
}
9,699 ✔
313

314
// creates an image based on user input from a plots.xml <plot>
315
// specification in the PNG/PPM format
316
ImageData Plot::create_image() const
143 ✔
317
{
318
  size_t width = pixels()[0];
143 ✔
319
  size_t height = pixels()[1];
143 ✔
320

321
  ImageData data({width, height}, not_found_);
143 ✔
322

323
  // generate ids for the plot
324
  auto ids = get_map<IdData>();
143 ✔
325

326
  // assign colors
327
  for (size_t y = 0; y < height; y++) {
30,063 ✔
328
    for (size_t x = 0; x < width; x++) {
7,622,120 ✔
329
      int idx = color_by_ == PlotColorBy::cells ? 0 : 2;
7,592,200 ✔
330
      auto id = ids.data_(y, x, idx);
7,592,200 ✔
331
      // no setting needed if not found
332
      if (id == NOT_FOUND) {
7,592,200 ✔
333
        continue;
1,082,532 ✔
334
      }
335
      if (id == OVERLAP) {
6,537,916 ✔
336
        data(x, y) = overlap_color_;
28,248 ✔
337
        continue;
28,248 ✔
338
      }
339
      if (PlotColorBy::cells == color_by_) {
6,509,668 ✔
340
        data(x, y) = colors_[model::cell_map[id]];
3,011,668 ✔
341
      } else if (PlotColorBy::mats == color_by_) {
3,498,000 !
342
        if (id == MATERIAL_VOID) {
3,498,000 !
UNCOV
343
          data(x, y) = WHITE;
×
344
          continue;
×
345
        }
346
        data(x, y) = colors_[model::material_map[id]];
3,498,000 ✔
347
      } // color_by if-else
348
    }
349
  }
350

351
  // draw mesh lines if present
352
  if (index_meshlines_mesh_ >= 0) {
143 ✔
353
    draw_mesh_lines(data);
33 ✔
354
  }
355

356
  return data;
143 ✔
357
}
143 ✔
358

359
void PlottableInterface::set_id(pugi::xml_node plot_node)
990 ✔
360
{
361
  int id {C_NONE};
990 ✔
362
  if (check_for_node(plot_node, "id")) {
990 !
363
    id = std::stoi(get_node_value(plot_node, "id"));
990 ✔
364
  }
365

366
  try {
990 ✔
367
    set_id(id);
990 ✔
UNCOV
368
  } catch (const std::runtime_error& e) {
×
369
    fatal_error(e.what());
×
370
  }
×
371
}
990 ✔
372

373
void PlottableInterface::set_id(int id)
1,001 ✔
374
{
375
  if (id < 0 && id != C_NONE) {
1,001 !
UNCOV
376
    throw std::runtime_error {fmt::format("Invalid plot ID: {}", id)};
×
377
  }
378

379
  if (id == C_NONE) {
1,001 ✔
380
    id = 1;
11 ✔
381
    for (const auto& p : model::plots) {
22 ✔
382
      id = std::max(id, p->id() + 1);
22 !
383
    }
384
  }
385

386
  if (id_ == id)
1,001 !
387
    return;
388

389
  // Check to make sure this ID doesn't already exist
390
  if (model::plot_map.find(id) != model::plot_map.end()) {
1,001 !
UNCOV
391
    throw std::runtime_error {
×
392
      fmt::format("Two or more plots use the same unique ID: {}", id)};
×
393
  }
394

395
  id_ = id;
1,001 ✔
396
}
397

398
// Checks if png or ppm is already present
399
bool file_extension_present(
981 ✔
400
  const std::string& filename, const std::string& extension)
401
{
402
  std::string file_extension_if_present =
981 ✔
403
    filename.substr(filename.find_last_of(".") + 1);
981 ✔
404
  if (file_extension_if_present == extension)
981 ✔
405
    return true;
55 ✔
406
  return false;
407
}
981 ✔
408

409
void Plot::set_output_path(pugi::xml_node plot_node)
902 ✔
410
{
411
  // Set output file path
412
  std::string filename;
902 ✔
413

414
  if (check_for_node(plot_node, "filename")) {
902 ✔
415
    filename = get_node_value(plot_node, "filename");
242 ✔
416
  } else {
417
    filename = fmt::format("plot_{}", id());
660 ✔
418
  }
419
  const std::string dir_if_present =
902 ✔
420
    filename.substr(0, filename.find_last_of("/") + 1);
902 ✔
421
  if (dir_if_present.size() > 0 && !dir_exists(dir_if_present)) {
902 ✔
422
    fatal_error(fmt::format("Directory '{}' does not exist!", dir_if_present));
9 ✔
423
  }
424
  // add appropriate file extension to name
425
  switch (type_) {
893 !
426
  case PlotType::slice:
838 ✔
427
#ifdef USE_LIBPNG
428
    if (!file_extension_present(filename, "png"))
838 !
429
      filename.append(".png");
838 ✔
430
#else
431
    if (!file_extension_present(filename, "ppm"))
432
      filename.append(".ppm");
433
#endif
434
    break;
435
  case PlotType::voxel:
55 ✔
436
    if (!file_extension_present(filename, "h5"))
55 !
437
      filename.append(".h5");
55 ✔
438
    break;
439
  }
440

441
  path_plot_ = filename;
893 ✔
442

443
  // Copy plot pixel size
444
  vector<int> pxls = get_node_array<int>(plot_node, "pixels");
1,786 ✔
445
  if (PlotType::slice == type_) {
893 ✔
446
    if (pxls.size() == 2) {
838 !
447
      pixels()[0] = pxls[0];
838 ✔
448
      pixels()[1] = pxls[1];
838 ✔
449
    } else {
UNCOV
450
      fatal_error(
×
451
        fmt::format("<pixels> must be length 2 in slice plot {}", id()));
×
452
    }
453
  } else if (PlotType::voxel == type_) {
55 !
454
    if (pxls.size() == 3) {
55 !
455
      pixels()[0] = pxls[0];
55 ✔
456
      pixels()[1] = pxls[1];
55 ✔
457
      pixels()[2] = pxls[2];
55 ✔
458
    } else {
UNCOV
459
      fatal_error(
×
460
        fmt::format("<pixels> must be length 3 in voxel plot {}", id()));
×
461
    }
462
  }
463
}
893 ✔
464

465
void PlottableInterface::set_bg_color(pugi::xml_node plot_node)
990 ✔
466
{
467
  // Copy plot background color
468
  if (check_for_node(plot_node, "background")) {
990 ✔
469
    vector<int> bg_rgb = get_node_array<int>(plot_node, "background");
44 ✔
470
    if (bg_rgb.size() == 3) {
44 !
471
      not_found_ = bg_rgb;
44 ✔
472
    } else {
UNCOV
473
      fatal_error(fmt::format("Bad background RGB in plot {}", id()));
×
474
    }
475
  }
44 ✔
476
}
990 ✔
477

478
void Plot::set_basis(pugi::xml_node plot_node)
893 ✔
479
{
480
  // Copy plot basis
481
  if (PlotType::slice == type_) {
893 ✔
482
    std::string pl_basis = "xy";
838 ✔
483
    if (check_for_node(plot_node, "basis")) {
838 !
484
      pl_basis = get_node_value(plot_node, "basis", true);
838 ✔
485
    }
486
    if ("xy" == pl_basis) {
838 ✔
487
      basis_ = PlotBasis::xy;
764 ✔
488
    } else if ("xz" == pl_basis) {
74 ✔
489
      basis_ = PlotBasis::xz;
22 ✔
490
    } else if ("yz" == pl_basis) {
52 !
491
      basis_ = PlotBasis::yz;
52 ✔
492
    } else {
UNCOV
493
      fatal_error(
×
494
        fmt::format("Unsupported plot basis '{}' in plot {}", pl_basis, id()));
×
495
    }
496
  }
838 ✔
497
}
893 ✔
498

499
void Plot::set_origin(pugi::xml_node plot_node)
893 ✔
500
{
501
  // Copy plotting origin
502
  auto pl_origin = get_node_array<double>(plot_node, "origin");
893 ✔
503
  if (pl_origin.size() == 3) {
893 !
504
    origin_ = pl_origin;
893 ✔
505
  } else {
UNCOV
506
    fatal_error(fmt::format("Origin must be length 3 in plot {}", id()));
×
507
  }
508
}
893 ✔
509

510
void Plot::set_width(pugi::xml_node plot_node)
893 ✔
511
{
512
  // Copy plotting width
513
  vector<double> pl_width = get_node_array<double>(plot_node, "width");
893 ✔
514
  if (PlotType::slice == type_) {
893 ✔
515
    if (pl_width.size() == 2) {
838 !
516
      width_.x = pl_width[0];
838 ✔
517
      width_.y = pl_width[1];
838 ✔
518
      switch (basis_) {
838 !
519
      case PlotBasis::xy:
764 ✔
520
        u_span_ = {width_.x, 0.0, 0.0};
764 ✔
521
        v_span_ = {0.0, width_.y, 0.0};
764 ✔
522
        break;
764 ✔
523
      case PlotBasis::xz:
22 ✔
524
        u_span_ = {width_.x, 0.0, 0.0};
22 ✔
525
        v_span_ = {0.0, 0.0, width_.y};
22 ✔
526
        break;
22 ✔
527
      case PlotBasis::yz:
52 ✔
528
        u_span_ = {0.0, width_.x, 0.0};
52 ✔
529
        v_span_ = {0.0, 0.0, width_.y};
52 ✔
530
        break;
52 ✔
UNCOV
531
      default:
×
532
        UNREACHABLE();
×
533
      }
534
    } else {
UNCOV
535
      fatal_error(
×
536
        fmt::format("<width> must be length 2 in slice plot {}", id()));
×
537
    }
538
  } else if (PlotType::voxel == type_) {
55 !
539
    if (pl_width.size() == 3) {
55 !
540
      pl_width = get_node_array<double>(plot_node, "width");
110 ✔
541
      width_ = pl_width;
55 ✔
542
    } else {
UNCOV
543
      fatal_error(
×
544
        fmt::format("<width> must be length 3 in voxel plot {}", id()));
×
545
    }
546
  }
547
}
893 ✔
548

549
void PlottableInterface::set_universe(pugi::xml_node plot_node)
990 ✔
550
{
551
  // Copy plot universe level
552
  if (check_for_node(plot_node, "level")) {
990 !
UNCOV
553
    level_ = std::stoi(get_node_value(plot_node, "level"));
×
554
    if (level_ < 0) {
×
555
      fatal_error(fmt::format("Bad universe level in plot {}", id()));
×
556
    }
557
  } else {
558
    level_ = PLOT_LEVEL_LOWEST;
990 ✔
559
  }
560
}
990 ✔
561

562
void PlottableInterface::set_color_by(pugi::xml_node plot_node)
990 ✔
563
{
564
  // Copy plot color type
565
  std::string pl_color_by = "cell";
990 ✔
566
  if (check_for_node(plot_node, "color_by")) {
990 ✔
567
    pl_color_by = get_node_value(plot_node, "color_by", true);
957 ✔
568
  }
569
  if ("cell" == pl_color_by) {
990 ✔
570
    color_by_ = PlotColorBy::cells;
287 ✔
571
  } else if ("material" == pl_color_by) {
703 !
572
    color_by_ = PlotColorBy::mats;
703 ✔
573
  } else {
UNCOV
574
    fatal_error(fmt::format(
×
575
      "Unsupported plot color type '{}' in plot {}", pl_color_by, id()));
×
576
  }
577
}
990 ✔
578

579
void PlottableInterface::set_default_colors()
1,001 ✔
580
{
581
  // Copy plot color type and initialize all colors randomly
582
  if (PlotColorBy::cells == color_by_) {
1,001 ✔
583
    colors_.resize(model::cells.size());
287 ✔
584
  } else if (PlotColorBy::mats == color_by_) {
714 !
585
    colors_.resize(model::materials.size());
714 ✔
586
  }
587

588
  for (auto& c : colors_) {
4,475 ✔
589
    c = random_color();
3,474 ✔
590
    // make sure we don't interfere with some default colors
591
    while (c == RED || c == WHITE) {
3,474 !
UNCOV
592
      c = random_color();
×
593
    }
594
  }
595
}
1,001 ✔
596

597
void PlottableInterface::set_user_colors(pugi::xml_node plot_node)
990 ✔
598
{
599
  for (auto cn : plot_node.children("color")) {
1,177 ✔
600
    // Make sure 3 values are specified for RGB
601
    vector<int> user_rgb = get_node_array<int>(cn, "rgb");
187 ✔
602
    if (user_rgb.size() != 3) {
187 !
UNCOV
603
      fatal_error(fmt::format("Bad RGB in plot {}", id()));
×
604
    }
605
    // Ensure that there is an id for this color specification
606
    int col_id;
187 ✔
607
    if (check_for_node(cn, "id")) {
187 !
608
      col_id = std::stoi(get_node_value(cn, "id"));
374 ✔
609
    } else {
UNCOV
610
      fatal_error(fmt::format(
×
611
        "Must specify id for color specification in plot {}", id()));
×
612
    }
613
    // Add RGB
614
    if (PlotColorBy::cells == color_by_) {
187 ✔
615
      if (model::cell_map.find(col_id) != model::cell_map.end()) {
88 !
616
        col_id = model::cell_map[col_id];
88 ✔
617
        colors_[col_id] = user_rgb;
88 ✔
618
      } else {
UNCOV
619
        warning(fmt::format(
×
620
          "Could not find cell {} specified in plot {}", col_id, id()));
×
621
      }
622
    } else if (PlotColorBy::mats == color_by_) {
99 !
623
      if (model::material_map.find(col_id) != model::material_map.end()) {
99 !
624
        col_id = model::material_map[col_id];
99 ✔
625
        colors_[col_id] = user_rgb;
99 ✔
626
      } else {
UNCOV
627
        warning(fmt::format(
×
628
          "Could not find material {} specified in plot {}", col_id, id()));
×
629
      }
630
    }
631
  } // color node loop
187 ✔
632
}
990 ✔
633

634
void Plot::set_meshlines(pugi::xml_node plot_node)
893 ✔
635
{
636
  // Deal with meshlines
637
  pugi::xpath_node_set mesh_line_nodes = plot_node.select_nodes("meshlines");
893 ✔
638

639
  if (!mesh_line_nodes.empty()) {
893 ✔
640
    if (PlotType::voxel == type_) {
33 !
UNCOV
641
      warning(fmt::format("Meshlines ignored in voxel plot {}", id()));
×
642
    }
643

644
    if (mesh_line_nodes.size() == 1) {
33 !
645
      // Get first meshline node
646
      pugi::xml_node meshlines_node = mesh_line_nodes[0].node();
33 ✔
647

648
      // Check mesh type
649
      std::string meshtype;
33 ✔
650
      if (check_for_node(meshlines_node, "meshtype")) {
33 !
651
        meshtype = get_node_value(meshlines_node, "meshtype");
33 ✔
652
      } else {
UNCOV
653
        fatal_error(fmt::format(
×
654
          "Must specify a meshtype for meshlines specification in plot {}",
UNCOV
655
          id()));
×
656
      }
657

658
      // Ensure that there is a linewidth for this meshlines specification
659
      std::string meshline_width;
33 ✔
660
      if (check_for_node(meshlines_node, "linewidth")) {
33 !
661
        meshline_width = get_node_value(meshlines_node, "linewidth");
33 ✔
662
        meshlines_width_ = std::stoi(meshline_width);
33 ✔
663
      } else {
UNCOV
664
        fatal_error(fmt::format(
×
665
          "Must specify a linewidth for meshlines specification in plot {}",
UNCOV
666
          id()));
×
667
      }
668

669
      // Check for color
670
      if (check_for_node(meshlines_node, "color")) {
33 !
671
        // Check and make sure 3 values are specified for RGB
UNCOV
672
        vector<int> ml_rgb = get_node_array<int>(meshlines_node, "color");
×
673
        if (ml_rgb.size() != 3) {
×
674
          fatal_error(
×
675
            fmt::format("Bad RGB for meshlines color in plot {}", id()));
×
676
        }
UNCOV
677
        meshlines_color_ = ml_rgb;
×
678
      }
×
679

680
      // Set mesh based on type
681
      if ("ufs" == meshtype) {
33 !
UNCOV
682
        if (!simulation::ufs_mesh) {
×
683
          fatal_error(
×
684
            fmt::format("No UFS mesh for meshlines on plot {}", id()));
×
685
        } else {
UNCOV
686
          for (int i = 0; i < model::meshes.size(); ++i) {
×
687
            if (const auto* m =
×
688
                  dynamic_cast<const RegularMesh*>(model::meshes[i].get())) {
×
689
              if (m == simulation::ufs_mesh) {
×
690
                index_meshlines_mesh_ = i;
×
691
              }
692
            }
693
          }
UNCOV
694
          if (index_meshlines_mesh_ == -1)
×
695
            fatal_error("Could not find the UFS mesh for meshlines plot");
×
696
        }
697
      } else if ("entropy" == meshtype) {
33 ✔
698
        if (!simulation::entropy_mesh) {
22 !
UNCOV
699
          fatal_error(
×
700
            fmt::format("No entropy mesh for meshlines on plot {}", id()));
×
701
        } else {
702
          for (int i = 0; i < model::meshes.size(); ++i) {
55 ✔
703
            if (const auto* m =
66 ✔
704
                  dynamic_cast<const RegularMesh*>(model::meshes[i].get())) {
55 !
705
              if (m == simulation::entropy_mesh) {
22 !
706
                index_meshlines_mesh_ = i;
22 ✔
707
              }
708
            }
709
          }
710
          if (index_meshlines_mesh_ == -1)
22 !
UNCOV
711
            fatal_error("Could not find the entropy mesh for meshlines plot");
×
712
        }
713
      } else if ("tally" == meshtype) {
11 !
714
        // Ensure that there is a mesh id if the type is tally
715
        int tally_mesh_id;
11 ✔
716
        if (check_for_node(meshlines_node, "id")) {
11 !
717
          tally_mesh_id = std::stoi(get_node_value(meshlines_node, "id"));
22 ✔
718
        } else {
UNCOV
719
          std::stringstream err_msg;
×
720
          fatal_error(fmt::format("Must specify a mesh id for meshlines tally "
×
721
                                  "mesh specification in plot {}",
UNCOV
722
            id()));
×
723
        }
×
724
        // find the tally index
725
        int idx;
11 ✔
726
        int err = openmc_get_mesh_index(tally_mesh_id, &idx);
11 ✔
727
        if (err != 0) {
11 !
UNCOV
728
          fatal_error(fmt::format("Could not find mesh {} specified in "
×
729
                                  "meshlines for plot {}",
UNCOV
730
            tally_mesh_id, id()));
×
731
        }
732
        index_meshlines_mesh_ = idx;
11 ✔
733
      } else {
UNCOV
734
        fatal_error(fmt::format("Invalid type for meshlines on plot {}", id()));
×
735
      }
736
    } else {
33 ✔
UNCOV
737
      fatal_error(fmt::format("Mutliple meshlines specified in plot {}", id()));
×
738
    }
739
  }
740
}
893 ✔
741

742
void PlottableInterface::set_mask(pugi::xml_node plot_node)
990 ✔
743
{
744
  // Deal with masks
745
  pugi::xpath_node_set mask_nodes = plot_node.select_nodes("mask");
990 ✔
746

747
  if (!mask_nodes.empty()) {
990 ✔
748
    if (mask_nodes.size() == 1) {
33 !
749
      // Get pointer to mask
750
      pugi::xml_node mask_node = mask_nodes[0].node();
33 ✔
751

752
      // Determine how many components there are and allocate
753
      vector<int> iarray = get_node_array<int>(mask_node, "components");
33 ✔
754
      if (iarray.size() == 0) {
33 !
UNCOV
755
        fatal_error(
×
756
          fmt::format("Missing <components> in mask of plot {}", id()));
×
757
      }
758

759
      // First we need to change the user-specified identifiers to indices
760
      // in the cell and material arrays
761
      for (auto& col_id : iarray) {
99 ✔
762
        if (PlotColorBy::cells == color_by_) {
66 !
763
          if (model::cell_map.find(col_id) != model::cell_map.end()) {
66 !
764
            col_id = model::cell_map[col_id];
66 ✔
765
          } else {
UNCOV
766
            fatal_error(fmt::format("Could not find cell {} specified in the "
×
767
                                    "mask in plot {}",
UNCOV
768
              col_id, id()));
×
769
          }
UNCOV
770
        } else if (PlotColorBy::mats == color_by_) {
×
771
          if (model::material_map.find(col_id) != model::material_map.end()) {
×
772
            col_id = model::material_map[col_id];
×
773
          } else {
UNCOV
774
            fatal_error(fmt::format("Could not find material {} specified in "
×
775
                                    "the mask in plot {}",
UNCOV
776
              col_id, id()));
×
777
          }
778
        }
779
      }
780

781
      // Alter colors based on mask information
782
      for (int j = 0; j < colors_.size(); j++) {
132 ✔
783
        if (contains(iarray, j)) {
99 ✔
784
          if (check_for_node(mask_node, "background")) {
66 !
785
            vector<int> bg_rgb = get_node_array<int>(mask_node, "background");
66 ✔
786
            colors_[j] = bg_rgb;
66 ✔
787
          } else {
66 ✔
UNCOV
788
            colors_[j] = WHITE;
×
789
          }
790
        }
791
      }
792

793
    } else {
33 ✔
UNCOV
794
      fatal_error(fmt::format("Mutliple masks specified in plot {}", id()));
×
795
    }
796
  }
797
}
990 ✔
798

799
void PlottableInterface::set_overlap_color(pugi::xml_node plot_node)
990 ✔
800
{
801
  color_overlaps_ = false;
990 ✔
802
  if (check_for_node(plot_node, "show_overlaps")) {
990 ✔
803
    color_overlaps_ = get_node_value_bool(plot_node, "show_overlaps");
22 ✔
804
    // check for custom overlap color
805
    if (check_for_node(plot_node, "overlap_color")) {
22 ✔
806
      if (!color_overlaps_) {
11 !
UNCOV
807
        warning(fmt::format(
×
808
          "Overlap color specified in plot {} but overlaps won't be shown.",
UNCOV
809
          id()));
×
810
      }
811
      vector<int> olap_clr = get_node_array<int>(plot_node, "overlap_color");
11 ✔
812
      if (olap_clr.size() == 3) {
11 !
813
        overlap_color_ = olap_clr;
11 ✔
814
      } else {
UNCOV
815
        fatal_error(fmt::format("Bad overlap RGB in plot {}", id()));
×
816
      }
817
    }
11 ✔
818
  }
819

820
  // make sure we allocate the vector for counting overlap checks if
821
  // they're going to be plotted
822
  if (color_overlaps_ && settings::run_mode == RunMode::PLOTTING) {
990 !
823
    settings::check_overlaps = true;
22 ✔
824
    model::overlap_check_count.resize(model::cells.size(), 0);
22 ✔
825
  }
826
}
990 ✔
827

828
PlottableInterface::PlottableInterface(pugi::xml_node plot_node)
990 ✔
829
{
830
  set_id(plot_node);
990 ✔
831
  set_bg_color(plot_node);
990 ✔
832
  set_universe(plot_node);
990 ✔
833
  set_color_by(plot_node);
990 ✔
834
  set_default_colors();
990 ✔
835
  set_user_colors(plot_node);
990 ✔
836
  set_mask(plot_node);
990 ✔
837
  set_overlap_color(plot_node);
990 ✔
838
}
990 ✔
839

840
Plot::Plot(pugi::xml_node plot_node, PlotType type)
902 ✔
841
  : PlottableInterface(plot_node), type_(type), index_meshlines_mesh_ {-1}
902 ✔
842
{
843
  set_output_path(plot_node);
902 ✔
844
  set_basis(plot_node);
893 ✔
845
  set_origin(plot_node);
893 ✔
846
  set_width(plot_node);
893 ✔
847
  set_meshlines(plot_node);
893 ✔
848
  slice_level_ = level_; // Copy level employed in SlicePlotBase::get_map
893 ✔
849
  show_overlaps_ = color_overlaps_;
893 ✔
850
}
893 ✔
851

852
//==============================================================================
853
// OUTPUT_PPM writes out a previously generated image to a PPM file
854
//==============================================================================
855

UNCOV
856
void output_ppm(const std::string& filename, const ImageData& data)
×
857
{
858
  // Open PPM file for writing
UNCOV
859
  std::string fname = filename;
×
860
  fname = strtrim(fname);
×
861
  std::ofstream of;
×
862

UNCOV
863
  of.open(fname);
×
864

865
  // Write header
UNCOV
866
  of << "P6\n";
×
867
  of << data.shape(0) << " " << data.shape(1) << "\n";
×
868
  of << "255\n";
×
869
  of.close();
×
870

UNCOV
871
  of.open(fname, std::ios::binary | std::ios::app);
×
872
  // Write color for each pixel
UNCOV
873
  for (int y = 0; y < data.shape(1); y++) {
×
874
    for (int x = 0; x < data.shape(0); x++) {
×
875
      RGBColor rgb = data(x, y);
×
876
      of << rgb.red << rgb.green << rgb.blue;
×
877
    }
878
  }
UNCOV
879
  of << "\n";
×
880
}
×
881

882
//==============================================================================
883
// OUTPUT_PNG writes out a previously generated image to a PNG file
884
//==============================================================================
885

886
#ifdef USE_LIBPNG
887
void output_png(const std::string& filename, const ImageData& data)
231 ✔
888
{
889
  // Open PNG file for writing
890
  std::string fname = filename;
231 ✔
891
  fname = strtrim(fname);
231 ✔
892
  auto fp = std::fopen(fname.c_str(), "wb");
231 ✔
893

894
  // Initialize write and info structures
895
  auto png_ptr =
231 ✔
896
    png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
231 ✔
897
  auto info_ptr = png_create_info_struct(png_ptr);
231 ✔
898

899
  // Setup exception handling
900
  if (setjmp(png_jmpbuf(png_ptr)))
231 !
UNCOV
901
    fatal_error("Error during png creation");
×
902

903
  png_init_io(png_ptr, fp);
231 ✔
904

905
  // Write header (8 bit colour depth)
906
  int width = data.shape(0);
231 !
907
  int height = data.shape(1);
231 !
908
  png_set_IHDR(png_ptr, info_ptr, width, height, 8, PNG_COLOR_TYPE_RGB,
231 ✔
909
    PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE);
910
  png_write_info(png_ptr, info_ptr);
231 ✔
911

912
  // Allocate memory for one row (3 bytes per pixel - RGB)
913
  std::vector<png_byte> row(3 * width);
231 ✔
914

915
  // Write color for each pixel
916
  for (int y = 0; y < height; y++) {
47,751 ✔
917
    for (int x = 0; x < width; x++) {
11,159,720 ✔
918
      RGBColor rgb = data(x, y);
11,112,200 ✔
919
      row[3 * x] = rgb.red;
11,112,200 ✔
920
      row[3 * x + 1] = rgb.green;
11,112,200 ✔
921
      row[3 * x + 2] = rgb.blue;
11,112,200 ✔
922
    }
923
    png_write_row(png_ptr, row.data());
47,520 ✔
924
  }
925

926
  // End write
927
  png_write_end(png_ptr, nullptr);
231 ✔
928

929
  // Clean up data structures
930
  std::fclose(fp);
231 ✔
931
  png_free_data(png_ptr, info_ptr, PNG_FREE_ALL, -1);
231 ✔
932
  png_destroy_write_struct(&png_ptr, &info_ptr);
231 ✔
933
}
231 ✔
934
#endif
935

936
//==============================================================================
937
// DRAW_MESH_LINES draws mesh line boundaries on an image
938
//==============================================================================
939

940
void Plot::draw_mesh_lines(ImageData& data) const
33 ✔
941
{
942
  RGBColor rgb;
33 !
943
  rgb = meshlines_color_;
33 ✔
944

945
  int ax1, ax2;
33 ✔
946
  Position expected_u {};
33 ✔
947
  Position expected_v {};
33 ✔
948
  switch (basis_) {
33 !
949
  case PlotBasis::xy:
22 ✔
950
    ax1 = 0;
22 ✔
951
    ax2 = 1;
22 ✔
952
    expected_u = {width_[0], 0.0, 0.0};
22 ✔
953
    expected_v = {0.0, width_[1], 0.0};
22 ✔
954
    break;
22 ✔
955
  case PlotBasis::xz:
11 ✔
956
    ax1 = 0;
11 ✔
957
    ax2 = 2;
11 ✔
958
    expected_u = {width_[0], 0.0, 0.0};
11 ✔
959
    expected_v = {0.0, 0.0, width_[1]};
11 ✔
960
    break;
11 ✔
UNCOV
961
  case PlotBasis::yz:
×
962
    ax1 = 1;
×
963
    ax2 = 2;
×
964
    expected_u = {0.0, width_[0], 0.0};
×
965
    expected_v = {0.0, 0.0, width_[1]};
×
966
    break;
×
967
  default:
×
968
    UNREACHABLE();
×
969
  }
970

971
  // Meshlines rely on axis-aligned indexing in global coordinates.
972
  constexpr double rel_tol {1e-12};
33 ✔
973
  double span_tol = rel_tol * (1.0 + u_span_.norm() + v_span_.norm());
33 ✔
974
  if ((u_span_ - expected_u).norm() > span_tol ||
66 !
975
      (v_span_ - expected_v).norm() > span_tol) {
33 ✔
UNCOV
976
    fatal_error("Meshlines are only supported for axis-aligned slice plots.");
×
977
  }
978

979
  Position ll_plot {origin_};
33 ✔
980
  Position ur_plot {origin_};
33 ✔
981

982
  ll_plot[ax1] -= width_[0] / 2.;
33 ✔
983
  ll_plot[ax2] -= width_[1] / 2.;
33 ✔
984
  ur_plot[ax1] += width_[0] / 2.;
33 ✔
985
  ur_plot[ax2] += width_[1] / 2.;
33 ✔
986

987
  Position width = ur_plot - ll_plot;
33 ✔
988

989
  // Find the (axis-aligned) lines of the mesh that intersect this plot.
990
  auto axis_lines =
33 ✔
991
    model::meshes[index_meshlines_mesh_]->plot(ll_plot, ur_plot);
33 ✔
992

993
  // Find the bounds along the second axis (accounting for low-D meshes).
994
  int ax2_min, ax2_max;
33 ✔
995
  if (axis_lines.second.size() > 0) {
33 !
996
    double frac = (axis_lines.second.back() - ll_plot[ax2]) / width[ax2];
33 ✔
997
    ax2_min = (1.0 - frac) * pixels()[1];
33 ✔
998
    if (ax2_min < 0)
33 ✔
999
      ax2_min = 0;
1000
    frac = (axis_lines.second.front() - ll_plot[ax2]) / width[ax2];
33 ✔
1001
    ax2_max = (1.0 - frac) * pixels()[1];
33 !
1002
    if (ax2_max > pixels()[1])
33 !
UNCOV
1003
      ax2_max = pixels()[1];
×
1004
  } else {
UNCOV
1005
    ax2_min = 0;
×
1006
    ax2_max = pixels()[1];
×
1007
  }
1008

1009
  // Iterate across the first axis and draw lines.
1010
  for (auto ax1_val : axis_lines.first) {
187 ✔
1011
    double frac = (ax1_val - ll_plot[ax1]) / width[ax1];
154 ✔
1012
    int ax1_ind = frac * pixels()[0];
154 ✔
1013
    for (int ax2_ind = ax2_min; ax2_ind < ax2_max; ++ax2_ind) {
24,948 ✔
1014
      for (int plus = 0; plus <= meshlines_width_; plus++) {
49,588 ✔
1015
        if (ax1_ind + plus >= 0 && ax1_ind + plus < pixels()[0])
24,794 !
1016
          data(ax1_ind + plus, ax2_ind) = rgb;
24,794 ✔
1017
        if (ax1_ind - plus >= 0 && ax1_ind - plus < pixels()[0])
24,794 !
1018
          data(ax1_ind - plus, ax2_ind) = rgb;
24,794 ✔
1019
      }
1020
    }
1021
  }
1022

1023
  // Find the bounds along the first axis.
1024
  int ax1_min, ax1_max;
33 ✔
1025
  if (axis_lines.first.size() > 0) {
33 !
1026
    double frac = (axis_lines.first.front() - ll_plot[ax1]) / width[ax1];
33 ✔
1027
    ax1_min = frac * pixels()[0];
33 ✔
1028
    if (ax1_min < 0)
33 ✔
1029
      ax1_min = 0;
1030
    frac = (axis_lines.first.back() - ll_plot[ax1]) / width[ax1];
33 ✔
1031
    ax1_max = frac * pixels()[0];
33 !
1032
    if (ax1_max > pixels()[0])
33 !
UNCOV
1033
      ax1_max = pixels()[0];
×
1034
  } else {
UNCOV
1035
    ax1_min = 0;
×
1036
    ax1_max = pixels()[0];
×
1037
  }
1038

1039
  // Iterate across the second axis and draw lines.
1040
  for (auto ax2_val : axis_lines.second) {
209 ✔
1041
    double frac = (ax2_val - ll_plot[ax2]) / width[ax2];
176 ✔
1042
    int ax2_ind = (1.0 - frac) * pixels()[1];
176 ✔
1043
    for (int ax1_ind = ax1_min; ax1_ind < ax1_max; ++ax1_ind) {
28,336 ✔
1044
      for (int plus = 0; plus <= meshlines_width_; plus++) {
56,320 ✔
1045
        if (ax2_ind + plus >= 0 && ax2_ind + plus < pixels()[1])
28,160 !
1046
          data(ax1_ind, ax2_ind + plus) = rgb;
28,160 ✔
1047
        if (ax2_ind - plus >= 0 && ax2_ind - plus < pixels()[1])
28,160 !
1048
          data(ax1_ind, ax2_ind - plus) = rgb;
28,160 ✔
1049
      }
1050
    }
1051
  }
1052
}
33 ✔
1053

1054
/* outputs a binary file that can be input into silomesh for 3D geometry
1055
 * visualization.  It works the same way as create_image by dragging a particle
1056
 * across the geometry for the specified number of voxels. The first 3 int's in
1057
 * the binary are the number of x, y, and z voxels.  The next 3 double's are
1058
 * the widths of the voxels in the x, y, and z directions. The next 3 double's
1059
 * are the x, y, and z coordinates of the lower left point. Finally the binary
1060
 * is filled with entries of four int's each. Each 'row' in the binary contains
1061
 * four int's: 3 for x,y,z position and 1 for cell or material id.  For 1
1062
 * million voxels this produces a file of approximately 15MB.
1063
 */
1064
void Plot::create_voxel() const
55 ✔
1065
{
1066
  // compute voxel widths in each direction
1067
  array<double, 3> vox;
55 ✔
1068
  vox[0] = width_[0] / static_cast<double>(pixels()[0]);
55 ✔
1069
  vox[1] = width_[1] / static_cast<double>(pixels()[1]);
55 ✔
1070
  vox[2] = width_[2] / static_cast<double>(pixels()[2]);
55 ✔
1071

1072
  // initial particle position
1073
  Position ll = origin_ - width_ / 2.;
55 ✔
1074

1075
  // Open binary plot file for writing
1076
  std::ofstream of;
55 ✔
1077
  std::string fname = std::string(path_plot_);
55 ✔
1078
  fname = strtrim(fname);
55 ✔
1079
  hid_t file_id = file_open(fname, 'w');
55 ✔
1080

1081
  // write header info
1082
  write_attribute(file_id, "filetype", "voxel");
55 ✔
1083
  write_attribute(file_id, "version", VERSION_VOXEL);
55 ✔
1084
  write_attribute(file_id, "openmc_version", VERSION);
55 ✔
1085

1086
#ifdef GIT_SHA1
1087
  write_attribute(file_id, "git_sha1", GIT_SHA1);
1088
#endif
1089

1090
  // Write current date and time
1091
  write_attribute(file_id, "date_and_time", time_stamp().c_str());
110 ✔
1092
  array<int, 3> h5_pixels;
55 ✔
1093
  std::copy(pixels().begin(), pixels().end(), h5_pixels.begin());
55 ✔
1094
  write_attribute(file_id, "num_voxels", h5_pixels);
55 ✔
1095
  write_attribute(file_id, "voxel_width", vox);
55 ✔
1096
  write_attribute(file_id, "lower_left", ll);
55 ✔
1097

1098
  // Create dataset for voxel data -- note that the dimensions are reversed
1099
  // since we want the order in the file to be z, y, x
1100
  hsize_t dims[3];
55 ✔
1101
  dims[0] = pixels()[2];
55 ✔
1102
  dims[1] = pixels()[1];
55 ✔
1103
  dims[2] = pixels()[0];
55 ✔
1104
  hid_t dspace, dset, memspace;
55 ✔
1105
  voxel_init(file_id, &(dims[0]), &dspace, &dset, &memspace);
55 ✔
1106

1107
  SlicePlotBase pltbase;
55 ✔
1108
  pltbase.origin_ = origin_;
55 ✔
1109
  pltbase.u_span_ = {width_.x, 0.0, 0.0};
55 ✔
1110
  pltbase.v_span_ = {0.0, width_.y, 0.0};
55 ✔
1111
  pltbase.pixels() = pixels();
55 ✔
1112
  pltbase.show_overlaps_ = color_overlaps_;
55 ✔
1113

1114
  ProgressBar pb;
55 ✔
1115
  for (int z = 0; z < pixels()[2]; z++) {
4,785 ✔
1116
    // update z coordinate
1117
    pltbase.origin_.z = ll.z + z * vox[2];
4,730 ✔
1118

1119
    // generate ids using plotbase
1120
    IdData ids = pltbase.get_map<IdData>();
4,730 ✔
1121

1122
    // select only cell/material ID data and flip the y-axis
1123
    int idx = color_by_ == PlotColorBy::cells ? 0 : 2;
4,730 !
1124
    // Extract 2D slice at index idx from 3D data
1125
    size_t rows = ids.data_.shape(0);
4,730 !
1126
    size_t cols = ids.data_.shape(1);
4,730 !
1127
    tensor::Tensor<int32_t> data_slice({rows, cols});
4,730 ✔
1128
    for (size_t r = 0; r < rows; ++r)
912,230 ✔
1129
      for (size_t c = 0; c < cols; ++c)
179,382,500 ✔
1130
        data_slice(r, c) = ids.data_(r, c, idx);
178,475,000 ✔
1131
    tensor::Tensor<int32_t> data_flipped = data_slice.flip(0);
4,730 ✔
1132

1133
    // Write to HDF5 dataset
1134
    voxel_write_slice(z, dspace, dset, memspace, data_flipped.data());
4,730 ✔
1135

1136
    // update progress bar
1137
    pb.set_value(
4,730 ✔
1138
      100. * static_cast<double>(z + 1) / static_cast<double>((pixels()[2])));
4,730 ✔
1139
  }
14,190 ✔
1140

1141
  voxel_finalize(dspace, dset, memspace);
55 ✔
1142
  file_close(file_id);
55 ✔
1143
}
55 ✔
1144

1145
void voxel_init(hid_t file_id, const hsize_t* dims, hid_t* dspace, hid_t* dset,
55 ✔
1146
  hid_t* memspace)
1147
{
1148
  // Create dataspace/dataset for voxel data
1149
  *dspace = H5Screate_simple(3, dims, nullptr);
55 ✔
1150
  *dset = H5Dcreate(file_id, "data", H5T_NATIVE_INT, *dspace, H5P_DEFAULT,
55 ✔
1151
    H5P_DEFAULT, H5P_DEFAULT);
1152

1153
  // Create dataspace for a slice of the voxel
1154
  hsize_t dims_slice[2] {dims[1], dims[2]};
55 ✔
1155
  *memspace = H5Screate_simple(2, dims_slice, nullptr);
55 ✔
1156

1157
  // Select hyperslab in dataspace
1158
  hsize_t start[3] {0, 0, 0};
55 ✔
1159
  hsize_t count[3] {1, dims[1], dims[2]};
55 ✔
1160
  H5Sselect_hyperslab(*dspace, H5S_SELECT_SET, start, nullptr, count, nullptr);
55 ✔
1161
}
55 ✔
1162

1163
void voxel_write_slice(
4,730 ✔
1164
  int x, hid_t dspace, hid_t dset, hid_t memspace, void* buf)
1165
{
1166
  hssize_t offset[3] {x, 0, 0};
4,730 ✔
1167
  H5Soffset_simple(dspace, offset);
4,730 ✔
1168
  H5Dwrite(dset, H5T_NATIVE_INT, memspace, dspace, H5P_DEFAULT, buf);
4,730 ✔
1169
}
4,730 ✔
1170

1171
void voxel_finalize(hid_t dspace, hid_t dset, hid_t memspace)
55 ✔
1172
{
1173
  H5Dclose(dset);
55 ✔
1174
  H5Sclose(dspace);
55 ✔
1175
  H5Sclose(memspace);
55 ✔
1176
}
55 ✔
1177

1178
RGBColor random_color(void)
3,474 ✔
1179
{
1180
  return {int(prn(&model::plotter_seed) * 255),
3,474 ✔
1181
    int(prn(&model::plotter_seed) * 255), int(prn(&model::plotter_seed) * 255)};
3,474 ✔
1182
}
1183

1184
RayTracePlot::RayTracePlot(pugi::xml_node node) : PlottableInterface(node)
88 ✔
1185
{
1186
  set_look_at(node);
88 ✔
1187
  set_camera_position(node);
88 ✔
1188
  set_field_of_view(node);
88 ✔
1189
  set_pixels(node);
88 ✔
1190
  set_orthographic_width(node);
88 ✔
1191
  set_output_path(node);
88 ✔
1192

1193
  if (check_for_node(node, "orthographic_width") &&
99 !
1194
      check_for_node(node, "field_of_view"))
11 ✔
UNCOV
1195
    fatal_error("orthographic_width and field_of_view are mutually exclusive "
×
1196
                "parameters.");
1197
}
88 ✔
1198

1199
void RayTracePlot::update_view()
110 ✔
1200
{
1201
  // Get centerline vector for camera-to-model. We create vectors around this
1202
  // that form a pixel array, and then trace rays along that.
1203
  auto up = up_ / up_.norm();
110 ✔
1204
  Direction looking_direction = look_at_ - camera_position_;
110 ✔
1205
  looking_direction /= looking_direction.norm();
110 ✔
1206
  if (std::abs(std::abs(looking_direction.dot(up)) - 1.0) < 1e-9)
110 !
UNCOV
1207
    fatal_error("Up vector cannot align with vector between camera position "
×
1208
                "and look_at!");
1209
  Direction cam_yaxis = looking_direction.cross(up);
110 ✔
1210
  cam_yaxis /= cam_yaxis.norm();
110 ✔
1211
  Direction cam_zaxis = cam_yaxis.cross(looking_direction);
110 ✔
1212
  cam_zaxis /= cam_zaxis.norm();
110 ✔
1213

1214
  // Cache the camera-to-model matrix
1215
  camera_to_model_ = {looking_direction.x, cam_yaxis.x, cam_zaxis.x,
110 ✔
1216
    looking_direction.y, cam_yaxis.y, cam_zaxis.y, looking_direction.z,
110 ✔
1217
    cam_yaxis.z, cam_zaxis.z};
110 ✔
1218
}
110 ✔
1219

1220
WireframeRayTracePlot::WireframeRayTracePlot(pugi::xml_node node)
55 ✔
1221
  : RayTracePlot(node)
55 ✔
1222
{
1223
  set_opacities(node);
55 ✔
1224
  set_wireframe_thickness(node);
55 ✔
1225
  set_wireframe_ids(node);
55 ✔
1226
  set_wireframe_color(node);
55 ✔
1227
  update_view();
55 ✔
1228
}
55 ✔
1229

1230
void WireframeRayTracePlot::set_wireframe_color(pugi::xml_node plot_node)
55 ✔
1231
{
1232
  // Copy plot wireframe color
1233
  if (check_for_node(plot_node, "wireframe_color")) {
55 !
UNCOV
1234
    vector<int> w_rgb = get_node_array<int>(plot_node, "wireframe_color");
×
1235
    if (w_rgb.size() == 3) {
×
1236
      wireframe_color_ = w_rgb;
×
1237
    } else {
UNCOV
1238
      fatal_error(fmt::format("Bad wireframe RGB in plot {}", id()));
×
1239
    }
UNCOV
1240
  }
×
1241
}
55 ✔
1242

1243
void RayTracePlot::set_output_path(pugi::xml_node node)
88 ✔
1244
{
1245
  // Set output file path
1246
  std::string filename;
88 ✔
1247

1248
  if (check_for_node(node, "filename")) {
88 ✔
1249
    filename = get_node_value(node, "filename");
77 ✔
1250
  } else {
1251
    filename = fmt::format("plot_{}", id());
11 ✔
1252
  }
1253

1254
#ifdef USE_LIBPNG
1255
  if (!file_extension_present(filename, "png"))
88 ✔
1256
    filename.append(".png");
33 ✔
1257
#else
1258
  if (!file_extension_present(filename, "ppm"))
1259
    filename.append(".ppm");
1260
#endif
1261
  path_plot_ = filename;
176 ✔
1262
}
88 ✔
1263

1264
bool WireframeRayTracePlot::trackstack_equivalent(
3,041,159 ✔
1265
  const std::vector<TrackSegment>& track1,
1266
  const std::vector<TrackSegment>& track2) const
1267
{
1268
  if (wireframe_ids_.empty()) {
3,041,159 ✔
1269
    // Draw wireframe for all surfaces/cells/materials
1270
    if (track1.size() != track2.size())
2,545,070 ✔
1271
      return false;
1272
    for (int i = 0; i < track1.size(); ++i) {
6,707,954 ✔
1273
      if (track1[i].id != track2[i].id ||
4,236,771 ✔
1274
          track1[i].surface_index != track2[i].surface_index) {
4,236,639 ✔
1275
        return false;
1276
      }
1277
    }
1278
    return true;
1279
  } else {
1280
    // This runs in O(nm) where n is the intersection stack size
1281
    // and m is the number of IDs we are wireframing. A simpler
1282
    // algorithm can likely be found.
1283
    for (const int id : wireframe_ids_) {
986,194 ✔
1284
      int t1_i = 0;
496,089 ✔
1285
      int t2_i = 0;
496,089 ✔
1286

1287
      // Advance to first instance of the ID
1288
      while (t1_i < track1.size() && t2_i < track2.size()) {
562,430 ✔
1289
        while (t1_i < track1.size() && track1[t1_i].id != id)
392,832 ✔
1290
          t1_i++;
229,053 ✔
1291
        while (t2_i < track2.size() && track2[t2_i].id != id)
393,668 ✔
1292
          t2_i++;
229,889 ✔
1293

1294
        // This one is really important!
1295
        if ((t1_i == track1.size() && t2_i != track2.size()) ||
163,779 ✔
1296
            (t1_i != track1.size() && t2_i == track2.size()))
162,096 ✔
1297
          return false;
3,718 ✔
1298
        if (t1_i == track1.size() && t2_i == track2.size())
160,061 !
1299
          break;
1300
        // Check if surface different
1301
        if (track1[t1_i].surface_index != track2[t2_i].surface_index)
68,607 ✔
1302
          return false;
1303

1304
        // Pretty sure this should not be used:
1305
        // if (t2_i != track2.size() - 1 &&
1306
        //     t1_i != track1.size() - 1 &&
1307
        //     track1[t1_i+1].id != track2[t2_i+1].id) return false;
1308
        if (t2_i != 0 && t1_i != 0 &&
67,122 ✔
1309
            track1[t1_i - 1].surface_index != track2[t2_i - 1].surface_index)
53,944 ✔
1310
          return false;
1311

1312
        // Check if neighboring cells are different
1313
        // if (track1[t1_i ? t1_i - 1 : 0].id != track2[t2_i ? t2_i - 1 : 0].id)
1314
        // return false; if (track1[t1_i < track1.size() - 1 ? t1_i + 1 : t1_i
1315
        // ].id !=
1316
        //    track2[t2_i < track2.size() - 1 ? t2_i + 1 : t2_i].id) return
1317
        //    false;
1318
        t1_i++, t2_i++;
66,341 ✔
1319
      }
1320
    }
1321
    return true;
1322
  }
1323
}
1324

1325
std::pair<Position, Direction> RayTracePlot::get_pixel_ray(
3,521,056 ✔
1326
  int horiz, int vert) const
1327
{
1328
  // Compute field of view in radians
1329
  constexpr double DEGREE_TO_RADIAN = PI / 180.0;
3,521,056 ✔
1330
  double horiz_fov_radians = horizontal_field_of_view_ * DEGREE_TO_RADIAN;
3,521,056 ✔
1331
  double p0 = static_cast<double>(pixels()[0]);
3,521,056 ✔
1332
  double p1 = static_cast<double>(pixels()[1]);
3,521,056 ✔
1333

1334
  // focal_plane_dist can be changed to alter the perspective distortion
1335
  // effect. This is in units of cm. This seems to look good most of the
1336
  // time. TODO let this variable be set through XML.
1337
  constexpr double focal_plane_dist = 10.0;
3,521,056 ✔
1338
  const double dx = 2.0 * focal_plane_dist * std::tan(0.5 * horiz_fov_radians);
3,521,056 ✔
1339
  const double dy = p1 / p0 * dx;
3,521,056 ✔
1340

1341
  std::pair<Position, Direction> result;
3,521,056 ✔
1342

1343
  // Generate the starting position/direction of the ray
1344
  if (orthographic_width_ == C_NONE) { // perspective projection
3,521,056 ✔
1345
    Direction camera_local_vec;
3,081,056 ✔
1346
    camera_local_vec.x = focal_plane_dist;
3,081,056 ✔
1347
    camera_local_vec.y = -0.5 * dx + horiz * dx / p0;
3,081,056 ✔
1348
    camera_local_vec.z = 0.5 * dy - vert * dy / p1;
3,081,056 ✔
1349
    camera_local_vec /= camera_local_vec.norm();
3,081,056 ✔
1350

1351
    result.first = camera_position_;
3,081,056 ✔
1352
    result.second = camera_local_vec.rotate(camera_to_model_);
3,081,056 ✔
1353
  } else { // orthographic projection
1354

1355
    double x_pix_coord = (static_cast<double>(horiz) - p0 / 2.0) / p0;
440,000 ✔
1356
    double y_pix_coord = (static_cast<double>(vert) - p1 / 2.0) / p1;
440,000 ✔
1357

1358
    result.first = camera_position_ +
440,000 ✔
1359
                   camera_y_axis() * x_pix_coord * orthographic_width_ +
440,000 ✔
1360
                   camera_z_axis() * y_pix_coord * orthographic_width_;
440,000 ✔
1361
    result.second = camera_x_axis();
440,000 ✔
1362
  }
1363

1364
  return result;
3,521,056 ✔
1365
}
1366

1367
ImageData WireframeRayTracePlot::create_image() const
55 ✔
1368
{
1369
  size_t width = pixels()[0];
55 ✔
1370
  size_t height = pixels()[1];
55 ✔
1371
  ImageData data({width, height}, not_found_);
55 ✔
1372

1373
  // This array marks where the initial wireframe was drawn. We convolve it with
1374
  // a filter that gets adjusted with the wireframe thickness in order to
1375
  // thicken the lines.
1376
  tensor::Tensor<int> wireframe_initial(
55 ✔
1377
    {static_cast<size_t>(width), static_cast<size_t>(height)}, 0);
55 ✔
1378

1379
  /* Holds all of the track segments for the current rendered line of pixels.
1380
   * old_segments holds a copy of this_line_segments from the previous line.
1381
   * By holding both we can check if the cell/material intersection stack
1382
   * differs from the left or upper neighbor. This allows a robustly drawn
1383
   * wireframe. If only checking the left pixel (which requires substantially
1384
   * less memory), the wireframe tends to be spotty and be disconnected for
1385
   * surface edges oriented horizontally in the rendering.
1386
   *
1387
   * Note that a vector of vectors is required rather than a 2-tensor,
1388
   * since the stack size varies within each column.
1389
   */
1390
  const int n_threads = num_threads();
55 ✔
1391
  std::vector<std::vector<std::vector<TrackSegment>>> this_line_segments(
55 ✔
1392
    n_threads);
55 ✔
1393
  for (int t = 0; t < n_threads; ++t) {
140 ✔
1394
    this_line_segments[t].resize(pixels()[0]);
85 ✔
1395
  }
1396

1397
  // The last thread writes to this, and the first thread reads from it.
1398
  std::vector<std::vector<TrackSegment>> old_segments(pixels()[0]);
55 ✔
1399

1400
#pragma omp parallel
30 ✔
1401
  {
25 ✔
1402
    const int n_threads = num_threads();
25 ✔
1403
    const int tid = thread_num();
25 ✔
1404

1405
    int vert = tid;
25 ✔
1406
    for (int iter = 0; iter <= pixels()[1] / n_threads; iter++) {
5,050 ✔
1407

1408
      // Save bottom line of current work chunk to compare against later. This
1409
      // used to be inside the below if block, but it causes a spurious line to
1410
      // be drawn at the bottom of the image. Not sure why, but moving it here
1411
      // fixes things.
1412
      if (tid == n_threads - 1)
5,025 ✔
1413
        old_segments = this_line_segments[n_threads - 1];
5,025 ✔
1414

1415
      if (vert < pixels()[1]) {
5,025 ✔
1416

1417
        for (int horiz = 0; horiz < pixels()[0]; ++horiz) {
1,005,000 ✔
1418

1419
          // RayTracePlot implements camera ray generation
1420
          std::pair<Position, Direction> ru = get_pixel_ray(horiz, vert);
1,000,000 ✔
1421

1422
          this_line_segments[tid][horiz].clear();
1,000,000 ✔
1423
          ProjectionRay ray(
1,000,000 ✔
1424
            ru.first, ru.second, *this, this_line_segments[tid][horiz]);
1,000,000 ✔
1425

1426
          ray.trace();
1,000,000 ✔
1427

1428
          // Now color the pixel based on what we have intersected...
1429
          // Loops backwards over intersections.
1430
          Position current_color(
1,000,000 ✔
1431
            not_found_.red, not_found_.green, not_found_.blue);
1,000,000 ✔
1432
          const auto& segments = this_line_segments[tid][horiz];
1,000,000 ✔
1433

1434
          // There must be at least two cell intersections to color, front and
1435
          // back of the cell. Maybe an infinitely thick cell could be present
1436
          // with no back, but why would you want to color that? It's easier to
1437
          // just skip that edge case and not even color it.
1438
          if (segments.size() <= 1)
1,000,000 ✔
1439
            continue;
616,655 ✔
1440

1441
          for (int i = segments.size() - 2; i >= 0; --i) {
1,072,335 ✔
1442
            int colormap_idx = segments[i].id;
688,990 ✔
1443
            RGBColor seg_color = colors_[colormap_idx];
688,990 ✔
1444
            Position seg_color_vec(
688,990 ✔
1445
              seg_color.red, seg_color.green, seg_color.blue);
688,990 ✔
1446
            double mixing =
688,990 ✔
1447
              std::exp(-xs_[colormap_idx] *
1,377,980 ✔
1448
                       (segments[i + 1].length - segments[i].length));
688,990 ✔
1449
            current_color =
688,990 ✔
1450
              current_color * mixing + (1.0 - mixing) * seg_color_vec;
688,990 ✔
1451
          }
1452

1453
          // save result converting from double-precision color coordinates to
1454
          // byte-sized
1455
          RGBColor result;
383,345 ✔
1456
          result.red = static_cast<uint8_t>(current_color.x);
383,345 ✔
1457
          result.green = static_cast<uint8_t>(current_color.y);
383,345 ✔
1458
          result.blue = static_cast<uint8_t>(current_color.z);
383,345 ✔
1459
          data(horiz, vert) = result;
383,345 ✔
1460

1461
          // Check to draw wireframe in horizontal direction. No inter-thread
1462
          // comm.
1463
          if (horiz > 0) {
383,345 ✔
1464
            if (!trackstack_equivalent(this_line_segments[tid][horiz],
382,345 ✔
1465
                  this_line_segments[tid][horiz - 1])) {
382,345 ✔
1466
              wireframe_initial(horiz, vert) = 1;
15,710 ✔
1467
            }
1468
          }
1469
        }
1,000,000 ✔
1470
      } // end "if" vert in correct range
1471

1472
      // We require a barrier before comparing vertical neighbors' intersection
1473
      // stacks. i.e. all threads must be done with their line.
1474
#pragma omp barrier
1475

1476
      // Now that the horizontal line has finished rendering, we can fill in
1477
      // wireframe entries that require comparison among all the threads. Hence
1478
      // the omp barrier being used. It has to be OUTSIDE any if blocks!
1479
      if (vert < pixels()[1]) {
5,025 ✔
1480
        // Loop over horizontal pixels, checking intersection stack of upper
1481
        // neighbor
1482

1483
        const std::vector<std::vector<TrackSegment>>* top_cmp = nullptr;
1484
        if (tid == 0)
1485
          top_cmp = &old_segments;
1486
        else
1487
          top_cmp = &this_line_segments[tid - 1];
1488

1489
        for (int horiz = 0; horiz < pixels()[0]; ++horiz) {
1,005,000 ✔
1490
          if (!trackstack_equivalent(
1,000,000 ✔
1491
                this_line_segments[tid][horiz], (*top_cmp)[horiz])) {
1,000,000 ✔
1492
            wireframe_initial(horiz, vert) = 1;
20,595 ✔
1493
          }
1494
        }
1495
      }
1496

1497
      // We need another barrier to ensure threads don't proceed to modify their
1498
      // intersection stacks on that horizontal line while others are
1499
      // potentially still working on the above.
1500
#pragma omp barrier
1501
      vert += n_threads;
5,025 ✔
1502
    }
1503
  } // end omp parallel
1504

1505
  // Now thicken the wireframe lines and apply them to our image
1506
  for (int vert = 0; vert < pixels()[1]; ++vert) {
11,055 ✔
1507
    for (int horiz = 0; horiz < pixels()[0]; ++horiz) {
2,211,000 ✔
1508
      if (wireframe_initial(horiz, vert)) {
2,200,000 ✔
1509
        if (wireframe_thickness_ == 1)
70,983 ✔
1510
          data(horiz, vert) = wireframe_color_;
30,195 ✔
1511
        for (int i = -wireframe_thickness_ / 2; i < wireframe_thickness_ / 2;
195,723 ✔
1512
             ++i)
1513
          for (int j = -wireframe_thickness_ / 2; j < wireframe_thickness_ / 2;
546,876 ✔
1514
               ++j)
1515
            if (i * i + j * j < wireframe_thickness_ * wireframe_thickness_) {
422,136 !
1516

1517
              // Check if wireframe pixel is out of bounds
1518
              int w_i = std::clamp(horiz + i, 0, pixels()[0] - 1);
422,136 ✔
1519
              int w_j = std::clamp(vert + j, 0, pixels()[1] - 1);
422,136 ✔
1520
              data(w_i, w_j) = wireframe_color_;
422,136 ✔
1521
            }
1522
      }
1523
    }
1524
  }
1525

1526
  return data;
110 ✔
1527
}
110 ✔
1528

1529
void WireframeRayTracePlot::create_output() const
55 ✔
1530
{
1531
  ImageData data = create_image();
55 ✔
1532
  write_image(data);
55 ✔
1533
}
55 ✔
1534

1535
void RayTracePlot::print_info() const
88 ✔
1536
{
1537
  fmt::print("Camera position: {} {} {}\n", camera_position_.x,
176 ✔
1538
    camera_position_.y, camera_position_.z);
88 ✔
1539
  fmt::print("Look at: {} {} {}\n", look_at_.x, look_at_.y, look_at_.z);
88 ✔
1540
  fmt::print(
176 ✔
1541
    "Horizontal field of view: {} degrees\n", horizontal_field_of_view_);
88 ✔
1542
  fmt::print("Pixels: {} {}\n", pixels()[0], pixels()[1]);
88 ✔
1543
}
88 ✔
1544

1545
void WireframeRayTracePlot::print_info() const
55 ✔
1546
{
1547
  fmt::print("Plot Type: Wireframe ray-traced\n");
55 ✔
1548
  RayTracePlot::print_info();
55 ✔
1549
}
55 ✔
1550

1551
void WireframeRayTracePlot::set_opacities(pugi::xml_node node)
55 ✔
1552
{
1553
  xs_.resize(colors_.size(), 1e6); // set to large value for opaque by default
55 ✔
1554

1555
  for (auto cn : node.children("color")) {
121 ✔
1556
    // Make sure 3 values are specified for RGB
1557
    double user_xs = std::stod(get_node_value(cn, "xs"));
132 ✔
1558
    int col_id = std::stoi(get_node_value(cn, "id"));
132 ✔
1559

1560
    // Add RGB
1561
    if (PlotColorBy::cells == color_by_) {
66 !
1562
      if (model::cell_map.find(col_id) != model::cell_map.end()) {
66 !
1563
        col_id = model::cell_map[col_id];
66 ✔
1564
        xs_[col_id] = user_xs;
66 ✔
1565
      } else {
UNCOV
1566
        warning(fmt::format(
×
UNCOV
1567
          "Could not find cell {} specified in plot {}", col_id, id()));
×
1568
      }
1569
    } else if (PlotColorBy::mats == color_by_) {
×
UNCOV
1570
      if (model::material_map.find(col_id) != model::material_map.end()) {
×
1571
        col_id = model::material_map[col_id];
×
1572
        xs_[col_id] = user_xs;
×
1573
      } else {
1574
        warning(fmt::format(
×
UNCOV
1575
          "Could not find material {} specified in plot {}", col_id, id()));
×
1576
      }
1577
    }
1578
  }
1579
}
55 ✔
1580

1581
void RayTracePlot::set_orthographic_width(pugi::xml_node node)
88 ✔
1582
{
1583
  if (check_for_node(node, "orthographic_width")) {
88 ✔
1584
    double orthographic_width =
11 ✔
1585
      std::stod(get_node_value(node, "orthographic_width", true));
11 ✔
1586
    if (orthographic_width < 0.0)
11 !
UNCOV
1587
      fatal_error("Requires positive orthographic_width");
×
1588
    orthographic_width_ = orthographic_width;
11 ✔
1589
  }
1590
}
88 ✔
1591

1592
void WireframeRayTracePlot::set_wireframe_thickness(pugi::xml_node node)
55 ✔
1593
{
1594
  if (check_for_node(node, "wireframe_thickness")) {
55 ✔
1595
    int wireframe_thickness =
22 ✔
1596
      std::stoi(get_node_value(node, "wireframe_thickness", true));
22 ✔
1597
    if (wireframe_thickness < 0)
22 !
UNCOV
1598
      fatal_error("Requires non-negative wireframe thickness");
×
1599
    wireframe_thickness_ = wireframe_thickness;
22 ✔
1600
  }
1601
}
55 ✔
1602

1603
void WireframeRayTracePlot::set_wireframe_ids(pugi::xml_node node)
55 ✔
1604
{
1605
  if (check_for_node(node, "wireframe_ids")) {
55 ✔
1606
    wireframe_ids_ = get_node_array<int>(node, "wireframe_ids");
11 ✔
1607
    // It is read in as actual ID values, but we have to convert to indices in
1608
    // mat/cell array
1609
    for (auto& x : wireframe_ids_)
22 ✔
1610
      x = color_by_ == PlotColorBy::mats ? model::material_map[x]
22 !
UNCOV
1611
                                         : model::cell_map[x];
×
1612
  }
1613
  // We make sure the list is sorted in order to later use
1614
  // std::binary_search.
1615
  std::sort(wireframe_ids_.begin(), wireframe_ids_.end());
55 ✔
1616
}
55 ✔
1617

1618
void RayTracePlot::set_pixels(pugi::xml_node node)
88 ✔
1619
{
1620
  vector<int> pxls = get_node_array<int>(node, "pixels");
88 ✔
1621
  if (pxls.size() != 2)
88 !
UNCOV
1622
    fatal_error(
×
UNCOV
1623
      fmt::format("<pixels> must be length 2 in projection plot {}", id()));
×
1624
  pixels()[0] = pxls[0];
88 ✔
1625
  pixels()[1] = pxls[1];
88 ✔
1626
}
88 ✔
1627

1628
void RayTracePlot::set_camera_position(pugi::xml_node node)
88 ✔
1629
{
1630
  vector<double> camera_pos = get_node_array<double>(node, "camera_position");
88 ✔
1631
  if (camera_pos.size() != 3) {
88 !
UNCOV
1632
    fatal_error(fmt::format(
×
1633
      "camera_position element must have three floating point values"));
1634
  }
1635
  camera_position_.x = camera_pos[0];
88 ✔
1636
  camera_position_.y = camera_pos[1];
88 ✔
1637
  camera_position_.z = camera_pos[2];
88 ✔
1638
}
88 ✔
1639

1640
void RayTracePlot::set_look_at(pugi::xml_node node)
88 ✔
1641
{
1642
  vector<double> look_at = get_node_array<double>(node, "look_at");
88 ✔
1643
  if (look_at.size() != 3) {
88 !
UNCOV
1644
    fatal_error("look_at element must have three floating point values");
×
1645
  }
1646
  look_at_.x = look_at[0];
88 ✔
1647
  look_at_.y = look_at[1];
88 ✔
1648
  look_at_.z = look_at[2];
88 ✔
1649
}
88 ✔
1650

1651
void RayTracePlot::set_field_of_view(pugi::xml_node node)
88 ✔
1652
{
1653
  // Defaults to 70 degree horizontal field of view (see .h file)
1654
  if (check_for_node(node, "horizontal_field_of_view")) {
88 !
UNCOV
1655
    double fov =
×
UNCOV
1656
      std::stod(get_node_value(node, "horizontal_field_of_view", true));
×
1657
    if (fov < 180.0 && fov > 0.0) {
×
1658
      horizontal_field_of_view_ = fov;
×
1659
    } else {
1660
      fatal_error(fmt::format("Horizontal field of view for plot {} "
×
1661
                              "out-of-range. Must be in (0, 180) degrees.",
1662
        id()));
×
1663
    }
1664
  }
1665
}
88 ✔
1666

1667
SolidRayTracePlot::SolidRayTracePlot(pugi::xml_node node) : RayTracePlot(node)
33 ✔
1668
{
1669
  set_opaque_ids(node);
33 ✔
1670
  set_diffuse_fraction(node);
33 ✔
1671
  set_light_position(node);
33 ✔
1672
  update_view();
33 ✔
1673
}
33 ✔
1674

1675
void SolidRayTracePlot::print_info() const
33 ✔
1676
{
1677
  fmt::print("Plot Type: Solid ray-traced\n");
33 ✔
1678
  RayTracePlot::print_info();
33 ✔
1679
}
33 ✔
1680

1681
ImageData SolidRayTracePlot::create_image() const
55 ✔
1682
{
1683
  size_t width = pixels()[0];
55 ✔
1684
  size_t height = pixels()[1];
55 ✔
1685
  ImageData data({width, height}, not_found_);
55 ✔
1686

1687
#pragma omp parallel for schedule(dynamic) collapse(2)
30 ✔
1688
  for (int horiz = 0; horiz < pixels()[0]; ++horiz) {
3,105 ✔
1689
    for (int vert = 0; vert < pixels()[1]; ++vert) {
603,560 ✔
1690
      // RayTracePlot implements camera ray generation
1691
      std::pair<Position, Direction> ru = get_pixel_ray(horiz, vert);
600,480 ✔
1692
      PhongRay ray(ru.first, ru.second, *this);
600,480 ✔
1693
      ray.trace();
600,480 ✔
1694
      data(horiz, vert) = ray.result_color();
600,480 ✔
1695
    }
600,480 ✔
1696
  }
1697

1698
  return data;
55 ✔
1699
}
1700

1701
void SolidRayTracePlot::create_output() const
33 ✔
1702
{
1703
  ImageData data = create_image();
33 ✔
1704
  write_image(data);
33 ✔
1705
}
33 ✔
1706

1707
void SolidRayTracePlot::set_opaque_ids(pugi::xml_node node)
33 ✔
1708
{
1709
  if (check_for_node(node, "opaque_ids")) {
33 !
1710
    auto opaque_ids_tmp = get_node_array<int>(node, "opaque_ids");
33 ✔
1711

1712
    // It is read in as actual ID values, but we have to convert to indices in
1713
    // mat/cell array
1714
    for (auto& x : opaque_ids_tmp)
99 ✔
1715
      x = color_by_ == PlotColorBy::mats ? model::material_map[x]
132 !
UNCOV
1716
                                         : model::cell_map[x];
×
1717

1718
    opaque_ids_.insert(opaque_ids_tmp.begin(), opaque_ids_tmp.end());
33 ✔
1719
  }
33 ✔
1720
}
33 ✔
1721

1722
void SolidRayTracePlot::set_light_position(pugi::xml_node node)
33 ✔
1723
{
1724
  if (check_for_node(node, "light_position")) {
33 ✔
1725
    auto light_pos_tmp = get_node_array<double>(node, "light_position");
11 ✔
1726

1727
    if (light_pos_tmp.size() != 3)
11 !
UNCOV
1728
      fatal_error("Light position must be given as 3D coordinates");
×
1729

1730
    light_location_.x = light_pos_tmp[0];
11 ✔
1731
    light_location_.y = light_pos_tmp[1];
11 ✔
1732
    light_location_.z = light_pos_tmp[2];
11 ✔
1733
  } else {
11 ✔
1734
    light_location_ = camera_position();
22 ✔
1735
  }
1736
}
33 ✔
1737

1738
void SolidRayTracePlot::set_diffuse_fraction(pugi::xml_node node)
33 ✔
1739
{
1740
  if (check_for_node(node, "diffuse_fraction")) {
33 ✔
1741
    diffuse_fraction_ = std::stod(get_node_value(node, "diffuse_fraction"));
11 ✔
1742
    if (diffuse_fraction_ < 0.0 || diffuse_fraction_ > 1.0) {
11 !
UNCOV
1743
      fatal_error("Must have 0 <= diffuse fraction <= 1");
×
1744
    }
1745
  }
1746
}
33 ✔
1747

1748
void ProjectionRay::on_intersection()
2,359,148 ✔
1749
{
1750
  // This records a tuple with the following info
1751
  //
1752
  // 1) ID (material or cell depending on color_by_)
1753
  // 2) Distance traveled by the ray through that ID
1754
  // 3) Index of the intersected surface (starting from 1)
1755

1756
  line_segments_.emplace_back(
2,359,148 ✔
1757
    plot_.color_by_ == PlottableInterface::PlotColorBy::mats
2,359,148 ✔
1758
      ? material()
545,919 ✔
1759
      : lowest_coord().cell(),
1,813,229 ✔
1760
    traversal_distance_, boundary().surface_index());
2,359,148 ✔
1761
}
2,359,148 ✔
1762

1763
void PhongRay::on_intersection()
905,971 ✔
1764
{
1765
  // Check if we hit an opaque material or cell
1766
  int hit_id = plot_.color_by_ == PlottableInterface::PlotColorBy::mats
905,971 ✔
1767
                 ? material()
905,971 !
UNCOV
1768
                 : lowest_coord().cell();
×
1769

1770
  // If we are reflected and have advanced beyond the camera,
1771
  // the ray is done. This is checked here because we should
1772
  // kill the ray even if the material is not opaque.
1773
  if (reflected_ && (r() - plot_.camera_position()).dot(u()) >= 0.0) {
905,971 !
UNCOV
1774
    stop();
×
1775
    return;
164,340 ✔
1776
  }
1777

1778
  // Anything that's not opaque has zero impact on the plot.
1779
  if (plot_.opaque_ids_.find(hit_id) == plot_.opaque_ids_.end())
905,971 ✔
1780
    return;
1781

1782
  if (!reflected_) {
741,631 ✔
1783
    // reflect the particle and set the color to be colored by
1784
    // the normal or the diffuse lighting contribution
1785
    reflected_ = true;
706,068 ✔
1786
    result_color_ = plot_.colors_[hit_id];
706,068 ✔
1787
    // The ray has been advanced slightly past the boundary. Use an
1788
    // approximation to the actual hit point for stable normal/lighting.
1789
    Position r_hit = r() - TINY_BIT * u();
706,068 ✔
1790
    Direction to_light = plot_.light_location_ - r_hit;
706,068 ✔
1791
    to_light /= to_light.norm();
706,068 ✔
1792

1793
    // TODO
1794
    // Not sure what can cause a surface token to be invalid here, although it
1795
    // sometimes happens for a few pixels. It's very very rare, so proceed by
1796
    // coloring the pixel with the overlap color. It seems to happen only for a
1797
    // few pixels on the outer boundary of a hex lattice.
1798
    //
1799
    // We cannot detect it in the outer loop, and it only matters here, so
1800
    // that's why the error handling is a little different than for a lost
1801
    // ray.
1802
    if (surface() == 0) {
706,068 !
UNCOV
1803
      result_color_ = plot_.overlap_color_;
×
UNCOV
1804
      stop();
×
1805
      return;
×
1806
    }
1807

1808
    // Get surface pointer
1809
    const auto& surf = model::surfaces.at(surface_index());
706,068 ✔
1810

1811
    // The crossed surface may be on a higher coordinate level than the
1812
    // innermost local coordinates, so we check the surface's coordinate level
1813
    // to find the appropriate coordinate level to use for the normal
1814
    // calculation
1815
    int surf_level = boundary().coord_level() - 1;
706,068 ✔
1816
    // ensure surface level is within bounds of current coordinate stack
1817
    surf_level = std::clamp(surf_level, 0, n_coord() - 1);
706,068 ✔
1818

1819
    Position r_hit_level =
706,068 ✔
1820
      coord(surf_level).r() - TINY_BIT * coord(surf_level).u();
706,068 ✔
1821
    Direction normal = surf->normal(r_hit_level);
706,068 ✔
1822
    normal /= normal.norm();
706,068 ✔
1823

1824
    // Need to apply rotations to find the normal vector in
1825
    // the base level universe's coordinate system.
1826
    for (int lev = surf_level - 1; lev >= 0; --lev) {
706,068 !
UNCOV
1827
      if (coord(lev + 1).rotated()) {
×
UNCOV
1828
        const Cell& c {*model::cells[coord(lev).cell()]};
×
1829
        normal = normal.inverse_rotate(c.rotation_);
×
1830
      }
1831
    }
1832

1833
    // use the normal opposed to the ray direction
1834
    if (normal.dot(u()) > 0.0) {
706,068 ✔
1835
      normal *= -1.0;
63,789 ✔
1836
    }
1837

1838
    // Facing away from the light means no lighting
1839
    double dotprod = normal.dot(to_light);
706,068 ✔
1840
    dotprod = std::max(0.0, dotprod);
706,068 ✔
1841

1842
    double modulation =
706,068 ✔
1843
      plot_.diffuse_fraction_ + (1.0 - plot_.diffuse_fraction_) * dotprod;
706,068 ✔
1844
    result_color_ *= modulation;
706,068 ✔
1845

1846
    // Now point the particle to the camera. We now begin
1847
    // checking to see if it's occluded by another surface
1848
    u() = to_light;
706,068 ✔
1849

1850
    orig_hit_id_ = hit_id;
706,068 ✔
1851

1852
    // OpenMC native CSG and DAGMC surfaces have some slight differences
1853
    // in how they interpret particles that are sitting on a surface.
1854
    // I don't know exactly why, but this makes everything work beautifully.
1855
    if (surf->geom_type() == GeometryType::DAG) {
706,068 !
UNCOV
1856
      surface() = 0;
×
1857
    } else {
1858
      surface() = -surface(); // go to other side
706,068 ✔
1859
    }
1860

1861
    // Must fully restart coordinate search. Why? Not sure.
1862
    clear();
706,068 ✔
1863

1864
    // Note this could likely be faster if we cached the previous
1865
    // cell we were in before the reflection. This is the easiest
1866
    // way to fully initialize all the sub-universe coordinates and
1867
    // directions though.
1868
    bool found = exhaustive_find_cell(*this);
706,068 ✔
1869
    if (!found) {
706,068 !
UNCOV
1870
      fatal_error("Lost particle after reflection.");
×
1871
    }
1872

1873
    // Must recalculate distance to boundary due to the
1874
    // direction change
1875
    compute_distance();
706,068 ✔
1876

1877
  } else {
1878
    // If it's not facing the light, we color with the diffuse contribution, so
1879
    // next we check if we're going to occlude the last reflected surface. if
1880
    // so, color by the diffuse contribution instead
1881

1882
    if (orig_hit_id_ == -1)
35,563 !
UNCOV
1883
      fatal_error("somehow a ray got reflected but not original ID set?");
×
1884

1885
    result_color_ = plot_.colors_[orig_hit_id_];
35,563 ✔
1886
    result_color_ *= plot_.diffuse_fraction_;
35,563 ✔
1887
    stop();
741,631 ✔
1888
  }
1889
}
1890

UNCOV
1891
extern "C" int openmc_id_map(const void* plot, int32_t* data_out)
×
1892
{
1893
  static bool warned {false};
×
UNCOV
1894
  if (!warned) {
×
1895
    warning("openmc_id_map is deprecated and will be removed in a future "
×
1896
            "release. Use openmc_slice_data.");
1897
    warned = true;
×
1898
  }
1899

UNCOV
1900
  auto plt = reinterpret_cast<const SlicePlotBase*>(plot);
×
UNCOV
1901
  if (!plt) {
×
1902
    set_errmsg("Invalid slice pointer passed to openmc_id_map");
×
1903
    return OPENMC_E_INVALID_ARGUMENT;
×
1904
  }
1905

UNCOV
1906
  if (plt->show_overlaps_ && model::overlap_check_count.size() == 0) {
×
UNCOV
1907
    model::overlap_check_count.resize(model::cells.size());
×
1908
  }
1909

UNCOV
1910
  auto ids = plt->get_map<IdData>();
×
1911

1912
  // write id data to array
UNCOV
1913
  std::copy(ids.data_.begin(), ids.data_.end(), data_out);
×
1914

1915
  return 0;
×
UNCOV
1916
}
×
1917

1918
extern "C" int openmc_property_map(const void* plot, double* data_out)
×
1919
{
1920
  static bool warned {false};
×
UNCOV
1921
  if (!warned) {
×
1922
    warning("openmc_property_map is deprecated and will be removed in a future "
×
1923
            "release. Use openmc_slice_data.");
1924
    warned = true;
×
1925
  }
1926

UNCOV
1927
  auto plt = reinterpret_cast<const SlicePlotBase*>(plot);
×
UNCOV
1928
  if (!plt) {
×
1929
    set_errmsg("Invalid slice pointer passed to openmc_property_map");
×
1930
    return OPENMC_E_INVALID_ARGUMENT;
×
1931
  }
1932

UNCOV
1933
  if (plt->show_overlaps_ && model::overlap_check_count.size() == 0) {
×
UNCOV
1934
    model::overlap_check_count.resize(model::cells.size());
×
1935
  }
1936

UNCOV
1937
  auto props = plt->get_map<PropertyData>();
×
1938

1939
  // write id data to array
UNCOV
1940
  std::copy(props.data_.begin(), props.data_.end(), data_out);
×
1941

1942
  return 0;
×
UNCOV
1943
}
×
1944

1945
extern "C" int openmc_slice_data(const double origin[3], const double u_span[3],
938 ✔
1946
  const double v_span[3], const size_t pixels[2], bool color_overlaps,
1947
  int level, int32_t filter_index, int32_t* geom_data, double* property_data)
1948
{
1949
  // Validate span vectors
1950
  Direction u_span_pos {u_span[0], u_span[1], u_span[2]};
938 ✔
1951
  Direction v_span_pos {v_span[0], v_span[1], v_span[2]};
938 ✔
1952
  double u_norm = u_span_pos.norm();
938 ✔
1953
  double v_norm = v_span_pos.norm();
938 ✔
1954
  if (u_norm == 0.0 || v_norm == 0.0) {
938 !
UNCOV
1955
    set_errmsg("Slice span vectors must be non-zero.");
×
UNCOV
1956
    return OPENMC_E_INVALID_ARGUMENT;
×
1957
  }
1958

1959
  constexpr double ORTHO_REL_TOL = 1e-10;
938 ✔
1960
  double dot = u_span_pos.dot(v_span_pos);
938 !
1961
  if (std::abs(dot) > ORTHO_REL_TOL * u_norm * v_norm) {
938 !
UNCOV
1962
    set_errmsg("Slice span vectors must be orthogonal.");
×
UNCOV
1963
    return OPENMC_E_INVALID_ARGUMENT;
×
1964
  }
1965

1966
  // Validate filter index if provided
1967
  if (filter_index >= 0) {
938 ✔
1968
    if (int err = verify_filter(filter_index))
22 !
1969
      return err;
1970
  }
1971

1972
  // Initialize overlap check vector if needed
1973
  if (color_overlaps && model::overlap_check_count.size() == 0) {
938 ✔
1974
    model::overlap_check_count.resize(model::cells.size());
132 ✔
1975
  }
1976

1977
  try {
938 ✔
1978
    // Create a temporary SlicePlotBase object to reuse get_map logic
1979
    SlicePlotBase plot_params;
938 ✔
1980
    plot_params.origin_ = Position {origin[0], origin[1], origin[2]};
938 ✔
1981
    plot_params.u_span_ = u_span_pos;
938 ✔
1982
    plot_params.v_span_ = v_span_pos;
938 ✔
1983
    plot_params.pixels_[0] = pixels[0];
938 ✔
1984
    plot_params.pixels_[1] = pixels[1];
938 ✔
1985
    plot_params.show_overlaps_ = color_overlaps;
938 ✔
1986
    plot_params.slice_level_ = level;
938 ✔
1987

1988
    // Clear overlap data structures on new slice call
1989
    model::overlap_keys.clear();
938 ✔
1990
    model::overlap_key_index.clear();
938 ✔
1991

1992
    // Use get_map<RasterData> to generate data
1993
    auto data = plot_params.get_map<RasterData>(filter_index);
938 ✔
1994
    std::copy(data.id_data_.begin(), data.id_data_.end(), geom_data);
938 ✔
1995

1996
    // Copy property data if requested
1997
    if (property_data != nullptr) {
938 ✔
1998
      std::copy(
77 ✔
1999
        data.property_data_.begin(), data.property_data_.end(), property_data);
2000
    }
2001
  } catch (const std::exception& e) {
938 !
UNCOV
2002
    set_errmsg(e.what());
×
UNCOV
2003
    return OPENMC_E_UNASSIGNED;
×
2004
  }
×
2005

2006
  return 0;
938 ✔
2007
}
2008

2009
// Gets the number of overlaps that we need data for
2010
extern "C" int openmc_slice_data_overlap_count(size_t* count)
528 ✔
2011
{
2012
  if (!count) {
528 !
UNCOV
2013
    set_errmsg("Null pointer passed for overlap count.");
×
UNCOV
2014
    return OPENMC_E_INVALID_ARGUMENT;
×
2015
  }
2016
  *count = model::overlap_keys.size();
528 ✔
2017

2018
  return 0;
528 ✔
2019
}
2020

2021
// Plotter pre-allocates array size based on what is returned with
2022
// overlap_count; populates an array of size 3*count
2023
extern "C" int openmc_slice_data_overlap_info(
33 ✔
2024
  size_t count, int32_t* overlap_info)
2025
{
2026
  for (size_t i = 0; i < count; ++i) {
77 ✔
2027
    overlap_info[i * 3] = model::overlap_keys[i].universe_id;
44 ✔
2028
    overlap_info[i * 3 + 1] = model::overlap_keys[i].cell1_id;
44 ✔
2029
    overlap_info[i * 3 + 2] = model::overlap_keys[i].cell2_id;
44 ✔
2030
  }
2031

2032
  return 0;
33 ✔
2033
}
2034

2035
extern "C" int openmc_get_plot_index(int32_t id, int32_t* index)
22 ✔
2036
{
2037
  auto it = model::plot_map.find(id);
22 !
2038
  if (it == model::plot_map.end()) {
22 !
UNCOV
2039
    set_errmsg("No plot exists with ID=" + std::to_string(id) + ".");
×
UNCOV
2040
    return OPENMC_E_INVALID_ID;
×
2041
  }
2042

2043
  *index = it->second;
22 ✔
2044
  return 0;
22 ✔
2045
}
2046

2047
extern "C" int openmc_plot_get_id(int32_t index, int32_t* id)
55 ✔
2048
{
2049
  if (index < 0 || index >= model::plots.size()) {
55 !
UNCOV
2050
    set_errmsg("Index in plots array is out of bounds.");
×
UNCOV
2051
    return OPENMC_E_OUT_OF_BOUNDS;
×
2052
  }
2053

2054
  *id = model::plots[index]->id();
55 ✔
2055
  return 0;
55 ✔
2056
}
2057

UNCOV
2058
extern "C" int openmc_plot_set_id(int32_t index, int32_t id)
×
2059
{
2060
  if (index < 0 || index >= model::plots.size()) {
×
UNCOV
2061
    set_errmsg("Index in plots array is out of bounds.");
×
2062
    return OPENMC_E_OUT_OF_BOUNDS;
×
2063
  }
2064

UNCOV
2065
  if (id < 0 && id != C_NONE) {
×
UNCOV
2066
    set_errmsg("Invalid plot ID.");
×
2067
    return OPENMC_E_INVALID_ARGUMENT;
×
2068
  }
2069

UNCOV
2070
  auto* plot = model::plots[index].get();
×
UNCOV
2071
  int32_t old_id = plot->id();
×
2072
  if (id == old_id)
×
2073
    return 0;
2074

UNCOV
2075
  model::plot_map.erase(old_id);
×
UNCOV
2076
  try {
×
2077
    plot->set_id(id);
×
2078
  } catch (const std::runtime_error& e) {
×
2079
    model::plot_map[old_id] = index;
×
2080
    set_errmsg(e.what());
×
2081
    return OPENMC_E_INVALID_ID;
×
2082
  }
×
2083
  model::plot_map[plot->id()] = index;
×
2084
  return 0;
×
2085
}
2086

2087
extern "C" size_t openmc_plots_size()
22 ✔
2088
{
2089
  return model::plots.size();
22 ✔
2090
}
2091

2092
int map_phong_domain_id(
55 ✔
2093
  const SolidRayTracePlot* plot, int32_t id, int32_t* index_out)
2094
{
2095
  if (!plot || !index_out) {
55 !
UNCOV
2096
    set_errmsg("Invalid plot pointer passed to map_phong_domain_id");
×
UNCOV
2097
    return OPENMC_E_INVALID_ARGUMENT;
×
2098
  }
2099

2100
  if (plot->color_by_ == PlottableInterface::PlotColorBy::mats) {
55 !
2101
    auto it = model::material_map.find(id);
55 !
2102
    if (it == model::material_map.end()) {
55 !
UNCOV
2103
      set_errmsg("Invalid material ID for SolidRayTracePlot");
×
UNCOV
2104
      return OPENMC_E_INVALID_ID;
×
2105
    }
2106
    *index_out = it->second;
55 ✔
2107
    return 0;
55 ✔
2108
  }
2109

UNCOV
2110
  if (plot->color_by_ == PlottableInterface::PlotColorBy::cells) {
×
UNCOV
2111
    auto it = model::cell_map.find(id);
×
2112
    if (it == model::cell_map.end()) {
×
2113
      set_errmsg("Invalid cell ID for SolidRayTracePlot");
×
2114
      return OPENMC_E_INVALID_ID;
×
2115
    }
2116
    *index_out = it->second;
×
UNCOV
2117
    return 0;
×
2118
  }
2119

UNCOV
2120
  set_errmsg("Unsupported color_by for SolidRayTracePlot");
×
UNCOV
2121
  return OPENMC_E_INVALID_TYPE;
×
2122
}
2123

2124
int get_solidraytrace_plot_by_index(int32_t index, SolidRayTracePlot** plot)
308 ✔
2125
{
2126
  if (!plot) {
308 !
UNCOV
2127
    set_errmsg("Null output pointer passed to get_solidraytrace_plot_by_index");
×
UNCOV
2128
    return OPENMC_E_INVALID_ARGUMENT;
×
2129
  }
2130

2131
  if (index < 0 || index >= model::plots.size()) {
308 !
UNCOV
2132
    set_errmsg("Index in plots array is out of bounds.");
×
UNCOV
2133
    return OPENMC_E_OUT_OF_BOUNDS;
×
2134
  }
2135

2136
  auto* plottable = model::plots[index].get();
308 !
2137
  auto* solid_plot = dynamic_cast<SolidRayTracePlot*>(plottable);
308 !
2138
  if (!solid_plot) {
308 !
UNCOV
2139
    set_errmsg("Plot at index=" + std::to_string(index) +
×
2140
               " is not a solid raytrace plot.");
2141
    return OPENMC_E_INVALID_TYPE;
×
2142
  }
2143

2144
  *plot = solid_plot;
308 ✔
2145
  return 0;
308 ✔
2146
}
2147

2148
extern "C" int openmc_solidraytrace_plot_create(int32_t* index)
11 ✔
2149
{
2150
  if (!index) {
11 !
UNCOV
2151
    set_errmsg(
×
2152
      "Null output pointer passed to openmc_solidraytrace_plot_create");
2153
    return OPENMC_E_INVALID_ARGUMENT;
×
2154
  }
2155

2156
  try {
11 ✔
2157
    auto new_plot = std::make_unique<SolidRayTracePlot>();
11 ✔
2158
    new_plot->set_id();
11 ✔
2159
    int32_t new_plot_id = new_plot->id();
11 ✔
2160
#ifdef USE_LIBPNG
2161
    new_plot->path_plot() = fmt::format("plot_{}.png", new_plot_id);
11 ✔
2162
#else
2163
    new_plot->path_plot() = fmt::format("plot_{}.ppm", new_plot_id);
2164
#endif
2165
    int32_t new_plot_index = model::plots.size();
11 ✔
2166
    model::plots.emplace_back(std::move(new_plot));
11 ✔
2167
    model::plot_map[new_plot_id] = new_plot_index;
11 ✔
2168
    *index = new_plot_index;
11 ✔
2169
  } catch (const std::exception& e) {
11 !
UNCOV
2170
    set_errmsg(e.what());
×
UNCOV
2171
    return OPENMC_E_ALLOCATE;
×
2172
  }
×
2173

2174
  return 0;
11 ✔
2175
}
2176

2177
extern "C" int openmc_solidraytrace_plot_get_pixels(
33 ✔
2178
  int32_t index, int32_t* width, int32_t* height)
2179
{
2180
  if (!width || !height) {
33 !
UNCOV
2181
    set_errmsg(
×
2182
      "Invalid arguments passed to openmc_solidraytrace_plot_get_pixels");
2183
    return OPENMC_E_INVALID_ARGUMENT;
×
2184
  }
2185

2186
  SolidRayTracePlot* plt = nullptr;
33 ✔
2187
  int err = get_solidraytrace_plot_by_index(index, &plt);
33 ✔
2188
  if (err)
33 !
2189
    return err;
2190

2191
  *width = plt->pixels()[0];
33 ✔
2192
  *height = plt->pixels()[1];
33 ✔
2193
  return 0;
33 ✔
2194
}
2195

2196
extern "C" int openmc_solidraytrace_plot_set_pixels(
11 ✔
2197
  int32_t index, int32_t width, int32_t height)
2198
{
2199
  if (width <= 0 || height <= 0) {
11 !
UNCOV
2200
    set_errmsg(
×
2201
      "Invalid arguments passed to openmc_solidraytrace_plot_set_pixels");
2202
    return OPENMC_E_INVALID_ARGUMENT;
×
2203
  }
2204

2205
  SolidRayTracePlot* plt = nullptr;
11 ✔
2206
  int err = get_solidraytrace_plot_by_index(index, &plt);
11 ✔
2207
  if (err)
11 !
2208
    return err;
2209

2210
  plt->pixels()[0] = width;
11 ✔
2211
  plt->pixels()[1] = height;
11 ✔
2212
  return 0;
11 ✔
2213
}
2214

2215
extern "C" int openmc_solidraytrace_plot_get_color_by(
11 ✔
2216
  int32_t index, int32_t* color_by)
2217
{
2218
  if (!color_by) {
11 !
UNCOV
2219
    set_errmsg(
×
2220
      "Invalid arguments passed to openmc_solidraytrace_plot_get_color_by");
2221
    return OPENMC_E_INVALID_ARGUMENT;
×
2222
  }
2223

2224
  SolidRayTracePlot* plt = nullptr;
11 ✔
2225
  int err = get_solidraytrace_plot_by_index(index, &plt);
11 ✔
2226
  if (err)
11 !
2227
    return err;
2228

2229
  if (plt->color_by_ == PlottableInterface::PlotColorBy::mats) {
11 !
2230
    *color_by = 0;
11 ✔
UNCOV
2231
  } else if (plt->color_by_ == PlottableInterface::PlotColorBy::cells) {
×
UNCOV
2232
    *color_by = 1;
×
2233
  } else {
2234
    set_errmsg("Unsupported color_by for SolidRayTracePlot");
×
UNCOV
2235
    return OPENMC_E_INVALID_TYPE;
×
2236
  }
2237

2238
  return 0;
2239
}
2240

2241
extern "C" int openmc_solidraytrace_plot_set_color_by(
11 ✔
2242
  int32_t index, int32_t color_by)
2243
{
2244
  SolidRayTracePlot* plt = nullptr;
11 ✔
2245
  int err = get_solidraytrace_plot_by_index(index, &plt);
11 ✔
2246
  if (err)
11 !
2247
    return err;
2248

2249
  if (color_by == 0) {
11 !
2250
    plt->color_by_ = PlottableInterface::PlotColorBy::mats;
11 ✔
UNCOV
2251
  } else if (color_by == 1) {
×
UNCOV
2252
    plt->color_by_ = PlottableInterface::PlotColorBy::cells;
×
2253
  } else {
2254
    set_errmsg("Invalid color_by value for SolidRayTracePlot");
×
UNCOV
2255
    return OPENMC_E_INVALID_ARGUMENT;
×
2256
  }
2257

2258
  return 0;
2259
}
2260

2261
extern "C" int openmc_solidraytrace_plot_set_default_colors(int32_t index)
11 ✔
2262
{
2263
  SolidRayTracePlot* plt = nullptr;
11 ✔
2264
  int err = get_solidraytrace_plot_by_index(index, &plt);
11 ✔
2265
  if (err)
11 !
2266
    return err;
2267

2268
  plt->set_default_colors();
11 ✔
2269
  return 0;
2270
}
2271

UNCOV
2272
extern "C" int openmc_solidraytrace_plot_set_all_opaque(int32_t index)
×
2273
{
2274
  SolidRayTracePlot* plt = nullptr;
×
UNCOV
2275
  int err = get_solidraytrace_plot_by_index(index, &plt);
×
2276
  if (err)
×
2277
    return err;
2278

UNCOV
2279
  plt->opaque_ids().clear();
×
UNCOV
2280
  if (plt->color_by_ == PlottableInterface::PlotColorBy::mats) {
×
2281
    for (int32_t i = 0; i < model::materials.size(); ++i) {
×
2282
      plt->opaque_ids().insert(i);
×
2283
    }
2284
    return 0;
×
2285
  }
2286

UNCOV
2287
  if (plt->color_by_ == PlottableInterface::PlotColorBy::cells) {
×
UNCOV
2288
    for (int32_t i = 0; i < model::cells.size(); ++i) {
×
2289
      plt->opaque_ids().insert(i);
×
2290
    }
2291
    return 0;
×
2292
  }
2293

UNCOV
2294
  set_errmsg("Unsupported color_by for SolidRayTracePlot");
×
2295
  return OPENMC_E_INVALID_TYPE;
2296
}
2297

2298
extern "C" int openmc_solidraytrace_plot_set_opaque(
22 ✔
2299
  int32_t index, int32_t id, bool visible)
2300
{
2301
  SolidRayTracePlot* plt = nullptr;
22 ✔
2302
  int err = get_solidraytrace_plot_by_index(index, &plt);
22 ✔
2303
  if (err)
22 !
2304
    return err;
2305

2306
  int32_t domain_index = -1;
22 ✔
2307
  err = map_phong_domain_id(plt, id, &domain_index);
22 ✔
2308
  if (err)
22 !
2309
    return err;
2310

2311
  if (visible) {
22 ✔
2312
    plt->opaque_ids().insert(domain_index);
11 ✔
2313
  } else {
2314
    plt->opaque_ids().erase(domain_index);
11 ✔
2315
  }
2316

2317
  return 0;
2318
}
2319

2320
extern "C" int openmc_solidraytrace_plot_set_color(
22 ✔
2321
  int32_t index, int32_t id, uint8_t r, uint8_t g, uint8_t b)
2322
{
2323
  SolidRayTracePlot* plt = nullptr;
22 ✔
2324
  int err = get_solidraytrace_plot_by_index(index, &plt);
22 ✔
2325
  if (err)
22 !
2326
    return err;
2327

2328
  int32_t domain_index = -1;
22 ✔
2329
  err = map_phong_domain_id(plt, id, &domain_index);
22 ✔
2330
  if (err)
22 !
2331
    return err;
2332

2333
  if (domain_index < 0 ||
22 !
2334
      static_cast<size_t>(domain_index) >= plt->colors_.size()) {
22 !
UNCOV
2335
    set_errmsg("Color index out of range for SolidRayTracePlot");
×
UNCOV
2336
    return OPENMC_E_OUT_OF_BOUNDS;
×
2337
  }
2338

2339
  plt->colors_[domain_index] = RGBColor(r, g, b);
22 ✔
2340
  return 0;
22 ✔
2341
}
2342

2343
extern "C" int openmc_solidraytrace_plot_get_camera_position(
11 ✔
2344
  int32_t index, double* x, double* y, double* z)
2345
{
2346
  if (!x || !y || !z) {
11 !
UNCOV
2347
    set_errmsg("Invalid arguments passed to "
×
2348
               "openmc_solidraytrace_plot_get_camera_position");
2349
    return OPENMC_E_INVALID_ARGUMENT;
×
2350
  }
2351

2352
  SolidRayTracePlot* plt = nullptr;
11 ✔
2353
  int err = get_solidraytrace_plot_by_index(index, &plt);
11 ✔
2354
  if (err)
11 !
2355
    return err;
2356

2357
  const auto& camera_position = plt->camera_position();
11 ✔
2358
  *x = camera_position.x;
11 ✔
2359
  *y = camera_position.y;
11 ✔
2360
  *z = camera_position.z;
11 ✔
2361
  return 0;
11 ✔
2362
}
2363

2364
extern "C" int openmc_solidraytrace_plot_set_camera_position(
11 ✔
2365
  int32_t index, double x, double y, double z)
2366
{
2367
  SolidRayTracePlot* plt = nullptr;
11 ✔
2368
  int err = get_solidraytrace_plot_by_index(index, &plt);
11 ✔
2369
  if (err)
11 !
2370
    return err;
2371

2372
  plt->camera_position() = {x, y, z};
11 ✔
2373
  return 0;
11 ✔
2374
}
2375

2376
extern "C" int openmc_solidraytrace_plot_get_look_at(
11 ✔
2377
  int32_t index, double* x, double* y, double* z)
2378
{
2379
  if (!x || !y || !z) {
11 !
UNCOV
2380
    set_errmsg(
×
2381
      "Invalid arguments passed to openmc_solidraytrace_plot_get_look_at");
2382
    return OPENMC_E_INVALID_ARGUMENT;
×
2383
  }
2384

2385
  SolidRayTracePlot* plt = nullptr;
11 ✔
2386
  int err = get_solidraytrace_plot_by_index(index, &plt);
11 ✔
2387
  if (err)
11 !
2388
    return err;
2389

2390
  const auto& look_at = plt->look_at();
11 ✔
2391
  *x = look_at.x;
11 ✔
2392
  *y = look_at.y;
11 ✔
2393
  *z = look_at.z;
11 ✔
2394
  return 0;
11 ✔
2395
}
2396

2397
extern "C" int openmc_solidraytrace_plot_set_look_at(
11 ✔
2398
  int32_t index, double x, double y, double z)
2399
{
2400
  SolidRayTracePlot* plt = nullptr;
11 ✔
2401
  int err = get_solidraytrace_plot_by_index(index, &plt);
11 ✔
2402
  if (err)
11 !
2403
    return err;
2404

2405
  plt->look_at() = {x, y, z};
11 ✔
2406
  return 0;
11 ✔
2407
}
2408

2409
extern "C" int openmc_solidraytrace_plot_get_up(
11 ✔
2410
  int32_t index, double* x, double* y, double* z)
2411
{
2412
  if (!x || !y || !z) {
11 !
UNCOV
2413
    set_errmsg("Invalid arguments passed to openmc_solidraytrace_plot_get_up");
×
UNCOV
2414
    return OPENMC_E_INVALID_ARGUMENT;
×
2415
  }
2416

2417
  SolidRayTracePlot* plt = nullptr;
11 ✔
2418
  int err = get_solidraytrace_plot_by_index(index, &plt);
11 ✔
2419
  if (err)
11 !
2420
    return err;
2421

2422
  const auto& up = plt->up();
11 ✔
2423
  *x = up.x;
11 ✔
2424
  *y = up.y;
11 ✔
2425
  *z = up.z;
11 ✔
2426
  return 0;
11 ✔
2427
}
2428

2429
extern "C" int openmc_solidraytrace_plot_set_up(
11 ✔
2430
  int32_t index, double x, double y, double z)
2431
{
2432
  SolidRayTracePlot* plt = nullptr;
11 ✔
2433
  int err = get_solidraytrace_plot_by_index(index, &plt);
11 ✔
2434
  if (err)
11 !
2435
    return err;
2436

2437
  plt->up() = {x, y, z};
11 ✔
2438
  return 0;
11 ✔
2439
}
2440

2441
extern "C" int openmc_solidraytrace_plot_get_light_position(
11 ✔
2442
  int32_t index, double* x, double* y, double* z)
2443
{
2444
  if (!x || !y || !z) {
11 !
UNCOV
2445
    set_errmsg("Invalid arguments passed to "
×
2446
               "openmc_solidraytrace_plot_get_light_position");
2447
    return OPENMC_E_INVALID_ARGUMENT;
×
2448
  }
2449

2450
  SolidRayTracePlot* plt = nullptr;
11 ✔
2451
  int err = get_solidraytrace_plot_by_index(index, &plt);
11 ✔
2452
  if (err)
11 !
2453
    return err;
2454

2455
  const auto& light_position = plt->light_location();
11 ✔
2456
  *x = light_position.x;
11 ✔
2457
  *y = light_position.y;
11 ✔
2458
  *z = light_position.z;
11 ✔
2459
  return 0;
11 ✔
2460
}
2461

2462
extern "C" int openmc_solidraytrace_plot_set_light_position(
11 ✔
2463
  int32_t index, double x, double y, double z)
2464
{
2465
  SolidRayTracePlot* plt = nullptr;
11 ✔
2466
  int err = get_solidraytrace_plot_by_index(index, &plt);
11 ✔
2467
  if (err)
11 !
2468
    return err;
2469

2470
  plt->light_location() = {x, y, z};
11 ✔
2471
  return 0;
11 ✔
2472
}
2473

2474
extern "C" int openmc_solidraytrace_plot_get_fov(int32_t index, double* fov)
11 ✔
2475
{
2476
  if (!fov) {
11 !
UNCOV
2477
    set_errmsg("Invalid arguments passed to openmc_solidraytrace_plot_get_fov");
×
UNCOV
2478
    return OPENMC_E_INVALID_ARGUMENT;
×
2479
  }
2480

2481
  SolidRayTracePlot* plt = nullptr;
11 ✔
2482
  int err = get_solidraytrace_plot_by_index(index, &plt);
11 ✔
2483
  if (err)
11 !
2484
    return err;
2485

2486
  *fov = plt->horizontal_field_of_view();
11 ✔
2487
  return 0;
11 ✔
2488
}
2489

2490
extern "C" int openmc_solidraytrace_plot_set_fov(int32_t index, double fov)
11 ✔
2491
{
2492
  SolidRayTracePlot* plt = nullptr;
11 ✔
2493
  int err = get_solidraytrace_plot_by_index(index, &plt);
11 ✔
2494
  if (err)
11 !
2495
    return err;
2496

2497
  plt->horizontal_field_of_view() = fov;
11 ✔
2498
  return 0;
11 ✔
2499
}
2500

2501
extern "C" int openmc_solidraytrace_plot_update_view(int32_t index)
22 ✔
2502
{
2503
  SolidRayTracePlot* plt = nullptr;
22 ✔
2504
  int err = get_solidraytrace_plot_by_index(index, &plt);
22 ✔
2505
  if (err)
22 !
2506
    return err;
2507

2508
  plt->update_view();
22 ✔
2509
  return 0;
2510
}
2511

2512
extern "C" int openmc_solidraytrace_plot_create_image(
22 ✔
2513
  int32_t index, uint8_t* data_out, int32_t width, int32_t height)
2514
{
2515
  if (!data_out || width <= 0 || height <= 0) {
22 !
UNCOV
2516
    set_errmsg(
×
2517
      "Invalid arguments passed to openmc_solidraytrace_plot_create_image");
2518
    return OPENMC_E_INVALID_ARGUMENT;
×
2519
  }
2520

2521
  SolidRayTracePlot* plt = nullptr;
22 ✔
2522
  int err = get_solidraytrace_plot_by_index(index, &plt);
22 ✔
2523
  if (err)
22 !
2524
    return err;
2525

2526
  if (plt->pixels()[0] != width || plt->pixels()[1] != height) {
22 !
UNCOV
2527
    set_errmsg(
×
2528
      "Requested image size does not match SolidRayTracePlot pixel settings");
2529
    return OPENMC_E_INVALID_SIZE;
×
2530
  }
2531

2532
  ImageData data = plt->create_image();
22 ✔
2533
  if (static_cast<int32_t>(data.shape()[0]) != width ||
22 !
2534
      static_cast<int32_t>(data.shape()[1]) != height) {
22 !
UNCOV
2535
    set_errmsg("Unexpected image size from SolidRayTracePlot create_image");
×
2536
    return OPENMC_E_INVALID_SIZE;
2537
  }
2538

2539
  for (int32_t y = 0; y < height; ++y) {
154 ✔
2540
    for (int32_t x = 0; x < width; ++x) {
1,188 ✔
2541
      const auto& color = data(x, y);
1,056 ✔
2542
      size_t idx = (static_cast<size_t>(y) * width + x) * 3;
1,056 ✔
2543
      data_out[idx + 0] = color.red;
1,056 ✔
2544
      data_out[idx + 1] = color.green;
1,056 ✔
2545
      data_out[idx + 2] = color.blue;
1,056 ✔
2546
    }
2547
  }
2548

2549
  return 0;
2550
}
22 ✔
2551

2552
extern "C" int openmc_solidraytrace_plot_get_color(
11 ✔
2553
  int32_t index, int32_t id, uint8_t* r, uint8_t* g, uint8_t* b)
2554
{
2555
  if (!r || !g || !b) {
11 !
UNCOV
2556
    set_errmsg(
×
2557
      "Invalid arguments passed to openmc_solidraytrace_plot_get_color");
2558
    return OPENMC_E_INVALID_ARGUMENT;
×
2559
  }
2560

2561
  SolidRayTracePlot* plt = nullptr;
11 ✔
2562
  int err = get_solidraytrace_plot_by_index(index, &plt);
11 ✔
2563
  if (err)
11 !
2564
    return err;
2565

2566
  int32_t domain_index = -1;
11 ✔
2567
  err = map_phong_domain_id(plt, id, &domain_index);
11 ✔
2568
  if (err)
11 !
2569
    return err;
2570

2571
  if (domain_index < 0 ||
11 !
2572
      static_cast<size_t>(domain_index) >= plt->colors_.size()) {
11 !
UNCOV
2573
    set_errmsg("Color index out of range for SolidRayTracePlot");
×
UNCOV
2574
    return OPENMC_E_OUT_OF_BOUNDS;
×
2575
  }
2576

2577
  const auto& color = plt->colors_[domain_index];
11 ✔
2578
  *r = color.red;
11 ✔
2579
  *g = color.green;
11 ✔
2580
  *b = color.blue;
11 ✔
2581
  return 0;
11 ✔
2582
}
2583

2584
extern "C" int openmc_solidraytrace_plot_get_diffuse_fraction(
11 ✔
2585
  int32_t index, double* diffuse_fraction)
2586
{
2587
  if (!diffuse_fraction) {
11 !
UNCOV
2588
    set_errmsg("Invalid arguments passed to "
×
2589
               "openmc_solidraytrace_plot_get_diffuse_fraction");
2590
    return OPENMC_E_INVALID_ARGUMENT;
×
2591
  }
2592

2593
  SolidRayTracePlot* plt = nullptr;
11 ✔
2594
  int err = get_solidraytrace_plot_by_index(index, &plt);
11 ✔
2595
  if (err)
11 !
2596
    return err;
2597

2598
  *diffuse_fraction = plt->diffuse_fraction();
11 ✔
2599
  return 0;
11 ✔
2600
}
2601

2602
extern "C" int openmc_solidraytrace_plot_set_diffuse_fraction(
11 ✔
2603
  int32_t index, double diffuse_fraction)
2604
{
2605
  SolidRayTracePlot* plt = nullptr;
11 ✔
2606
  int err = get_solidraytrace_plot_by_index(index, &plt);
11 ✔
2607
  if (err)
11 !
2608
    return err;
2609

2610
  if (diffuse_fraction < 0.0 || diffuse_fraction > 1.0) {
11 !
UNCOV
2611
    set_errmsg("Diffuse fraction must be between 0 and 1");
×
UNCOV
2612
    return OPENMC_E_INVALID_ARGUMENT;
×
2613
  }
2614

2615
  plt->diffuse_fraction() = diffuse_fraction;
11 ✔
2616
  return 0;
11 ✔
2617
}
2618

2619
} // namespace openmc
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc