View Javadoc
1   // ******************************************************************************
2   //
3   // Title:       Force Field X.
4   // Description: Force Field X - Software for Molecular Biophysics.
5   // Copyright:   Copyright (c) Michael J. Schnieders 2001-2025.
6   //
7   // This file is part of Force Field X.
8   //
9   // Force Field X is free software; you can redistribute it and/or modify it
10  // under the terms of the GNU General Public License version 3 as published by
11  // the Free Software Foundation.
12  //
13  // Force Field X is distributed in the hope that it will be useful, but WITHOUT
14  // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
15  // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
16  // details.
17  //
18  // You should have received a copy of the GNU General Public License along with
19  // Force Field X; if not, write to the Free Software Foundation, Inc., 59 Temple
20  // Place, Suite 330, Boston, MA 02111-1307 USA
21  //
22  // Linking this library statically or dynamically with other modules is making a
23  // combined work based on this library. Thus, the terms and conditions of the
24  // GNU General Public License cover the whole combination.
25  //
26  // As a special exception, the copyright holders of this library give you
27  // permission to link this library with independent modules to produce an
28  // executable, regardless of the license terms of these independent modules, and
29  // to copy and distribute the resulting executable under terms of your choice,
30  // provided that you also meet, for each linked independent module, the terms
31  // and conditions of the license of that module. An independent module is a
32  // module which is not derived from or based on this library. If you modify this
33  // library, you may extend this exception to your version of the library, but
34  // you are not obligated to do so. If you do not wish to do so, delete this
35  // exception statement from your version.
36  //
37  // ******************************************************************************
38  package ffx.numerics.quickhull;
39  
40  /*
41   * #%L
42   * A Robust 3D Convex Hull Algorithm in Java
43   * %%
44   * Copyright (C) 2004 - 2014 John E. Lloyd
45   * %%
46   * Redistribution and use in source and binary forms, with or without
47   * modification, are permitted provided that the following conditions are met:
48   * 
49   * 1. Redistributions of source code must retain the above copyright notice,
50   *    this list of conditions and the following disclaimer.
51   * 2. Redistributions in binary form must reproduce the above copyright notice,
52   *    this list of conditions and the following disclaimer in the documentation
53   *    and/or other materials provided with the distribution.
54   * 
55   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
56   * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
57   * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
58   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
59   * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
60   * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
61   * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
62   * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
63   * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
64   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
65   * POSSIBILITY OF SUCH DAMAGE.
66   * #L%
67   */
68  
69  import java.util.Random;
70  
71  import ffx.utilities.FFXTest;
72  import org.junit.Test;
73  
74  /**
75   * Testing class for QuickHull3D. Running the command
76   * 
77   * <pre>
78   *   java com.github.quickhull3d.QuickHull3DTest
79   * </pre>
80   * 
81   * will cause QuickHull3D to be tested on a number of randomly choosen input
82   * sets, with degenerate points added near the edges and vertics of the convex
83   * hull.
84   * <p>
85   * The command
86   * 
87   * <pre>
88   *   java com.github.quickhull3d.QuickHull3DTest -timing
89   * </pre>
90   * 
91   * will cause timing information to be produced instead.
92   * 
93   * @author John E. Lloyd, Fall 2004
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; // random number generator
114 
115     static boolean testRotation = true;
116 
117     static int degeneracyTest = VERTEX_DEGENERACY;
118 
119     static double epsScale = 2.0;
120 
121     /**
122      * Creates a testing object.
123      */
124     public QuickHull3DTest() {
125         rand = new Random();
126         rand.setSeed(0x1234);
127     }
128 
129     /**
130      * Returns true if two face index sets are equal, modulo a cyclical
131      * permuation.
132      * 
133      * @param indices1
134      *            index set for first face
135      * @param indices2
136      *            index set for second face
137      * @return true if the index sets are equivalent
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      * Returns the coordinates for <code>num</code> points whose x, y, and z
163      * values are randomly chosen within a given range.
164      * 
165      * @param num
166      *            number of points to produce
167      * @param range
168      *            coordinate values will lie between -range and range
169      * @return array of coordinate values
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      * Returns the coordinates for <code>num</code> randomly chosen points which
190      * are degenerate which respect to the specified dimensionality.
191      * 
192      * @param num
193      *            number of points to produce
194      * @param dimen
195      *            dimensionality of degeneracy: 0 = coincident, 1 = colinear, 2
196      *            = coplaner.
197      * @return array of coordinate values
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 // dimen == 2
230         {
231             Vector3d nrm = new Vector3d();
232             nrm.setRandom(-1, 1, rand);
233             nrm.normalize();
234             for (int i = 0; i < num; i++) { // compute a random point and
235                                             // project it to the plane
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      * Returns the coordinates for <code>num</code> points whose x, y, and z
252      * values are randomly chosen to lie within a sphere.
253      * 
254      * @param num
255      *            number of points to produce
256      * @param radius
257      *            radius of the sphere
258      * @return array of coordinate values
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      * Returns the coordinates for <code>num</code> points whose x, y, and z
278      * values are each randomly chosen to lie within a specified range, and then
279      * clipped to a maximum absolute value. This means a large number of points
280      * may lie on the surface of cube, which is useful for creating degenerate
281      * convex hull situations.
282      * 
283      * @param num
284      *            number of points to produce
285      * @param range
286      *            coordinate values will lie between -range and range, before
287      *            clipping
288      * @param max
289      *            maximum absolute value to which the coordinates are clipped
290      * @return array of coordinate values
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      * Returns randomly shuffled coordinates for points on a three-dimensional
326      * grid, with a presecribed width between each point.
327      * 
328      * @param gridSize
329      *            number of points in each direction, so that the total number
330      *            of points produced is the cube of gridSize.
331      * @param width
332      *            distance between each point along a particular direction
333      * @return array of coordinate values
334      */
335     public double[] randomGridPoints(int gridSize, double width) {
336         // gridSize gives the number of points across a given dimension
337         // any given coordinate indexed by i has value
338         // (i/(gridSize-1) - 0.5)*width
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         // translate face indices back into original indices
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             // random point on an edge
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      * Runs a set of explicit and random tests on QuickHull3D, and prints
567      * <code>Passed</code> to System.out if all is well.
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             // test cases furnished by Mariano Zelke, Berlin
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      * Runs timing tests on QuickHull3D, and prints the results to System.out.
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      * Runs a set of tests on the QuickHull3D class, and prints
730      * <code>Passed</code> if all is well. Otherwise, an error message and stack
731      * trace are printed.
732      * <p>
733      * If the option <code>-timing</code> is supplied, then timing information
734      * is produced instead.
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 }