From 7c837bca686310c143244d2c67220b85edefa175 Mon Sep 17 00:00:00 2001 From: jlizier Date: Tue, 22 Aug 2017 21:28:57 +1000 Subject: [PATCH] Adding Conditional MI AutoAnalyser java implementation and shell scripts to run it. Includes some minor changes to AutoAnalyser to implement it (allowing selective disabling of column selection fields under the all combinations option, and variable indenting for all combos option), and to other child classes of it to follow suit. Also adds detection of column number inputs being out of bounds or mangled. --- demos/AutoAnalyser/runCMIAutoAnalyser.bat | 8 + demos/AutoAnalyser/runCMIAutoAnalyser.sh | 8 + .../demos/autoanalysis/AutoAnalyser.java | 50 ++- .../demos/autoanalysis/AutoAnalyserCMI.java | 366 ++++++++++++++++++ .../AutoAnalyserChannelCalculator.java | 3 + .../autoanalysis/AutoAnalyserEntropy.java | 4 +- 6 files changed, 421 insertions(+), 18 deletions(-) create mode 100755 demos/AutoAnalyser/runCMIAutoAnalyser.bat create mode 100755 demos/AutoAnalyser/runCMIAutoAnalyser.sh create mode 100644 demos/java/infodynamics/demos/autoanalysis/AutoAnalyserCMI.java diff --git a/demos/AutoAnalyser/runCMIAutoAnalyser.bat b/demos/AutoAnalyser/runCMIAutoAnalyser.bat new file mode 100755 index 0000000..07dbda8 --- /dev/null +++ b/demos/AutoAnalyser/runCMIAutoAnalyser.bat @@ -0,0 +1,8 @@ +@ECHO OFF + +REM Make sure the latest example source file is compiled. +javac -classpath "..\java;..\..\infodynamics.jar" "..\java\infodynamics\demos\autoanalysis\AutoAnalyserCMI.java" + +REM Run the example: +java -classpath "..\java;..\..\infodynamics.jar" infodynamics.demos.autoanalysis.AutoAnalyserCMI + diff --git a/demos/AutoAnalyser/runCMIAutoAnalyser.sh b/demos/AutoAnalyser/runCMIAutoAnalyser.sh new file mode 100755 index 0000000..5c508a4 --- /dev/null +++ b/demos/AutoAnalyser/runCMIAutoAnalyser.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# Make sure the latest example source file is compiled. +javac -classpath "../java:../../infodynamics.jar" "../java/infodynamics/demos/autoanalysis/AutoAnalyserCMI.java" + +# Run the example: +java -classpath "../java:../../infodynamics.jar" infodynamics.demos.autoanalysis.AutoAnalyserCMI + diff --git a/demos/java/infodynamics/demos/autoanalysis/AutoAnalyser.java b/demos/java/infodynamics/demos/autoanalysis/AutoAnalyser.java index 44ea6d5..8157e16 100644 --- a/demos/java/infodynamics/demos/autoanalysis/AutoAnalyser.java +++ b/demos/java/infodynamics/demos/autoanalysis/AutoAnalyser.java @@ -121,6 +121,9 @@ public abstract class AutoAnalyser extends JFrame // Should be overridden by the child classes. protected String[] variableColNumLabels = null; + protected boolean[] disableVariableColTextFieldsForAllCombos = null; + protected int indentsForAllCombos = 1; + // Store which calculator type we're using: @SuppressWarnings("rawtypes") protected Class calcClass = null; @@ -688,22 +691,30 @@ public abstract class AutoAnalyser extends JFrame int[] singleCalcColumns = new int[numVariables]; Vector variableCombinations = new Vector(); - if (allCombosCheckBox.isSelected()) { - // We're doing all combinations - fillOutAllCombinations(variableCombinations); - } else { - // we're doing a single combination - for (int i = 0; i < numVariables; i++) { - singleCalcColumns[i] = Integer.parseInt(variableColTextFields[i].getText()); - if ((singleCalcColumns[i] < 0) || (singleCalcColumns[i] >= dataColumns)) { - JOptionPane.showMessageDialog(this, - String.format("%s column must be between 0 and %d for this data set", - variableColNumLabels[i], dataColumns-1)); - resultsLabel.setText(" "); - return; + try { + if (allCombosCheckBox.isSelected()) { + // We're doing all combinations + fillOutAllCombinations(variableCombinations); + } else { + // we're doing a single combination + for (int i = 0; i < numVariables; i++) { + singleCalcColumns[i] = Integer.parseInt(variableColTextFields[i].getText()); + if ((singleCalcColumns[i] < 0) || (singleCalcColumns[i] >= dataColumns)) { + JOptionPane.showMessageDialog(this, + String.format("%s column must be between 0 and %d for this data set", + variableColNumLabels[i], dataColumns-1)); + resultsLabel.setText(" "); + return; + } } + variableCombinations.add(singleCalcColumns); } - variableCombinations.add(singleCalcColumns); + } catch (Exception e) { + // Catches number format exception, and column number out of bounds + JOptionPane.showMessageDialog(this, + e.getMessage()); + resultsLabel.setText("Cannot parse a column number from input: " + e.getMessage()); + return; } // Generate headers: @@ -1013,10 +1024,13 @@ public abstract class AutoAnalyser extends JFrame // Get the indents right from here on, // and prepare a string of the column labels for // each variable, to be used in later formatting. - for (int i = 0; i < numVariables; i++) { + for (int i = 0; i < indentsForAllCombos; i++) { javaPrefix += " "; pythonPrefix += "\t"; matlabPrefix += "\t"; + } + + for (int i = 0; i < numVariables; i++) { extraFormatTerms.append(columnVariables[i] + ", "); } @@ -1324,7 +1338,7 @@ public abstract class AutoAnalyser extends JFrame * * @param variableCombinations */ - protected abstract void fillOutAllCombinations(Vector variableCombinations); + protected abstract void fillOutAllCombinations(Vector variableCombinations) throws Exception; /** * Method to allow child classes to set up the loops over all combinations of @@ -1748,7 +1762,9 @@ public abstract class AutoAnalyser extends JFrame // "All pairs" checkbox -- update in case changed: if (allCombosCheckBox.isSelected()) { for (int i = 0; i < numVariables; i++) { - variableColTextFields[i].setEnabled(false); + if (disableVariableColTextFieldsForAllCombos[i]) { + variableColTextFields[i].setEnabled(false); + } } } else { for (int i = 0; i < numVariables; i++) { diff --git a/demos/java/infodynamics/demos/autoanalysis/AutoAnalyserCMI.java b/demos/java/infodynamics/demos/autoanalysis/AutoAnalyserCMI.java new file mode 100644 index 0000000..526c9ff --- /dev/null +++ b/demos/java/infodynamics/demos/autoanalysis/AutoAnalyserCMI.java @@ -0,0 +1,366 @@ +/* + * Java Information Dynamics Toolkit (JIDT) + * Copyright (C) 2015, Joseph T. Lizier + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package infodynamics.demos.autoanalysis; + +import infodynamics.measures.continuous.ConditionalMutualInfoCalculatorMultiVariate; +import infodynamics.measures.continuous.ConditionalMutualInfoMultiVariateCommon; +import infodynamics.measures.continuous.InfoMeasureCalculatorContinuous; +import infodynamics.measures.continuous.gaussian.ConditionalMutualInfoCalculatorMultiVariateGaussian; +import infodynamics.measures.continuous.kraskov.ConditionalMutualInfoCalculatorMultiVariateKraskov; +import infodynamics.measures.continuous.kraskov.ConditionalMutualInfoCalculatorMultiVariateKraskov1; +import infodynamics.measures.continuous.kraskov.ConditionalMutualInfoCalculatorMultiVariateKraskov2; +import infodynamics.measures.discrete.ConditionalMutualInformationCalculatorDiscrete; +import infodynamics.measures.discrete.InfoMeasureCalculatorDiscrete; +import infodynamics.utils.MatrixUtils; + +import javax.swing.JOptionPane; +import javax.swing.event.DocumentListener; + +import java.awt.event.ActionListener; +import java.awt.event.MouseListener; +import java.util.Vector; + +/** + * This class provides a GUI to build a simple conditional mutual information calculation, + * and supply the code to execute it. + * + * + * @author Joseph Lizier + * + */ +public class AutoAnalyserCMI extends AutoAnalyser + implements ActionListener, DocumentListener, MouseListener { + + /** + * Need serialVersionUID to be serializable + */ + private static final long serialVersionUID = 1L; + + // Property names for specific continuous calculators: + protected String[] gaussianProperties; + protected String[] gaussianPropertiesFieldNames; + protected String[] gaussianPropertyDescriptions; + protected String[] kraskovProperties; + protected String[] kraskovPropertiesFieldNames; + protected String[] kraskovPropertyDescriptions; + + protected static final String DISCRETE_PROPNAME_TIME_DIFF = "time difference"; + + protected static final String CALC_TYPE_KRASKOV_ALG1 = CALC_TYPE_KRASKOV + " alg. 1"; + protected static final String CALC_TYPE_KRASKOV_ALG2 = CALC_TYPE_KRASKOV + " alg. 2"; + + /** + * Constructor to initialise the GUI for MI + */ + protected void makeSpecificInitialisations() { + + numVariables = 3; + variableColNumLabels = new String[] {"Source", "Destination", "Conditional"}; + useAllCombosCheckBox = true; + useStatSigCheckBox = true; + wordForCombinations = "pairs"; + variableRelationshipFormatString = "col_%d -> col_%d | col_%d"; + disableVariableColTextFieldsForAllCombos = new boolean[] {true, true, false}; + indentsForAllCombos = 2; + + // Set up the properties for CMI: + measureAcronym = "CMI"; + appletTitle = "JIDT Conditional MI Auto-Analyser"; + + calcTypes = new String[] { + CALC_TYPE_DISCRETE, CALC_TYPE_BINNED, CALC_TYPE_GAUSSIAN, + CALC_TYPE_KRASKOV_ALG1, CALC_TYPE_KRASKOV_ALG2}; + // No kernel calculator defined for CMI (yet, and unlikely to happen) + unitsForEachCalc = new String[] {"bits", "bits", "nats", "nats", "nats"}; + + // Discrete: + discreteClass = ConditionalMutualInformationCalculatorDiscrete.class; + discreteProperties = new String[] { + DISCRETE_PROPNAME_BASE + }; + discretePropertyDefaultValues = new String[] { + "2" + }; + discretePropertyDescriptions = new String[] { + "Number of discrete states available for each variable (i.e. 2 for binary).
" + + "Can be set individually for each variable -- see code." + }; + + // Continuous: + abstractContinuousClass = ConditionalMutualInfoCalculatorMultiVariate.class; + // Common properties for all continuous calcs: + commonContPropertyNames = new String[] { + // None + }; + commonContPropertiesFieldNames = new String[] { + // None + }; + commonContPropertyDescriptions = new String[] { + // None + }; + // Gaussian properties: + gaussianProperties = new String[] { + }; + gaussianPropertiesFieldNames = new String[] { + }; + gaussianPropertyDescriptions = new String[] { + }; + // KSG (Kraskov): + kraskovProperties = new String[] { + ConditionalMutualInfoMultiVariateCommon.PROP_NORMALISE, + ConditionalMutualInfoCalculatorMultiVariateKraskov.PROP_K, + ConditionalMutualInfoCalculatorMultiVariateKraskov.PROP_ADD_NOISE, + ConditionalMutualInfoCalculatorMultiVariateKraskov.PROP_DYN_CORR_EXCL_TIME, + ConditionalMutualInfoCalculatorMultiVariateKraskov.PROP_NORM_TYPE, + ConditionalMutualInfoCalculatorMultiVariateKraskov.PROP_NUM_THREADS, + }; + kraskovPropertiesFieldNames = new String[] { + "ConditionalMutualInfoMultiVariateCommon.PROP_NORMALISE", + "ConditionalMutualInfoCalculatorMultiVariateKraskov.PROP_K", + "ConditionalMutualInfoCalculatorMultiVariateKraskov.PROP_ADD_NOISE", + "ConditionalMutualInfoCalculatorMultiVariateKraskov.PROP_DYN_CORR_EXCL_TIME", + "ConditionalMutualInfoCalculatorMultiVariateKraskov.PROP_NORM_TYPE", + "ConditionalMutualInfoCalculatorMultiVariateKraskov.PROP_NUM_THREADS", + }; + kraskovPropertyDescriptions = new String[] { + "(boolean) whether to normalise
each incoming time-series to mean 0, standard deviation 1, or not (recommended)", + "Number of k nearest neighbours to use
in the full joint kernel space in the KSG algorithm", + "Standard deviation for an amount
of random Gaussian noise to add to each variable, " + + "to avoid having neighbourhoods with artificially large counts.
" + + "(\"false\" may be used to indicate \"0\".). The amount is added in after any normalisation.", + "Dynamic correlation exclusion time or
Theiler window (see Kantz and Schreiber); " + + "0 (default) means no dynamic exclusion window", + "
Norm type to use in KSG algorithm between the points in each marginal space.
Options are: " + + "\"MAX_NORM\" (default), otherwise \"EUCLIDEAN\" or \"EUCLIDEAN_SQUARED\" (both equivalent here)", + "Number of parallel threads to use
in computation: an integer > 0 or \"USE_ALL\" " + + "(default, to indicate to use all available processors)", + }; + + } + + @Override + protected void fillOutAllCombinations(Vector variableCombinations) throws Exception { + // All combinations here means all pairs of sources and destinations, + // with the conditional fixed. + int conditional = Integer.parseInt(variableColTextFields[2].getText()); + if (conditional >= dataColumns) { + throw new Exception(String.format("%s column must be between 0 and %d for this data set", + variableColNumLabels[2], dataColumns-1)); + } + for (int s = 0; s < dataColumns; s++) { + for (int d = 0; d < dataColumns; d++) { + variableCombinations.add(new int[] {s, d, conditional}); + } + } + } + + @Override + protected String[] setUpLoopsForAllCombos(StringBuffer javaCode, + StringBuffer pythonCode, StringBuffer matlabCode) { + // Set up loops in the code: + int conditional = Integer.parseInt(variableColTextFields[2].getText()); + // 1. Java code + javaCode.append(" \n"); + javaCode.append(" int c = " + conditional + ";\n"); + javaCode.append(" // Compute for all source-destination pairs:\n"); + javaCode.append(" for (int s = 0; s < " + dataColumns + + "; s++) {\n"); + javaCode.append(" for (int d = 0; d < " + dataColumns + + "; d++) {\n"); + String javaPrefix = " "; + javaCode.append(javaPrefix + "// For each source-dest pair (given conditional):\n"); + javaCode.append(javaPrefix + "if ((s == d) || (s == c) || (d == c)) {\n"); + javaCode.append(javaPrefix + " continue;\n"); + javaCode.append(javaPrefix + "}\n"); + // 2. Python code + pythonCode.append("\n"); + pythonCode.append("c = " + conditional + "\n"); + pythonCode.append("# Compute for all pairs:\n"); + pythonCode.append("for s in range(" + dataColumns + "):\n"); + pythonCode.append("\tfor d in range(" + dataColumns + "):\n"); + String pythonPrefix = "\t\t"; + pythonCode.append(pythonPrefix+ "# For each source-dest pair (given conditional):\n"); + pythonCode.append(pythonPrefix + "if ((s == d) or (s == c) or (d == c)):\n"); + pythonCode.append(pythonPrefix + "\tcontinue\n"); + // 3. Matlab code + matlabCode.append("\n"); + matlabCode.append("c = " + (conditional+1) + ";\n"); + matlabCode.append("% Compute for all pairs:\n"); + matlabCode.append("for s = 1:" + dataColumns + "\n"); + matlabCode.append("\tfor d = 1:" + dataColumns + "\n"); + String matlabPrefix = "\t\t"; + matlabCode.append(matlabPrefix + "% For each source-dest pair (given conditional):\n"); + matlabCode.append(matlabPrefix + "if ((s == d) || (s == c) || (d == c))\n"); + matlabCode.append(matlabPrefix + "\tcontinue;\n"); + matlabCode.append(matlabPrefix + "end\n"); + + // Return the variables to index each column: + return new String[] {"s", "d", "c"}; + } + + @Override + protected void finaliseLoopsForAllCombos(StringBuffer javaCode, + StringBuffer pythonCode, StringBuffer matlabCode) { + + // 1. Java code + javaCode.append(" }\n"); + javaCode.append(" }\n"); + // 2. Python code + // Nothing to do + // 3. Matlab code + matlabCode.append("\tend\n"); + matlabCode.append("end\n"); + } + + @Override + protected String formatStringWithColumnNumbers(String formatStr, int[] columnNumbers) { + // We format the source and target variable numbers into the + // return string here: + return String.format(formatStr, + columnNumbers[0], columnNumbers[1], columnNumbers[2]); + } + + @Override + protected boolean skipColumnCombo(int[] columnCombo) { + if ((columnCombo[0] == columnCombo[1]) || + (columnCombo[0] == columnCombo[2]) || + (columnCombo[1] == columnCombo[2])) { + // Two columns are the same here, + // so don't compute the conditional MI + return true; + } + return false; + } + + @Override + protected void setObservations(InfoMeasureCalculatorDiscrete calcDiscrete, + InfoMeasureCalculatorContinuous calcContinuous, + int[] columnCombo) throws Exception { + + String selectedCalcType = (String) + calcTypeComboBox.getSelectedItem(); + + int sourceColumn = columnCombo[0]; + int destColumn = columnCombo[1]; + int condColumn = columnCombo[2]; + + // Set observations + if (selectedCalcType.equalsIgnoreCase(CALC_TYPE_DISCRETE)) { + ConditionalMutualInformationCalculatorDiscrete cmiCalc = + (ConditionalMutualInformationCalculatorDiscrete) calcDiscrete; + cmiCalc.addObservations( + MatrixUtils.selectColumn(dataDiscrete, sourceColumn), + MatrixUtils.selectColumn(dataDiscrete, destColumn), + MatrixUtils.selectColumn(dataDiscrete, condColumn)); + } else if (selectedCalcType.equalsIgnoreCase(CALC_TYPE_BINNED)) { + ConditionalMutualInformationCalculatorDiscrete cmiCalc = + (ConditionalMutualInformationCalculatorDiscrete) calcDiscrete; + cmiCalc.addObservations( + MatrixUtils.discretise( + MatrixUtils.selectColumn(data, sourceColumn), + Integer.parseInt(propertyValues.get(DISCRETE_PROPNAME_BASE))), + MatrixUtils.discretise( + MatrixUtils.selectColumn(data, destColumn), + Integer.parseInt(propertyValues.get(DISCRETE_PROPNAME_BASE))), + MatrixUtils.discretise( + MatrixUtils.selectColumn(data, condColumn), + Integer.parseInt(propertyValues.get(DISCRETE_PROPNAME_BASE)))); + } else { + ConditionalMutualInfoCalculatorMultiVariate cmiCalc = + (ConditionalMutualInfoCalculatorMultiVariate) calcContinuous; + cmiCalc.setObservations( + MatrixUtils.selectColumn(data, sourceColumn), + MatrixUtils.selectColumn(data, destColumn), + MatrixUtils.selectColumn(data, condColumn)); + } + } + + protected CalcProperties assignCalcProperties(String selectedCalcType) + throws Exception { + // Let the super class handle discrete calculators + CalcProperties calcProperties = super.assignCalcProperties(selectedCalcType); + if (calcProperties == null) { + // We need to assign properties for a continuous calculator + calcProperties = new CalcProperties(); + calcProperties.calc = assignCalcObjectContinuous(selectedCalcType); + calcProperties.calcClass = calcProperties.calc.getClass(); + if (selectedCalcType.equalsIgnoreCase(CALC_TYPE_GAUSSIAN)) { + calcProperties.classSpecificPropertyNames = gaussianProperties; + calcProperties.classSpecificPropertiesFieldNames = gaussianPropertiesFieldNames; + calcProperties.classSpecificPropertyDescriptions = gaussianPropertyDescriptions; + } else if (selectedCalcType.startsWith(CALC_TYPE_KRASKOV)) { + // The if statement will work for both MI Kraskov calculators + calcProperties.classSpecificPropertyNames = kraskovProperties; + calcProperties.classSpecificPropertiesFieldNames = kraskovPropertiesFieldNames; + calcProperties.classSpecificPropertyDescriptions = kraskovPropertyDescriptions; + } else { + calcProperties = null; + throw new Exception("No recognised calculator selected: " + + selectedCalcType); + } + } + return calcProperties; + } + + /** + * Method to assign and initialise our continuous calculator class + */ + @Override + protected ConditionalMutualInfoCalculatorMultiVariate assignCalcObjectContinuous(String selectedCalcType) throws Exception { + if (selectedCalcType.equalsIgnoreCase(CALC_TYPE_GAUSSIAN)) { + return new ConditionalMutualInfoCalculatorMultiVariateGaussian(); + } else if (selectedCalcType.equalsIgnoreCase(CALC_TYPE_KRASKOV_ALG1)) { + return new ConditionalMutualInfoCalculatorMultiVariateKraskov1(); + } else if (selectedCalcType.equalsIgnoreCase(CALC_TYPE_KRASKOV_ALG2)) { + return new ConditionalMutualInfoCalculatorMultiVariateKraskov2(); + } else { + throw new Exception("No recognised continuous calculator selected: " + + selectedCalcType); + } + + } + + /** + * Method to assign and initialise our discrete calculator class + */ + protected DiscreteCalcAndArguments assignCalcObjectDiscrete() throws Exception { + int base; + try { + String basePropValueStr = propertyValues.get(DISCRETE_PROPNAME_BASE); + base = Integer.parseInt(basePropValueStr); + } catch (Exception ex) { + JOptionPane.showMessageDialog(this, + ex.getMessage()); + resultsLabel.setText("Cannot find a value for property " + DISCRETE_PROPNAME_BASE); + return null; + } + + return new DiscreteCalcAndArguments( + new ConditionalMutualInformationCalculatorDiscrete(base, base, base), + base, + base + ", " + base + ", " + base); + } + + /** + * @param args + */ + public static void main(String[] args) { + new AutoAnalyserCMI(); + } +} diff --git a/demos/java/infodynamics/demos/autoanalysis/AutoAnalyserChannelCalculator.java b/demos/java/infodynamics/demos/autoanalysis/AutoAnalyserChannelCalculator.java index 93b8438..d365b90 100644 --- a/demos/java/infodynamics/demos/autoanalysis/AutoAnalyserChannelCalculator.java +++ b/demos/java/infodynamics/demos/autoanalysis/AutoAnalyserChannelCalculator.java @@ -64,6 +64,9 @@ public abstract class AutoAnalyserChannelCalculator extends AutoAnalyser { useStatSigCheckBox = true; wordForCombinations = "pairs"; variableRelationshipFormatString = "col_%d -> col_%d"; + + disableVariableColTextFieldsForAllCombos = new boolean[] {true, true}; + indentsForAllCombos = 2; } @Override diff --git a/demos/java/infodynamics/demos/autoanalysis/AutoAnalyserEntropy.java b/demos/java/infodynamics/demos/autoanalysis/AutoAnalyserEntropy.java index b9ceb52..3131cae 100644 --- a/demos/java/infodynamics/demos/autoanalysis/AutoAnalyserEntropy.java +++ b/demos/java/infodynamics/demos/autoanalysis/AutoAnalyserEntropy.java @@ -69,7 +69,9 @@ public class AutoAnalyserEntropy extends AutoAnalyser { useStatSigCheckBox = false; wordForCombinations = "variables"; variableRelationshipFormatString = "col_%d"; - + disableVariableColTextFieldsForAllCombos = new boolean[] {true}; + indentsForAllCombos = 1; + // Set up the properties for Entropy: measureAcronym = "H"; appletTitle = "JIDT Entropy Auto-Analyser";