diff --git a/beast-fx/src/main/java/beastfx/app/tools/LogAnalyser.java b/beast-fx/src/main/java/beastfx/app/tools/LogAnalyser.java index 14358062..2217a830 100644 --- a/beast-fx/src/main/java/beastfx/app/tools/LogAnalyser.java +++ b/beast-fx/src/main/java/beastfx/app/tools/LogAnalyser.java @@ -250,21 +250,29 @@ protected void readLogFile(String fileName, int burnInPercentage) throws IOExcep if (str.indexOf('#') < 0 && str.matches("[-0-9].*")) if (++data >= 0 && data < m_fTraces[0].length) for (String str2 : str.split("\\s")) { - try { - if (str2.indexOf('.') >= 0) { - m_types[i] = type.REAL; + if ("true".equalsIgnoreCase(str2) || "false".equalsIgnoreCase(str2)) { + // set true = 1 false = 0 + m_types[i] = type.BOOL; + m_fTraces[i][data] = str2.equals("true") ? 1.0 : 0.0; + i++; + } else { + try { + if (str2.indexOf('.') >= 0) { + m_types[i] = type.REAL; + } + m_fTraces[i][data] = Double.parseDouble(str2); + } catch (Exception e) { + + if (m_ranges[i] == null) { + m_ranges[i] = new ArrayList<>(); + } + if (!m_ranges[i].contains(str2)) { + m_ranges[i].add(str2); + } + m_fTraces[i][data] = 1.0 * m_ranges[i].indexOf(str2); } - m_fTraces[i][data] = Double.parseDouble(str2); - } catch (Exception e) { - if (m_ranges[i] == null) { - m_ranges[i] = new ArrayList<>(); - } - if (!m_ranges[i].contains(str2)) { - m_ranges[i].add(str2); - } - m_fTraces[i][data] = 1.0 * m_ranges[i].indexOf(str2); + i++; } - i++; } while (reported < 81 && 1000.0 * reported < 81000.0 * (data + 1)/ total) { log("*"); @@ -274,12 +282,9 @@ protected void readLogFile(String fileName, int burnInPercentage) throws IOExcep logln(""); // determine types for (int i = 0; i < items; i++) - if (m_ranges[i] != null) - if (m_ranges[i].size() == 2 && m_ranges[i].contains("true") && m_ranges[i].contains("false") || - m_ranges[i].size() == 1 && (m_ranges[i].contains("true") || m_ranges[i].contains("false"))) - m_types[i] = type.BOOL; - else - m_types[i] = type.NOMINAL; + if (m_ranges[i] != null && (! m_types[i].equals(type.BOOL)) ) { + m_types[i] = type.NOMINAL; + } fin.close(); } // readLogFile @@ -323,8 +328,8 @@ public void calcStats(String [] tags) { m_fStdDev[i] = Double.NaN; } - if (m_types[i] == type.REAL || m_types[i] == type.INTEGER) { - // calc median, and 95% HPD interval + if (m_types[i] == type.REAL || m_types[i] == type.INTEGER || m_types[i] == type.BOOL) { + // calc median, and 95% HPD interval (BOOL traces are true = 1, false = 0) Double[] sorted = trace.clone(); Arrays.sort(sorted); m_fMedian[i] = sorted[trace.length / 2]; diff --git a/beast-fx/src/test/java/test/beastfx/app/tools/LogAnalyserTest.java b/beast-fx/src/test/java/test/beastfx/app/tools/LogAnalyserTest.java new file mode 100644 index 00000000..361f91f2 --- /dev/null +++ b/beast-fx/src/test/java/test/beastfx/app/tools/LogAnalyserTest.java @@ -0,0 +1,96 @@ +package test.beastfx.app.tools; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +import org.junit.jupiter.api.Test; + +import beastfx.app.tools.LogAnalyser; + +import static org.junit.jupiter.api.Assertions.*; + +public class LogAnalyserTest { + + /** writes a trace log with the given rows under the header Sample, x, y, z */ + private static File log(String... rows) throws IOException { + File file = File.createTempFile("LogAnalyserTest", ".log"); + file.deleteOnExit(); + StringBuilder b = new StringBuilder("Sample\tx\ty\tz\n"); + for (String row : rows) { + b.append(row).append('\n'); + } + Files.writeString(file.toPath(), b.toString()); + return file; + } + + /** + * Boolean columns must read as true = 1 and false = 0, whichever value appears first. + */ + @Test + void testBooleansAreOneAndZero() throws IOException { + LogAnalyser analyser = new LogAnalyser(log( + "0\ttrue\tfalse\tA", + "1\tfalse\ttrue\tB", + "2\tfalse\ttrue\tA", + "3\tfalse\ttrue\tA").getPath(), 0, true, true); + + // starts with true + assertArrayEquals(new Double[] {1.0, 0.0, 0.0, 0.0}, analyser.getTrace("x")); + assertEquals(0.25, analyser.getMean("x"), 1e-12); + // starts with false + assertArrayEquals(new Double[] {0.0, 1.0, 1.0, 1.0}, analyser.getTrace("y")); + assertEquals(0.75, analyser.getMean("y"), 1e-12); + // other nominal values are still numbered in order of first appearance + assertArrayEquals(new Double[] {0.0, 1.0, 0.0, 0.0}, analyser.getTrace("z")); + } + + /** the same holds when burn-in removes the first rows */ + @Test + void testBooleansAfterBurnIn() throws IOException { + LogAnalyser analyser = new LogAnalyser(log( + "0\tfalse\tfalse\tA", + "1\ttrue\tfalse\tA", + "2\tfalse\ttrue\tA", + "3\ttrue\ttrue\tA", + "4\ttrue\ttrue\tA", + "5\tfalse\ttrue\tA", + "6\ttrue\ttrue\tA", + "7\ttrue\ttrue\tA", + "8\ttrue\ttrue\tA", + "9\ttrue\ttrue\tA").getPath(), 10, true, true); + + // burn-in 10% drops row 0; x now starts with true + assertArrayEquals(new Double[] {1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0}, analyser.getTrace("x")); + // y: one false, then eight true + assertEquals(8.0 / 9.0, analyser.getMean("y"), 1e-12); + } + + /** + * A boolean column gets every statistic, the same as the 1/0 column it stands for: + * x is true/false, y the same values as 1/0. + */ + @Test + void testBooleanStatsMatchOneZero() throws IOException { + String[] rows = new String[200]; + java.util.Random random = new java.util.Random(1); + boolean on = true; + for (int k = 0; k < rows.length; k++) { + if (random.nextDouble() < 0.2) { + on = !on; + } + rows[k] = (k * 1000) + "\t" + on + "\t" + (on ? 1 : 0) + "\t" + random.nextDouble(); + } + LogAnalyser analyser = new LogAnalyser(log(rows).getPath(), 10, true, true); + + assertEquals(analyser.getMean("y"), analyser.getMean("x"), 1e-12); + assertEquals(analyser.getStdError("y"), analyser.getStdError("x"), 1e-12); + assertEquals(analyser.getStdDev("y"), analyser.getStdDev("x"), 1e-12); + assertEquals(analyser.getMedian("y"), analyser.getMedian("x"), 1e-12); + assertEquals(analyser.get95HPDlow("y"), analyser.get95HPDlow("x"), 1e-12); + assertEquals(analyser.get95HPDup("y"), analyser.get95HPDup("x"), 1e-12); + assertEquals(analyser.getACT("y"), analyser.getACT("x"), 1e-12); + assertEquals(analyser.getESS("y"), analyser.getESS("x"), 1e-12); + assertFalse(Double.isNaN(analyser.getESS("x"))); + } +}