From 9d403f1191ae1d2211f0da4d0e1f5f5012a7287f Mon Sep 17 00:00:00 2001 From: EvaLiyt Date: Thu, 1 Oct 2026 11:54:21 +1300 Subject: [PATCH 1/5] ensure true=1 false=0 when convert a boolean column --- .../java/beastfx/app/tools/LogAnalyser.java | 11 ++- .../beastfx/app/tools/LogAnalyserTest.java | 69 +++++++++++++++++++ 2 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 beast-fx/src/test/java/test/beastfx/app/tools/LogAnalyserTest.java 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..30b658ba 100644 --- a/beast-fx/src/main/java/beastfx/app/tools/LogAnalyser.java +++ b/beast-fx/src/main/java/beastfx/app/tools/LogAnalyser.java @@ -276,9 +276,16 @@ protected void readLogFile(String fileName, int burnInPercentage) throws IOExcep 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_ranges[i].size() == 1 && (m_ranges[i].contains("true") || m_ranges[i].contains("false"))) { m_types[i] = type.BOOL; - else + // values were numbered in order of first appearance + // use true = 1 false = 0 + for (int j = 0; j < m_fTraces[i].length; j++) { + if (m_fTraces[i][j] != null) { + m_fTraces[i][j] = m_ranges[i].get(m_fTraces[i][j].intValue()).equals("true") ? 1.0 : 0.0; + } + } + } else m_types[i] = type.NOMINAL; fin.close(); 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..fa244dd5 --- /dev/null +++ b/beast-fx/src/test/java/test/beastfx/app/tools/LogAnalyserTest.java @@ -0,0 +1,69 @@ +package test.beastfx.app.tools; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +import org.junit.jupiter.api.Test; + +import beastfx.app.tools.LogAnalyser; + +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); + } +} From 4430526239fc85ca56ba33f79687915aa85e0853 Mon Sep 17 00:00:00 2001 From: EvaLiyt Date: Mon, 5 Oct 2026 12:12:57 +1300 Subject: [PATCH 2/5] set booleans also compute median hpd ess etc as integer for not throwing NaN --- .../java/beastfx/app/tools/LogAnalyser.java | 4 +-- .../beastfx/app/tools/LogAnalyserTest.java | 28 +++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) 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 30b658ba..c1e5e47c 100644 --- a/beast-fx/src/main/java/beastfx/app/tools/LogAnalyser.java +++ b/beast-fx/src/main/java/beastfx/app/tools/LogAnalyser.java @@ -330,8 +330,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 index fa244dd5..e2dd83da 100644 --- a/beast-fx/src/test/java/test/beastfx/app/tools/LogAnalyserTest.java +++ b/beast-fx/src/test/java/test/beastfx/app/tools/LogAnalyserTest.java @@ -66,4 +66,32 @@ void testBooleansAfterBurnIn() throws IOException { // 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); + assertEquals(false, Double.isNaN(analyser.getESS("x"))); + } } From fc4bdaf0849bb5a89d887c3fa6b28b82f4dfffd6 Mon Sep 17 00:00:00 2001 From: EvaLiyt Date: Mon, 5 Oct 2026 16:08:24 +1300 Subject: [PATCH 3/5] clean up the code to do the smallest changes --- .../java/beastfx/app/tools/LogAnalyser.java | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) 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 c1e5e47c..7a16251a 100644 --- a/beast-fx/src/main/java/beastfx/app/tools/LogAnalyser.java +++ b/beast-fx/src/main/java/beastfx/app/tools/LogAnalyser.java @@ -243,6 +243,8 @@ protected void readLogFile(String fileName, int burnInPercentage) throws IOExcep // grab data from the log, ignoring burn in samples m_types = new type[items]; Arrays.fill(m_types, type.INTEGER); + // columns holding at least one true or false + boolean[] hasBool = new boolean[items]; int reported = 0; while (fin.ready()) { str = fin.readLine(); @@ -250,6 +252,13 @@ 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")) { + if (str2.equals("true") || str2.equals("false")) { + // set true = 1 false = 0 + m_fTraces[i][data] = str2.equals("true") ? 1.0 : 0.0; + hasBool[i] = true; + i++; + continue; + } try { if (str2.indexOf('.') >= 0) { m_types[i] = type.REAL; @@ -275,18 +284,10 @@ protected void readLogFile(String fileName, int burnInPercentage) throws IOExcep // 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; - // values were numbered in order of first appearance - // use true = 1 false = 0 - for (int j = 0; j < m_fTraces[i].length; j++) { - if (m_fTraces[i][j] != null) { - m_fTraces[i][j] = m_ranges[i].get(m_fTraces[i][j].intValue()).equals("true") ? 1.0 : 0.0; - } - } - } else - m_types[i] = type.NOMINAL; + m_types[i] = type.NOMINAL; + else if (hasBool[i]) { + m_types[i] = type.BOOL; + } fin.close(); } // readLogFile From 22dbb1eb2380bec164449e8058ee6953d29ad709 Mon Sep 17 00:00:00 2001 From: EvaLiyt Date: Mon, 5 Oct 2026 17:03:08 +1300 Subject: [PATCH 4/5] clean up the code and ignore case issue for true and false --- .../java/beastfx/app/tools/LogAnalyser.java | 38 +++++++++---------- .../beastfx/app/tools/LogAnalyserTest.java | 7 ++-- 2 files changed, 22 insertions(+), 23 deletions(-) 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 7a16251a..4a92b946 100644 --- a/beast-fx/src/main/java/beastfx/app/tools/LogAnalyser.java +++ b/beast-fx/src/main/java/beastfx/app/tools/LogAnalyser.java @@ -252,28 +252,30 @@ 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")) { - if (str2.equals("true") || str2.equals("false")) { + 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; hasBool[i] = true; i++; - continue; - } - 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<>(); + } 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); } - 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("*"); @@ -283,10 +285,8 @@ 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] != null && (! m_types[i].equals(type.BOOL)) ) { m_types[i] = type.NOMINAL; - else if (hasBool[i]) { - m_types[i] = type.BOOL; } fin.close(); 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 index e2dd83da..361f91f2 100644 --- a/beast-fx/src/test/java/test/beastfx/app/tools/LogAnalyserTest.java +++ b/beast-fx/src/test/java/test/beastfx/app/tools/LogAnalyserTest.java @@ -1,8 +1,5 @@ package test.beastfx.app.tools; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; - import java.io.File; import java.io.IOException; import java.nio.file.Files; @@ -11,6 +8,8 @@ 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 */ @@ -92,6 +91,6 @@ void testBooleanStatsMatchOneZero() throws IOException { 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); - assertEquals(false, Double.isNaN(analyser.getESS("x"))); + assertFalse(Double.isNaN(analyser.getESS("x"))); } } From 9e5b00c1ce3dc56b911ca7b274191b2e37b8c7e8 Mon Sep 17 00:00:00 2001 From: EvaLiyt Date: Wed, 7 Oct 2026 10:40:42 +1300 Subject: [PATCH 5/5] remove unnecessary check --- beast-fx/src/main/java/beastfx/app/tools/LogAnalyser.java | 3 --- 1 file changed, 3 deletions(-) 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 4a92b946..2217a830 100644 --- a/beast-fx/src/main/java/beastfx/app/tools/LogAnalyser.java +++ b/beast-fx/src/main/java/beastfx/app/tools/LogAnalyser.java @@ -243,8 +243,6 @@ protected void readLogFile(String fileName, int burnInPercentage) throws IOExcep // grab data from the log, ignoring burn in samples m_types = new type[items]; Arrays.fill(m_types, type.INTEGER); - // columns holding at least one true or false - boolean[] hasBool = new boolean[items]; int reported = 0; while (fin.ready()) { str = fin.readLine(); @@ -256,7 +254,6 @@ protected void readLogFile(String fileName, int burnInPercentage) throws IOExcep // set true = 1 false = 0 m_types[i] = type.BOOL; m_fTraces[i][data] = str2.equals("true") ? 1.0 : 0.0; - hasBool[i] = true; i++; } else { try {