1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38 package ffx.numerics.quickhull;
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69 import java.util.Random;
70
71 import ffx.utilities.FFXTest;
72 import org.junit.Test;
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95 public class QuickHull3DTest extends FFXTest {
96
97 static private final double DOUBLE_PREC = 2.2204460492503131e-16;
98
99 static boolean triangulate = false;
100
101 static boolean doTesting = true;
102
103 static boolean doTiming = false;
104
105 static boolean debugEnable = false;
106
107 static final int NO_DEGENERACY = 0;
108
109 static final int EDGE_DEGENERACY = 1;
110
111 static final int VERTEX_DEGENERACY = 2;
112
113 Random rand;
114
115 static boolean testRotation = true;
116
117 static int degeneracyTest = VERTEX_DEGENERACY;
118
119 static double epsScale = 2.0;
120
121
122
123
124 public QuickHull3DTest() {
125 rand = new Random();
126 rand.setSeed(0x1234);
127 }
128
129
130
131
132
133
134
135
136
137
138
139 public boolean faceIndicesEqual(int[] indices1, int[] indices2) {
140 if (indices1.length != indices2.length) {
141 return false;
142 }
143 int len = indices1.length;
144 int j;
145 for (j = 0; j < len; j++) {
146 if (indices1[0] == indices2[j]) {
147 break;
148 }
149 }
150 if (j == len) {
151 return false;
152 }
153 for (int i = 1; i < len; i++) {
154 if (indices1[i] != indices2[(j + i) % len]) {
155 return false;
156 }
157 }
158 return true;
159 }
160
161
162
163
164
165
166
167
168
169
170
171 public double[] randomPoints(int num, double range) {
172 double[] coords = new double[num * 3];
173
174 for (int i = 0; i < num; i++) {
175 for (int k = 0; k < 3; k++) {
176 coords[i * 3 + k] = 2 * range * (rand.nextDouble() - 0.5);
177 }
178 }
179 return coords;
180 }
181
182 private void randomlyPerturb(Point3d pnt, double tol) {
183 pnt.x += tol * (rand.nextDouble() - 0.5);
184 pnt.y += tol * (rand.nextDouble() - 0.5);
185 pnt.z += tol * (rand.nextDouble() - 0.5);
186 }
187
188
189
190
191
192
193
194
195
196
197
198
199 public double[] randomDegeneratePoints(int num, int dimen) {
200 double[] coords = new double[num * 3];
201 Point3d pnt = new Point3d();
202
203 Point3d base = new Point3d();
204 base.setRandom(-1, 1, rand);
205
206 double tol = DOUBLE_PREC;
207
208 if (dimen == 0) {
209 for (int i = 0; i < num; i++) {
210 pnt.set(base);
211 randomlyPerturb(pnt, tol);
212 coords[i * 3 + 0] = pnt.x;
213 coords[i * 3 + 1] = pnt.y;
214 coords[i * 3 + 2] = pnt.z;
215 }
216 } else if (dimen == 1) {
217 Vector3d u = new Vector3d();
218 u.setRandom(-1, 1, rand);
219 u.normalize();
220 for (int i = 0; i < num; i++) {
221 double a = 2 * (rand.nextDouble() - 0.5);
222 pnt.scale(a, u);
223 pnt.add(base);
224 randomlyPerturb(pnt, tol);
225 coords[i * 3 + 0] = pnt.x;
226 coords[i * 3 + 1] = pnt.y;
227 coords[i * 3 + 2] = pnt.z;
228 }
229 } else
230 {
231 Vector3d nrm = new Vector3d();
232 nrm.setRandom(-1, 1, rand);
233 nrm.normalize();
234 for (int i = 0; i < num; i++) {
235
236 Vector3d perp = new Vector3d();
237 pnt.setRandom(-1, 1, rand);
238 perp.scale(pnt.dot(nrm), nrm);
239 pnt.sub(perp);
240 pnt.add(base);
241 randomlyPerturb(pnt, tol);
242 coords[i * 3 + 0] = pnt.x;
243 coords[i * 3 + 1] = pnt.y;
244 coords[i * 3 + 2] = pnt.z;
245 }
246 }
247 return coords;
248 }
249
250
251
252
253
254
255
256
257
258
259
260 public double[] randomSphericalPoints(int num, double radius) {
261 double[] coords = new double[num * 3];
262 Point3d pnt = new Point3d();
263
264 for (int i = 0; i < num;) {
265 pnt.setRandom(-radius, radius, rand);
266 if (pnt.norm() <= radius) {
267 coords[i * 3 + 0] = pnt.x;
268 coords[i * 3 + 1] = pnt.y;
269 coords[i * 3 + 2] = pnt.z;
270 i++;
271 }
272 }
273 return coords;
274 }
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292 public double[] randomCubedPoints(int num, double range, double max) {
293 double[] coords = new double[num * 3];
294
295 for (int i = 0; i < num; i++) {
296 for (int k = 0; k < 3; k++) {
297 double x = 2 * range * (rand.nextDouble() - 0.5);
298 if (x > max) {
299 x = max;
300 } else if (x < -max) {
301 x = -max;
302 }
303 coords[i * 3 + k] = x;
304 }
305 }
306 return coords;
307 }
308
309 private double[] shuffleCoords(double[] coords) {
310 int num = coords.length / 3;
311
312 for (int i = 0; i < num; i++) {
313 int i1 = rand.nextInt(num);
314 int i2 = rand.nextInt(num);
315 for (int k = 0; k < 3; k++) {
316 double tmp = coords[i1 * 3 + k];
317 coords[i1 * 3 + k] = coords[i2 * 3 + k];
318 coords[i2 * 3 + k] = tmp;
319 }
320 }
321 return coords;
322 }
323
324
325
326
327
328
329
330
331
332
333
334
335 public double[] randomGridPoints(int gridSize, double width) {
336
337
338
339
340 int num = gridSize * gridSize * gridSize;
341
342 double[] coords = new double[num * 3];
343
344 int idx = 0;
345 for (int i = 0; i < gridSize; i++) {
346 for (int j = 0; j < gridSize; j++) {
347 for (int k = 0; k < gridSize; k++) {
348 coords[idx * 3 + 0] = (i / (double) (gridSize - 1) - 0.5) * width;
349 coords[idx * 3 + 1] = (j / (double) (gridSize - 1) - 0.5) * width;
350 coords[idx * 3 + 2] = (k / (double) (gridSize - 1) - 0.5) * width;
351 idx++;
352 }
353 }
354 }
355 shuffleCoords(coords);
356 return coords;
357 }
358
359 void explicitFaceCheck(QuickHull3D hull, int[][] checkFaces) throws Exception {
360 int[][] faceIndices = hull.getFaces();
361 if (faceIndices.length != checkFaces.length) {
362 throw new Exception("Error: " + faceIndices.length + " faces vs. " + checkFaces.length);
363 }
364
365 Point3d[] pnts = hull.getVertices();
366 int[] vtxIndices = hull.getVertexPointIndices();
367
368 for (int j = 0; j < faceIndices.length; j++) {
369 int[] idxs = faceIndices[j];
370 for (int k = 0; k < idxs.length; k++) {
371 idxs[k] = vtxIndices[idxs[k]];
372 }
373 }
374 for (int i = 0; i < checkFaces.length; i++) {
375 int[] cf = checkFaces[i];
376 int j;
377 for (j = 0; j < faceIndices.length; j++) {
378 if (faceIndices[j] != null) {
379 if (faceIndicesEqual(cf, faceIndices[j])) {
380 faceIndices[j] = null;
381 break;
382 }
383 }
384 }
385 if (j == faceIndices.length) {
386 String s = "";
387 for (int k = 0; k < cf.length; k++) {
388 s += cf[k] + " ";
389 }
390 throw new Exception("Error: face " + s + " not found");
391 }
392 }
393 }
394
395 int cnt = 0;
396
397 void singleTest(double[] coords, int[][] checkFaces) throws Exception {
398 QuickHull3D hull = new QuickHull3D();
399
400 hull.build(coords, coords.length / 3);
401 if (triangulate) {
402 hull.triangulate();
403 }
404
405 if (!hull.check(System.out)) {
406 throw new AssertionError();
407 }
408 if (checkFaces != null) {
409 explicitFaceCheck(hull, checkFaces);
410 }
411 if (degeneracyTest != NO_DEGENERACY) {
412 degenerateTest(hull, coords);
413 }
414 }
415
416 double[] addDegeneracy(int type, double[] coords, QuickHull3D hull) {
417 int numv = coords.length / 3;
418 int[][] faces = hull.getFaces();
419 double[] coordsx = new double[coords.length + faces.length * 3];
420 for (int i = 0; i < coords.length; i++) {
421 coordsx[i] = coords[i];
422 }
423
424 double[] lam = new double[3];
425 double eps = hull.getDistanceTolerance();
426
427 for (int i = 0; i < faces.length; i++) {
428
429 lam[0] = rand.nextDouble();
430 lam[1] = 1 - lam[0];
431 lam[2] = 0.0;
432
433 if (type == VERTEX_DEGENERACY && (i % 2 == 0)) {
434 lam[0] = 1.0;
435 lam[1] = lam[2] = 0;
436 }
437
438 for (int j = 0; j < 3; j++) {
439 int vtxi = faces[i][j];
440 for (int k = 0; k < 3; k++) {
441 coordsx[numv * 3 + k] += lam[j] * coords[vtxi * 3 + k] + epsScale * eps * (rand.nextDouble() - 0.5);
442 }
443 }
444 numv++;
445 }
446 shuffleCoords(coordsx);
447 return coordsx;
448 }
449
450 void degenerateTest(QuickHull3D hull, double[] coords) throws Exception {
451 double[] coordsx = addDegeneracy(degeneracyTest, coords, hull);
452
453 QuickHull3D xhull = new QuickHull3D();
454
455 try {
456 xhull.build(coordsx, coordsx.length / 3);
457 if (triangulate) {
458 xhull.triangulate();
459 }
460 } catch (Exception e) {
461 for (int i = 0; i < coordsx.length / 3; i++) {
462 logger.info(coordsx[i * 3 + 0] + ", " + coordsx[i * 3 + 1] + ", " + coordsx[i * 3 + 2] + ", ");
463 }
464 }
465
466 if (!xhull.check(System.out)) {
467 throw new AssertionError();
468 }
469 }
470
471 void rotateCoords(double[] res, double[] xyz, double roll, double pitch, double yaw) {
472 double sroll = Math.sin(roll);
473 double croll = Math.cos(roll);
474 double spitch = Math.sin(pitch);
475 double cpitch = Math.cos(pitch);
476 double syaw = Math.sin(yaw);
477 double cyaw = Math.cos(yaw);
478
479 double m00 = croll * cpitch;
480 double m10 = sroll * cpitch;
481 double m20 = -spitch;
482
483 double m01 = croll * spitch * syaw - sroll * cyaw;
484 double m11 = sroll * spitch * syaw + croll * cyaw;
485 double m21 = cpitch * syaw;
486
487 double m02 = croll * spitch * cyaw + sroll * syaw;
488 double m12 = sroll * spitch * cyaw - croll * syaw;
489 double m22 = cpitch * cyaw;
490
491 double x, y, z;
492
493 for (int i = 0; i < xyz.length - 2; i += 3) {
494 res[i + 0] = m00 * xyz[i + 0] + m01 * xyz[i + 1] + m02 * xyz[i + 2];
495 res[i + 1] = m10 * xyz[i + 0] + m11 * xyz[i + 1] + m12 * xyz[i + 2];
496 res[i + 2] = m20 * xyz[i + 0] + m21 * xyz[i + 1] + m22 * xyz[i + 2];
497 }
498 }
499
500 void printCoords(double[] coords) {
501 int nump = coords.length / 3;
502 for (int i = 0; i < nump; i++) {
503 logger.info(coords[i * 3 + 0] + ", " + coords[i * 3 + 1] + ", " + coords[i * 3 + 2] + ", ");
504 }
505 }
506
507 void testException(double[] coords, String msg) {
508 QuickHull3D hull = new QuickHull3D();
509 Exception ex = null;
510 try {
511 hull.build(coords);
512 } catch (Exception e) {
513 ex = e;
514 }
515 if (ex == null) {
516 logger.info("Expected exception " + msg);
517 logger.info("Got no exception");
518 logger.info("Input pnts:");
519 printCoords(coords);
520 throw new AssertionError();
521 } else if (ex.getMessage() == null || !ex.getMessage().equals(msg)) {
522 logger.info("Expected exception " + msg);
523 logger.info("Got exception " + ex.getMessage());
524 logger.info("Input pnts:");
525 printCoords(coords);
526 throw new AssertionError();
527 }
528 }
529
530 void test(double[] coords, int[][] checkFaces) throws Exception {
531 double[][] rpyList = new double[][]{
532 {
533 0,
534 0,
535 0
536 },
537 {
538 10,
539 20,
540 30
541 },
542 {
543 -45,
544 60,
545 91
546 },
547 {
548 125,
549 67,
550 81
551 }
552 };
553 double[] xcoords = new double[coords.length];
554
555 singleTest(coords, checkFaces);
556 if (testRotation) {
557 for (int i = 0; i < rpyList.length; i++) {
558 double[] rpy = rpyList[i];
559 rotateCoords(xcoords, coords, Math.toRadians(rpy[0]), Math.toRadians(rpy[1]), Math.toRadians(rpy[2]));
560 singleTest(xcoords, checkFaces);
561 }
562 }
563 }
564
565
566
567
568
569 public void explicitAndRandomTests() {
570 try {
571 double[] coords = null;
572
573 logger.info("Testing degenerate input ...");
574 for (int dimen = 0; dimen < 3; dimen++) {
575 for (int i = 0; i < 10; i++) {
576 coords = randomDegeneratePoints(10, dimen);
577 if (dimen == 0) {
578 testException(coords, "Input points appear to be coincident");
579 } else if (dimen == 1) {
580 testException(coords, "Input points appear to be colinear");
581 } else if (dimen == 2) {
582 testException(coords, "Input points appear to be coplanar");
583 }
584 }
585 }
586
587 logger.info("Explicit tests ...");
588
589
590 coords = new double[]{
591 21,
592 0,
593 0,
594 0,
595 21,
596 0,
597 0,
598 0,
599 0,
600 18,
601 2,
602 6,
603 1,
604 18,
605 5,
606 2,
607 1,
608 3,
609 14,
610 3,
611 10,
612 4,
613 14,
614 14,
615 3,
616 4,
617 10,
618 10,
619 6,
620 12,
621 5,
622 10,
623 15,
624 };
625 test(coords, null);
626
627 coords = new double[]{
628 0.0,
629 0.0,
630 0.0,
631 21.0,
632 0.0,
633 0.0,
634 0.0,
635 21.0,
636 0.0,
637 2.0,
638 1.0,
639 2.0,
640 17.0,
641 2.0,
642 3.0,
643 1.0,
644 19.0,
645 6.0,
646 4.0,
647 3.0,
648 5.0,
649 13.0,
650 4.0,
651 5.0,
652 3.0,
653 15.0,
654 8.0,
655 6.0,
656 5.0,
657 6.0,
658 9.0,
659 6.0,
660 11.0,
661 };
662 test(coords, null);
663
664 logger.info("Testing 20 to 200 random points ...");
665 for (int n = 20; n < 200; n += 10) {
666 for (int i = 0; i < 10; i++) {
667 coords = randomPoints(n, 1.0);
668 test(coords, null);
669 }
670 }
671
672 logger.info("Testing 20 to 200 random points in a sphere ...");
673 for (int n = 20; n < 200; n += 10) {
674 for (int i = 0; i < 10; i++) {
675 coords = randomSphericalPoints(n, 1.0);
676 test(coords, null);
677 }
678 }
679
680 logger.info("Testing 20 to 200 random points clipped to a cube ...");
681 for (int n = 20; n < 200; n += 10) {
682 for (int i = 0; i < 10; i++) {
683 coords = randomCubedPoints(n, 1.0, 0.5);
684 test(coords, null);
685 }
686 }
687
688 logger.info("Testing 8 to 1000 randomly shuffled points on a grid ...");
689 for (int n = 2; n <= 10; n++) {;
690 for (int i = 0; i < 10; i++) {
691 coords = randomGridPoints(n, 4.0);
692 test(coords, null);
693 }
694 }
695
696 } catch (Exception e) {
697 throw new AssertionError();
698 }
699
700 logger.info("\nPassed\n");
701 }
702
703
704
705
706 public void timingTests() {
707 long t0, t1;
708 int n = 10;
709 QuickHull3D hull = new QuickHull3D();
710 logger.info("warming up ... ");
711 for (int i = 0; i < 2; i++) {
712 double[] coords = randomSphericalPoints(10000, 1.0);
713 hull.build(coords);
714 }
715 int cnt = 10;
716 for (int i = 0; i < 4; i++) {
717 n *= 10;
718 double[] coords = randomSphericalPoints(n, 1.0);
719 t0 = System.currentTimeMillis();
720 for (int k = 0; k < cnt; k++) {
721 hull.build(coords);
722 }
723 t1 = System.currentTimeMillis();
724 logger.info(n + " points: " + (t1 - t0) / (double) cnt + " msec");
725 }
726 }
727
728
729
730
731
732
733
734
735
736 public static void main(String[] args) {
737 QuickHull3DTest tester = new QuickHull3DTest();
738
739 for (int i = 0; i < args.length; i++) {
740 if (args[i].equals("-timing")) {
741 doTiming = true;
742 doTesting = false;
743 } else {
744 logger.info("Usage: java com.github.quickhull3d.QuickHull3DTest [-timing]");
745 throw new AssertionError();
746 }
747 }
748 if (doTesting) {
749 tester.explicitAndRandomTests();
750 }
751
752 if (doTiming) {
753 tester.timingTests();
754 }
755 }
756
757 @Test
758 public void doTest() {
759 main(new String[0]);
760 }
761
762 }