/* jCAE stand for Java Computer Aided Engineering. Features are : Small CAD
modeler, Finite element mesher, Plugin architecture.
Copyright (C) 2007,2008,2009, by EADS France
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */
package org.jcae.mesh;
import org.jcae.mesh.xmldata.MeshReader;
import org.jcae.mesh.xmldata.MeshToSoupConvert;
import org.jcae.mesh.xmldata.MMesh1DReader;
import org.jcae.mesh.amibe.ds.MMesh1D;
import org.jcae.mesh.amibe.validation.*;
import org.jcae.mesh.amibe.traits.MeshTraitsBuilder;
import org.jcae.mesh.amibe.ds.Mesh;
import org.jcae.mesh.amibe.ds.Triangle;
import org.jcae.mesh.amibe.ds.Vertex;
import org.jcae.mesh.oemm.RawStorage;
import java.io.File;
import java.io.IOException;
import java.io.FileNotFoundException;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.EntityResolver;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import java.util.logging.Logger;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.FileHandler;
import java.util.logging.XMLFormatter;
import org.junit.Test;
import static org.junit.Assert.*;
import org.junit.BeforeClass;
public class MesherTest
{
private Logger root;
private static final String dir = System.getProperty("test.dir", "test");
private static int counter = 0;
private static FileHandler app = null;
private static final XMLFormatter xmlLayout = new XMLFormatter();
private static File logFile = null;
private static final EntityResolver myEntityResolver = new FakeEntityResolver();
// Hopefully test timing depends linearly on CPU power,
private static final long timerScale = Long.parseLong(System.getProperty("org.jcae.mesh.timerScale", "1000"));
// This EntityResolver avoids FileNotFoundException when parsing XML output since logger.dtd is unavailable.
public static class FakeEntityResolver implements EntityResolver
{
public final InputSource resolveEntity(String publicId, String systemId)
{
return new InputSource(new java.io.ByteArrayInputStream(new byte[0]));
}
}
private void checkNumberOfTriangles(String outputDir, int target, double delta)
{
int [] res = MeshReader.getInfos(outputDir);
assertTrue("Number of triangles out of range ["+(target*(1.0-delta))+" - "+(target*(1.0+delta))+"]: "+res[1], res[1] >= target*(1.0-delta) && res[1] <= target*(1.0+delta));
}
private void checkMeshQuality(String outputDir, double minAngleDeg, double maxEdgeLength)
{
Mesh mesh = new Mesh(new MeshTraitsBuilder());
try
{
MeshReader.readObject3D(mesh, outputDir);
}
catch (IOException ex)
{
throw new RuntimeException();
}
QualityProcedure qprocAngle = new MinAngleFace();
QualityFloat dataAngle = new QualityFloat(mesh.getTriangles().size());
dataAngle.setQualityProcedure(qprocAngle);
QualityProcedure qprocEdgeLength = new MaxLengthFace();
QualityFloat dataEdgeLength = new QualityFloat(mesh.getTriangles().size());
dataEdgeLength.setQualityProcedure(qprocEdgeLength);
for (Triangle f: mesh.getTriangles())
{
if (!f.isWritable())
continue;
dataAngle.compute(f);
dataEdgeLength.compute(f);
}
dataAngle.finish();
dataEdgeLength.finish();
double resAngle = dataAngle.getValueByPercent(0.0) * 180.0 / 3.14159265358979323844;
assertTrue("Min angle too low; expected: "+minAngleDeg+" found: "+resAngle, minAngleDeg < resAngle);
double resEdgeLength = dataEdgeLength.getValueByPercent(1.0);
assertTrue("Max edge length too large; expected: "+maxEdgeLength+" found: "+resEdgeLength, maxEdgeLength > resEdgeLength);
}
public static class ComputeTriangleQuality implements RawStorage.SoupReaderInterface
{
private final Vertex [] n = new Vertex[3];
private final Mesh mesh = new Mesh(new MeshTraitsBuilder());
private final QualityFloat [] data;
public ComputeTriangleQuality(QualityFloat [] qdata)
{
data = qdata;
}
public void processVertex(int i, double [] xyz)
{
n[i] = mesh.createVertex(xyz[0], xyz[1], xyz[2]);
}
public void processTriangle(int group)
{
Triangle t = mesh.createTriangle(n);
for (QualityFloat qf: data)
qf.compute(t);
}
}
private void checkLargeMeshQuality(String outputDir, double minAngleDeg, double maxEdgeLength)
{
String soupFile = outputDir+java.io.File.separator+"soup";
if (!(new File(soupFile)).exists())
{
// Create a outputDir/soup file
MMesh1D mesh1d = MMesh1DReader.readObject(outputDir);
MeshToSoupConvert.meshToSoup(outputDir, mesh1d.getGeometry());
}
QualityProcedure qprocAngle = new MinAngleFace();
QualityFloat dataAngle = new QualityFloat(100000);
dataAngle.setQualityProcedure(qprocAngle);
QualityProcedure qprocEdgeLength = new MaxLengthFace();
QualityFloat dataEdgeLength = new QualityFloat(100000);
dataEdgeLength.setQualityProcedure(qprocEdgeLength);
// Read triangle soup
ComputeTriangleQuality ctq = new ComputeTriangleQuality(new QualityFloat[] { dataAngle, dataEdgeLength });
RawStorage.readSoup(soupFile, ctq);
dataAngle.finish();
dataEdgeLength.finish();
double resAngle = dataAngle.getValueByPercent(0.0) * 180.0 / 3.14159265358979323844;
assertTrue("Min angle too low; expected: "+minAngleDeg+" found: "+resAngle, minAngleDeg < resAngle);
double resEdgeLength = dataEdgeLength.getValueByPercent(1.0);
assertTrue("Max edge length too large; expected: "+maxEdgeLength+" found: "+resEdgeLength, maxEdgeLength > resEdgeLength);
}
private void startLogger()
{
LogManager.getLogManager().reset();
root = Logger.getLogger("");
counter++;
try
{
File logDir = new File(dir, "logs");
logDir.mkdirs();
if(!logDir.exists() || !logDir.isDirectory())
throw new RuntimeException("Unable to create directory "+logDir.getPath());
logFile = new File(logDir, "test."+counter+".xml");
app = new FileHandler(logFile.getPath(), false);
app.setFormatter(xmlLayout);
app.setLevel(Level.INFO);
root.addHandler(app);
}
catch (IOException ex)
{
throw new RuntimeException();
}
}
private Document stopLogger()
{
app.close();
app = null;
DocumentBuilderFactory dbf = null;
try {
dbf = DocumentBuilderFactory.newInstance();
dbf.setIgnoringElementContentWhitespace(true);
dbf.setValidating(false);
} catch(FactoryConfigurationError fce) {
throw fce;
}
try {
DocumentBuilder docBuilder = dbf.newDocumentBuilder();
docBuilder.setEntityResolver(myEntityResolver);
return docBuilder.parse(logFile);
} catch (ParserConfigurationException ex) {
ex.printStackTrace();
throw new RuntimeException();
} catch (SAXException ex) {
ex.printStackTrace();
throw new RuntimeException();
} catch (FileNotFoundException ex) {
ex.printStackTrace();
throw new RuntimeException();
} catch (IOException ex) {
ex.printStackTrace();
throw new RuntimeException();
}
}
private long getMesherRuntimeMillis(Document doc)
{
XPath xpath = XPathFactory.newInstance().newXPath();
try
{
XPathExpression xpathTimestamp = xpath.compile("//millis/text()");
// Find first <message> containing "Meshing face" message
NodeList events = (NodeList) xpath.evaluate("//message/text()[contains(string(), 'Meshing face')]/ancestor::record", doc, XPathConstants.NODESET);
long t1 = Long.parseLong(xpathTimestamp.evaluate(events.item(0)));
// <record> element after last 'Meshing face' message
long t2 = Long.parseLong(xpathTimestamp.evaluate(events.item(events.getLength() - 1).getNextSibling()));
return t2 - t1;
}
catch (XPathExpressionException ex)
{
throw new RuntimeException(ex.getCause());
}
}
// A timer for classes with a main method which calls logger.info() at start and end.
private void checkMainRuntimeMillis(Document doc, String klass, long seconds)
{
XPath xpath = XPathFactory.newInstance().newXPath();
try
{
NodeList events = (NodeList) xpath.evaluate("//record[class='"+klass+"']/millis/text()", doc, XPathConstants.NODESET);
assertTrue(events.getLength() == 2);
long t1 = Long.parseLong(events.item(0).getNodeValue());
long t2 = Long.parseLong(events.item(events.getLength() - 1).getNodeValue());
long time = t2 - t1;
assertTrue(""+klass+" too long: max time (ms): "+(seconds * timerScale)+" Effective time (ms): "+time, time < seconds * timerScale);
}
catch (XPathExpressionException ex)
{
throw new RuntimeException(ex.getCause());
}
}
private static String getGeometryFile(String type)
{
return dir + File.separator + "input" + File.separator + type +".brep";
}
private static String getOutputDirectory(String type, int cnt)
{
return dir + File.separator + "output" + File.separator + "test-"+type+"."+cnt;
}
private String runSingleTest(String type, double length, int nrTriangles, double minAngleDeg)
{
startLogger();
root.info("Running "+type+" test with length: "+length);
String geoFile = getGeometryFile(type);
String outDir = getOutputDirectory(type, counter);
org.jcae.mesh.Mesher.main(new String[] {geoFile, outDir, ""+length, "0.0"});
if (nrTriangles > 0)
checkNumberOfTriangles(outDir, nrTriangles, 0.1);
if (minAngleDeg > 0)
{
int [] res = MeshReader.getInfos(outDir);
if (res[1] < 100000)
checkMeshQuality(outDir, minAngleDeg, 4.0*length);
else
checkLargeMeshQuality(outDir, minAngleDeg, 4.0*length);
}
return outDir;
}
@BeforeClass
public static void checkEnv()
{
if (!Boolean.getBoolean("run.test.large"))
throw new RuntimeException("MesherTest takes too much time, re-run with -Drun.test.large=true if you really want to run this file");
}
private void runSingleTestTimer(String type, long seconds)
{
Document doc = stopLogger();
long time = getMesherRuntimeMillis(doc);
assertTrue("Mesher took too long: max time (ms): "+(seconds * timerScale)+" Effective time (ms): "+time, time < seconds * timerScale);
}
@Test public void sphere_0_05()
{
runSingleTest("sphere", 0.05, 12000, 10.0);
}
@Test public void timer_sphere_0_05()
{
runSingleTestTimer("sphere", 3L);
}
@Test public void sphere_0_01()
{
runSingleTest("sphere", 0.01, 300000, 10.0);
}
@Test public void timer_sphere_0_01()
{
runSingleTestTimer("sphere", 50L);
}
@Test public void sphere_0_005()
{
runSingleTest("sphere", 0.005, 1200000, 10.0);
}
@Test public void timer_sphere_0_005()
{
runSingleTestTimer("sphere", 200L);
}
// Results should be similar with sphere1000
@Test public void sphere1000_50()
{
runSingleTest("sphere1000", 50.0, 12000, 10.0);
}
@Test public void timer_sphere1000_50()
{
runSingleTestTimer("sphere1000", 3L);
}
@Test public void sphere1000_10()
{
runSingleTest("sphere1000", 10.0, 300000, 10.0);
}
@Test public void timer_sphere1000_10()
{
runSingleTestTimer("sphere1000", 50L);
}
@Test public void sphere1000_5()
{
runSingleTest("sphere1000", 5, 1200000, 10.0);
}
@Test public void timer_sphere1000_5()
{
runSingleTestTimer("sphere1000", 200L);
}
@Test public void cylinder_0_05()
{
runSingleTest("cylinder", 0.05, 36000, 20.0);
}
@Test public void timer_cylinder_0_05()
{
runSingleTestTimer("cylinder", 3L);
}
@Test public void cylinder_0_01()
{
runSingleTest("cylinder", 0.01, 900000, 20.0);
}
@Test public void timer_cylinder_0_01()
{
runSingleTestTimer("cylinder", 80L);
}
// cylinder1000 is different, radius is multiplied by 1000 but height by 600
@Test public void cylinder1000_50()
{
runSingleTest("cylinder1000", 50.0, 3600, 20.0);
}
@Test public void timer_cylinder1000_50()
{
runSingleTestTimer("cylinder1000", 1L);
}
@Test public void cylinder1000_10()
{
runSingleTest("cylinder1000", 10.0, 90000, 20.0);
}
@Test public void timer_cylinder1000_10()
{
runSingleTestTimer("cylinder1000", 10L);
}
@Test public void cylinder1000_5()
{
runSingleTest("cylinder1000", 5.0, 360000, 20.0);
}
@Test public void timer_cylinder1000_5()
{
runSingleTestTimer("cylinder1000", 45L);
}
@Test public void cone_0_01()
{
runSingleTest("cone", 0.01, 168000, 10.0);
}
@Test public void timer_cone_0_01()
{
runSingleTestTimer("cone", 20L);
}
@Test public void torus_0_01()
{
runSingleTest("torus", 0.01, 280000, 20.0);
}
@Test public void timer_torus_0_01()
{
runSingleTestTimer("torus", 30L);
}
@Test public void shellHole()
{
runSingleTest("shell_hole", 0.5, 300000, 20.0);
}
@Test public void timer_shellHole()
{
runSingleTestTimer("shell_hole", 30L);
}
@Test public void oemm()
{
String geoFile = getGeometryFile("15_cylinder_head");
if (!(new File(geoFile).exists()))
{
throw new RuntimeException("Missing brep file; you must download, uncompress http://www.opencascade.org/ex/att/15_cylinder_head.brep.gz and copy it into "+geoFile);
}
System.setProperty("org.jcae.mesh.Mesher.triangleSoup", "true");
String coarseDir = runSingleTest("15_cylinder_head", 5.0, 41000 , 0.0);
runSingleTestTimer("15_cylinder_head", 30L);
String fineDir = runSingleTest("15_cylinder_head", 1.2, 530000 , 0.0);
runSingleTestTimer("15_cylinder_head", 100L);
System.setProperty("org.jcae.mesh.Mesher.triangleSoup", "false");
startLogger();
org.jcae.mesh.MeshOEMMIndex.main(new String[] {fineDir, fineDir+"-oemm", "4", "10000", geoFile});
org.jcae.mesh.MeshOEMMPopulate.main(new String[] {fineDir+"-oemm", coarseDir+"-oemm", coarseDir+java.io.File.separator+"soup"});
Document doc = stopLogger();
checkMainRuntimeMillis(doc, "org.jcae.mesh.MeshOEMMIndex", 40L);
checkMainRuntimeMillis(doc, "org.jcae.mesh.MeshOEMMPopulate", 5L);
}
}