EGSnrc C++ class library  Report PIRS-898 (2021)
Iwan Kawrakow, Ernesto Mainegra-Hing, Frederic Tessier, Reid Townson and Blake Walters
test_egs_triangle_mesh.cpp
1 /*
2 ###############################################################################
3 #
4 # EGSnrc egs_triangle_mesh test suite
5 # Copyright (C) 2022 Max Orok
6 #
7 # This file is part of EGSnrc.
8 #
9 # EGSnrc is free software: you can redistribute it and/or modify it under
10 # the terms of the GNU Affero General Public License as published by the
11 # Free Software Foundation, either version 3 of the License, or (at your
12 # option) any later version.
13 #
14 # EGSnrc is distributed in the hope that it will be useful, but WITHOUT ANY
15 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16 # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
17 # more details.
18 #
19 # You should have received a copy of the GNU Affero General Public License
20 # along with EGSnrc. If not, see <http://www.gnu.org/licenses/>.
21 #
22 ###############################################################################
23 #
24 # Author: Max Orok, 2022
25 #
26 # Contributors:
27 #
28 ###############################################################################
29 */
30 
31 #include "egs_input.h"
32 #include "egs_triangle_mesh.h"
33 #include "stl_parser.h"
34 
35 #include <cstdarg>
36 #include <cstdio>
37 #include <fstream>
38 #include <sstream>
39 
40 // Test runner globals
41 int num_total = 0;
42 int num_failed = 0;
43 
44 #define RUN_TEST(test_fn) \
45  std::cerr << "test " << #test_fn << "... "; \
46  num_total++; \
47  try { \
48  test_fn; \
49  std::cerr << "ok\n"; \
50  } catch (const std::runtime_error& err) { \
51  num_failed++; \
52  std::cerr << "FAILED: " << err.what() << "\n"; \
53  }
54 
55 #define EXPECT_ERROR(stmt, err_msg) \
56  try { \
57  stmt; \
58  std::ostringstream oss; \
59  oss << "expected exception with message: \"" << err_msg << "\""; \
60  throw std::runtime_error(oss.str()); \
61  } catch (const std::exception& err) { \
62  if (err.what() != std::string(err_msg)) { \
63  std::ostringstream oss; \
64  oss << "got error message: \"" \
65  << err.what() << "\" but expected: \"" << err_msg << "\""; \
66  throw std::runtime_error(oss.str()); \
67  } \
68  }
69 
70 
71 // RAII class for a temporary file
72 class TempFile {
73 public:
74  TempFile(const std::string &filename, const std::string &contents)
75  : filename_(filename) {
76  {
77  std::ofstream out(filename_);
78  out << contents;
79  }
80  }
81 
82  ~TempFile() {
83  std::remove(this->filename_.c_str());
84  }
85 
86  std::string filename() const {
87  return filename_;
88  }
89 private:
90  std::string filename_;
91 };
92 
93 static bool egsvec_eq(EGS_Vector a, EGS_Vector b) {
94  return a.x == b.x && a.y == b.y && a.z == b.z;
95 }
96 
97 static bool approx_eq(double a, double b, double e = 1e-6) {
98  return (std::abs(a - b) <= e * (std::abs(a) + std::abs(b) + 1.0));
99 }
100 
101 static bool egsvec_approx_eq(EGS_Vector a, EGS_Vector b) {
102  return approx_eq(a.x, b.x) && approx_eq(a.y, b.y) && approx_eq(a.z, b.z);
103 }
104 
105 // Custom egsInfoFunction that throws error messages as exceptions for testing
106 void egsInfoThrowing(const char *msg, ...) {
107  char buf[8192];
108  va_list ap;
109  va_start(ap, msg);
110  vsprintf(buf, msg, ap);
111  va_end(ap);
112  throw std::runtime_error(buf); // buf copied by constructor
113 }
114 
115 namespace stl_parser {
116 
117 static void missing_file() {
118  EXPECT_ERROR(stl_parser::parse_stl_file("temp_missing.stl"), "STL file "
119  "`temp_missing.stl` does not exist or is not readable");
120 }
121 
122 static void empty_file() {
123  TempFile empty("temp_empty.stl", "");
124  EXPECT_ERROR(stl_parser::parse_stl_file(empty.filename()),
125  "failed to parse STL file `temp_empty.stl`");
126 }
127 
128 static void parse_ascii_file() {
129  TempFile ascii("temp_ascii.stl", R"(solid
130 facet normal 0.6229 0.35962 0.694744
131  outer loop
132  vertex 43.062 20.491 -149.441
133  vertex 41.768 19.197 -147.611
134  vertex 43.946 19.607 -149.776
135  endloop
136 endfacet
137 facet normal 0.730297 0.632387 0.258365
138  outer loop
139  vertex 43.536 20.965 -151.941
140  vertex 43.062 20.491 -149.441
141  vertex 43.946 19.607 -149.776
142  endloop
143 endfacet
144 facet normal 0.81508 0.547608 0.189131
145  outer loop
146  vertex 43.536 20.965 -151.941
147  vertex 43.946 19.607 -149.776
148  vertex 44.593 18.96 -150.691
149  endloop
150 endfacet
151 endsolid)");
152 
153  auto mesh = stl_parser::parse_stl_file(ascii.filename());
154  if (mesh.elements.size() != 3) {
155  throw std::runtime_error("expected 3 triangles, got "
156  + std::to_string(mesh.elements.size()));
157  }
158  if (!egsvec_approx_eq(mesh.elements[0].n, EGS_Vector(0.6229f, 0.35962f, 0.694744f)) ||
159  !egsvec_approx_eq(mesh.elements[0].a, EGS_Vector(43.062f, 20.491f, -149.441f)) ||
160  !egsvec_approx_eq(mesh.elements[0].b, EGS_Vector(41.768f, 19.197f, -147.611f)) ||
161  !egsvec_approx_eq(mesh.elements[0].c, EGS_Vector(43.946f, 19.607f, -149.776f))) {
162  throw std::runtime_error("element 0 parsing failed");
163  }
164  if (!egsvec_approx_eq(mesh.elements[1].n, EGS_Vector(0.730297f, 0.632387f, 0.258365f)) ||
165  !egsvec_approx_eq(mesh.elements[1].a, EGS_Vector(43.536f, 20.965f, -151.941f)) ||
166  !egsvec_approx_eq(mesh.elements[1].b, EGS_Vector(43.062f, 20.491f, -149.441f)) ||
167  !egsvec_approx_eq(mesh.elements[1].c, EGS_Vector(43.946f, 19.607f, -149.776f))) {
168  throw std::runtime_error("element 1 parsing failed");
169  }
170  if (!egsvec_approx_eq(mesh.elements[2].n, EGS_Vector(0.81508f, 0.547608f, 0.189131f)) ||
171  !egsvec_approx_eq(mesh.elements[2].a, EGS_Vector(43.536f, 20.965f, -151.941f)) ||
172  !egsvec_approx_eq(mesh.elements[2].b, EGS_Vector(43.946f, 19.607f, -149.776f)) ||
173  !egsvec_approx_eq(mesh.elements[2].c, EGS_Vector(44.593f, 18.96f, -150.691f))) {
174  throw std::runtime_error("element 2 parsing failed");
175  }
176 }
177 
178 static void catch_zero_triangles() {
179  // write 84 zero bytes (80 byte comment + 4 n_tri)
180  std::string header(84, 0);
181  TempFile binfile("temp_zero_tris.stl", header);
182  EXPECT_ERROR(stl_parser::parse_stl_file(binfile.filename()),
183  "STL file `temp_zero_tris.stl` has 0 triangles");
184 }
185 
186 static void truncated_file() {
187  // write binary file header
188  // 1. 80 byte comment
189  std::string header(80, 'A');
190  // 2. Number of triangles (LE bytes for 100,000)
191  for (auto b : {
192  0xA0, 0x86, 0x1, 0
193  }) {
194  header.push_back(b);
195  }
196  TempFile binfile("temp_truncated_file.stl", header);
197  std::ifstream input(binfile.filename(), std::ios::binary);
198  // Missing triangle data
199  EXPECT_ERROR(stl_parser::parse_stl_file(binfile.filename()),
200  "failed to parse STL file `temp_truncated_file.stl`");
201 }
202 
203 static void parse_binary_file() {
204  // Test parser with known binary STL file
205  EGS_TriangleMeshSpec mesh = stl_parser::parse_stl_file("sample_bin.stl");
206 
207  if (mesh.elements.size() != 3) {
208  throw std::runtime_error("expected 3 triangles, got "
209  + std::to_string(mesh.elements.size()));
210  }
211 
212  if (!egsvec_approx_eq(mesh.elements[0].n, EGS_Vector(0.6229f, 0.35962f, 0.694744f)) ||
213  !egsvec_approx_eq(mesh.elements[0].a, EGS_Vector(43.062f, 20.491f, -149.441f)) ||
214  !egsvec_approx_eq(mesh.elements[0].b, EGS_Vector(41.768f, 19.197f, -147.611f)) ||
215  !egsvec_approx_eq(mesh.elements[0].c, EGS_Vector(43.946f, 19.607f, -149.776f))) {
216  throw std::runtime_error("element 0 parsing failed");
217  }
218  if (!egsvec_approx_eq(mesh.elements[1].n, EGS_Vector(0.730297f, 0.632387f, 0.258365f)) ||
219  !egsvec_approx_eq(mesh.elements[1].a, EGS_Vector(43.536f, 20.965f, -151.941f)) ||
220  !egsvec_approx_eq(mesh.elements[1].b, EGS_Vector(43.062f, 20.491f, -149.441f)) ||
221  !egsvec_approx_eq(mesh.elements[1].c, EGS_Vector(43.946f, 19.607f, -149.776f))) {
222  throw std::runtime_error("element 1 parsing failed");
223  }
224  if (!egsvec_approx_eq(mesh.elements[2].n, EGS_Vector(0.81508f, 0.547608f, 0.189131f)) ||
225  !egsvec_approx_eq(mesh.elements[2].a, EGS_Vector(43.536f, 20.965f, -151.941f)) ||
226  !egsvec_approx_eq(mesh.elements[2].b, EGS_Vector(43.946f, 19.607f, -149.776f)) ||
227  !egsvec_approx_eq(mesh.elements[2].c, EGS_Vector(44.593f, 18.96f, -150.691f))) {
228  throw std::runtime_error("element 2 parsing failed");
229  }
230 }
231 
232 } // namespace stl_parser
233 
234 namespace egs_triangle_mesh {
235 
236 static void input_errors() {
237  egsSetInfoFunction(Warning, egsInfoThrowing);
238  egsSetInfoFunction(Fatal, egsInfoThrowing);
239 
240  // Missing `file` key fails
241  {
242  EGS_Input egsinp;
243  std::string bad_stl_file(
244  ":start geometry definition:\n"
245  " :start geometry:\n"
246  " name = my_mesh\n"
247  " library = egs_triangle_mesh\n"
248  " #file = bad_input.stl\n" // commented out filename
249  " :stop geometry:\n"
250  " simulation geometry = my_mesh\n"
251  ":stop geometry definition:\n"
252  );
253  egsinp.setContentFromString(bad_stl_file);
254  EXPECT_ERROR(EGS_TriangleMesh::createGeometry(&egsinp),
255  "createGeometry(EGS_TriangleMesh): no mesh file key `file` in input\n");
256  }
257 
258  // Non-existent STL file fails
259  {
260  EGS_Input egsinp;
261  std::string bad_stl_file(
262  ":start geometry definition:\n"
263  " :start geometry:\n"
264  " name = my_mesh\n"
265  " library = egs_triangle_mesh\n"
266  " file = bad_input.stl\n" // bad filename
267  " :stop geometry:\n"
268  " simulation geometry = my_mesh\n"
269  ":stop geometry definition:\n"
270  );
271  egsinp.setContentFromString(bad_stl_file);
272  EXPECT_ERROR(EGS_TriangleMesh::createGeometry(&egsinp),
273  "\ncreateGeometry(EGS_TriangleMesh): STL file `bad_input.stl` does not exist or is not readable\n");
274  }
275 
276  // bad scale value fails
277  {
278  EGS_Input egsinp;
279  std::string bad_stl_scale(
280  ":start geometry definition:\n"
281  " :start geometry:\n"
282  " name = my_mesh\n"
283  " library = egs_triangle_mesh\n"
284  " file = sample_bin.stl\n"
285  " scale = -1.0\n"
286  " :stop geometry:\n"
287  " simulation geometry = my_mesh\n"
288  ":stop geometry definition:\n"
289  );
290  egsinp.setContentFromString(bad_stl_scale);
291  EXPECT_ERROR(EGS_TriangleMesh::createGeometry(&egsinp),
292  "createGeometry(EGS_TriangleMesh): invalid scale value (-1), expected a positive number\n");
293  }
294 
295  // Reset egs info functions
297 }
298 
299 static void zero_elements() {
300  EGS_TriangleMeshSpec spec; // empty mesh spec
301  EXPECT_ERROR(EGS_TriangleMesh(std::move(spec)), "empty triangles vector in EGS_TriangleMesh constructor");
302 
303 }
304 
305 static void check_scale() {
306 
307  EGS_TriangleMeshSpec mesh = stl_parser::parse_stl_file("sample_bin.stl");
308  EGS_TriangleMeshSpec scaled_mesh = stl_parser::parse_stl_file("sample_bin.stl");
309  scaled_mesh.scale(0.1);
310 
311  for (int i = 0; i < mesh.elements.size(); i++) {
312  if (!egsvec_eq(mesh.elements[i].n, scaled_mesh.elements[i].n) ||
313  !egsvec_eq(0.1 * mesh.elements[i].a, scaled_mesh.elements[i].a) ||
314  !egsvec_eq(0.1 * mesh.elements[i].b, scaled_mesh.elements[i].b) ||
315  !egsvec_eq(0.1 * mesh.elements[i].c, scaled_mesh.elements[i].c)) {
316  throw std::runtime_error("STL mesh scaling failed");
317  }
318  }
319 }
320 
321 // TODO: is there a way to test that `set media` block exists and only has
322 // one medium? Maybe not, looking at the EGS_BaseGeometry::setMedia impl.
323 
324 } // namespace egs_triangle_mesh
325 
326 int main() {
327 
328  RUN_TEST(stl_parser::empty_file());
329  RUN_TEST(stl_parser::missing_file());
330  RUN_TEST(stl_parser::catch_zero_triangles());
331  RUN_TEST(stl_parser::truncated_file());
332  RUN_TEST(stl_parser::parse_binary_file());
333  RUN_TEST(stl_parser::parse_ascii_file());
334 
335  RUN_TEST(egs_triangle_mesh::input_errors());
336  RUN_TEST(egs_triangle_mesh::zero_elements());
337  RUN_TEST(egs_triangle_mesh::check_scale());
338 
339  std::cerr << "\ntest result: " << num_total - num_failed << " out of " <<
340  num_total << " tests passed\n";
341 
342  return num_failed;
343 }
344 
345 #undef RUN_TEST
346 #undef EXPECT_ERROR
static EGS_BaseGeometry * createGeometry(EGS_Input *)
Create a geometry (or geometries) from a given input.
A class for storing information in a tree-like structure of key-value pairs. This class is used throu...
Definition: egs_input.h:182
int setContentFromString(string &input)
Definition: egs_input.cpp:209
A container for raw unstructured triangle surface mesh data.
void scale(EGS_Float factor)
Multiply all node coordinates by a constant factor.
std::vector< EGS_TriangleMeshSpec::Triangle > elements
Unique elements.
A triangular surface mesh geometry.
A class representing 3D vectors.
Definition: egs_vector.h:57
EGS_Float y
y-component
Definition: egs_vector.h:62
EGS_Float z
z-component
Definition: egs_vector.h:63
EGS_Float x
x-component
Definition: egs_vector.h:61
EGS_Input class header file.
Triangle surface mesh geometry: header.
int main(int argc, char **argv)
A main program for egspp applications.
Definition: egspp.cpp:58
EGS_InfoFunction egsSetInfoFunction(EGS_InfoType t, EGS_InfoFunction func)
Set a function to be used for outputing information, warning messages or reporting fatal errors.
void egsSetDefaultIOFunctions()
Reset I/O functions to their defaults.