From 2443d39a6fa49fd2953d330dbc58625f8d5248e6 Mon Sep 17 00:00:00 2001 From: walterxie Date: Thu, 30 Nov 2023 10:01:38 +1300 Subject: [PATCH] SLPhy can take macro now #420 --- examples/macro/MacroLanguage.lphy | 2 + .../test/java/lphy/base/LPhyExamplesTest.java | 2 +- .../GraphicalModelPanel.java | 5 +- .../main/java/lphy/core/io/FileConfig.java | 6 +- .../java/lphy/core/io/MacroProcessor.java | 81 ++++++++++++++----- .../core/parser/LPhyParserDictionary.java | 16 +++- .../simulator/NamedRandomValueSimulator.java | 14 +--- .../main/java/lphy/core/simulator/SLPhy.java | 10 ++- .../java/lphy/core/simulator/Sampler.java | 5 +- .../java/lphy/core/simulator/Simulate.java | 5 +- 10 files changed, 98 insertions(+), 48 deletions(-) diff --git a/examples/macro/MacroLanguage.lphy b/examples/macro/MacroLanguage.lphy index 4c3b697f..fc87e6cf 100644 --- a/examples/macro/MacroLanguage.lphy +++ b/examples/macro/MacroLanguage.lphy @@ -1,3 +1,5 @@ +// It only shows the default when using studio, +// but macro value can be replaced by SLPhy (command line application). data { L = {{L = 100}}; taxa = taxa(names=1:{{n = 10}}); diff --git a/lphy-base/src/test/java/lphy/base/LPhyExamplesTest.java b/lphy-base/src/test/java/lphy/base/LPhyExamplesTest.java index 101c30e1..c9b2f7ae 100644 --- a/lphy-base/src/test/java/lphy/base/LPhyExamplesTest.java +++ b/lphy-base/src/test/java/lphy/base/LPhyExamplesTest.java @@ -56,7 +56,7 @@ public void testLPhyExamplesInDir(File exampleDir) { try { FileReader lphyFile = new FileReader(exampleDir.getAbsoluteFile() + File.separator + fileName); BufferedReader fin = new BufferedReader(lphyFile); - lPhyMetaParser.source(fin); + lPhyMetaParser.source(fin, null); } catch (Exception e) { // failedFiles.add(fileName); System.err.println("Example " + fileName + " failed\n"); diff --git a/lphy-studio/src/main/java/lphystudio/app/graphicalmodelpanel/GraphicalModelPanel.java b/lphy-studio/src/main/java/lphystudio/app/graphicalmodelpanel/GraphicalModelPanel.java index e458994e..256bdba8 100644 --- a/lphy-studio/src/main/java/lphystudio/app/graphicalmodelpanel/GraphicalModelPanel.java +++ b/lphy-studio/src/main/java/lphystudio/app/graphicalmodelpanel/GraphicalModelPanel.java @@ -4,7 +4,6 @@ import lphy.core.logger.LoggerUtils; import lphy.core.model.*; import lphy.core.parser.LPhyParserDictionary; -import lphy.core.parser.REPL; import lphy.core.parser.graphicalmodel.GraphicalModel; import lphy.core.parser.graphicalmodel.GraphicalModelListener; import lphy.core.simulator.Sampler; @@ -230,7 +229,7 @@ public void layout() { // } /** - * This is duplicated to {@link REPL#source(BufferedReader)}, + * This is duplicated to {@link LPhyParserDictionary#source(BufferedReader, String[])}, * but has extra code using {@link LineCodeColorizer} * and panel function. * @@ -240,7 +239,7 @@ public void layout() { public void source(BufferedReader reader) throws IOException { LPhyParserDictionary metaData = component.getParserDictionary(); - metaData.source(reader); + metaData.source(reader, null); repaint(); } diff --git a/lphy/src/main/java/lphy/core/io/FileConfig.java b/lphy/src/main/java/lphy/core/io/FileConfig.java index b071840b..cdcfa15c 100644 --- a/lphy/src/main/java/lphy/core/io/FileConfig.java +++ b/lphy/src/main/java/lphy/core/io/FileConfig.java @@ -111,9 +111,11 @@ public static class Utils { public static FileConfig createSimulationFileConfig(File lphyFile, File outDir, int numReplicates, Long seed ) throws IOException { + + String currentDir = lphyFile.getAbsoluteFile().getParent(); // if user.dir is not the parent folder of lphyFile, then set to it - if (! UserDir.getUserDir().toAbsolutePath().equals(lphyFile.getParentFile())) { - UserDir.setUserDir(lphyFile.getParentFile().getAbsolutePath()); + if (! UserDir.getUserDir().toAbsolutePath().equals(currentDir)) { + UserDir.setUserDir(currentDir); } if (seed != null) diff --git a/lphy/src/main/java/lphy/core/io/MacroProcessor.java b/lphy/src/main/java/lphy/core/io/MacroProcessor.java index dee6d4f8..26cb3184 100644 --- a/lphy/src/main/java/lphy/core/io/MacroProcessor.java +++ b/lphy/src/main/java/lphy/core/io/MacroProcessor.java @@ -1,5 +1,7 @@ package lphy.core.io; +import lphy.core.logger.LoggerUtils; + import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; @@ -15,44 +17,77 @@ public class MacroProcessor { public static final String CONST_MACRO = "\\{\\{\\s*(\\w+)\\s*=\\s*(.*?)\\s*\\}\\}"; private final Pattern pattern; - public MacroProcessor(String regex) { + // init by user input (e.g. cmd args), if map key does not have the var name when per-processing script, + // then use the default from macro. + Map macroMap; + + public MacroProcessor(String regex, String[] constants) { this.pattern = Pattern.compile(regex); + macroMap = new HashMap<>(); + if (constants != null) + // init macroMap based on user input + parseConstants(constants); } - public MacroProcessor() { - this(CONST_MACRO); + /** + * Process the marco in a lphy script, such as {{n = 10}}. + * @param constants constants inputted by user (e.g. command line arguments), + * where each string element looks like n=10. + * Null, if no user input. + */ + public MacroProcessor(String[] constants) { + this(CONST_MACRO, constants); } - // Singleton - private static MacroProcessor INSTANCE; - public static MacroProcessor getInstance() { - if(INSTANCE == null) { - INSTANCE = new MacroProcessor(); - } - return INSTANCE; - } - public static String process(String input) { - Map macroMap = getInstance().getMacroMap(input); - // TODO modify the macroMap based on user input? - return getInstance().getResult(input, macroMap); + /** + * @param input lphy string before preprocessing macros. + * @return a lphy string after replacing all macro into actual values. + */ + public String process(String input) { + assignDefaultValues(input); + return getResult(input); } - public Map getMacroMap(String input) { - Map macroMap = new HashMap<>(); + /** + * Parse command line argument into {@link #macroMap}. + * E.g. -D n=12;L=100 or -D n=20" + * + * @param constants + */ + private void parseConstants(String[] constants) { + for (String arg : constants) { + // n = 12 + if (arg.contains("=")) { + // the pattern will be applied at most limit - 1 times, + // the array's length will be no greater than limit, + // and the array's last entry will contain all input beyond the last matched delimiter. + String[] varVal = arg.split("=", 2); + if (varVal.length == 2) { + // trim spaces + macroMap.put(varVal[0].trim(), varVal[1].trim()); + } + } + } + } + + private void assignDefaultValues(String input) { // Populate the macroMap based on the initial matches Matcher matcher = pattern.matcher(input); while (matcher.find()) { String identifier = matcher.group(1); String literal = matcher.group(2); - macroMap.put(identifier, literal); + // use the default from macro, only when the var name not exist + if (!macroMap.containsKey(identifier)) + macroMap.put(identifier, literal); } - return macroMap; } - public String getResult(String input, Map macroMap) { + private String getResult(String input) { + if (macroMap.isEmpty()) return input; + Matcher replaceMatcher = pattern.matcher(input); StringBuilder stringBuffer = new StringBuilder(); @@ -63,6 +98,9 @@ public String getResult(String input, Map macroMap) { // Slashes ('\') and dollar signs ('$') will be given no special meaning. replacement = Matcher.quoteReplacement(replacement); replaceMatcher.appendReplacement(stringBuffer, replacement); + + LoggerUtils.log.info("Macro replaces the value of constant var : " + replaceMatcher.group(1) + + " = " + replacement); } } replaceMatcher.appendTail(stringBuffer); @@ -73,7 +111,8 @@ public String getResult(String input, Map macroMap) { public static void main(String[] args) { String input = "{{identifier1 = value1}} some text {{identifier2 = value2}}"; - String result = MacroProcessor.process(input); + MacroProcessor macroProcessor = new MacroProcessor(null); + String result = macroProcessor.process(input); System.out.println(result); } diff --git a/lphy/src/main/java/lphy/core/parser/LPhyParserDictionary.java b/lphy/src/main/java/lphy/core/parser/LPhyParserDictionary.java index 71f8af6e..aa5c3504 100644 --- a/lphy/src/main/java/lphy/core/parser/LPhyParserDictionary.java +++ b/lphy/src/main/java/lphy/core/parser/LPhyParserDictionary.java @@ -79,11 +79,12 @@ default List getKeywords() { /** * @param sourceFile the lphy source file + * @param constants constants inputted by user using macro */ - default void source(File sourceFile) throws IOException { + default void source(File sourceFile, String[] constants) throws IOException { FileReader reader = new FileReader(sourceFile); BufferedReader bufferedReader = new BufferedReader(reader); - source(bufferedReader); + source(bufferedReader, constants); reader.close(); } @@ -92,16 +93,23 @@ default void source(File sourceFile) throws IOException { * Before {@link #parse(String)}, the line will be preprocessed by * {@link MacroProcessor}, so that the marco templating language * will be replaced by the actual values. + * * @param bufferedReader the lphy source + * @param constants constants inputted by user (e.g. command line arguments), + * where each string element looks like n=10. + * Null, if no user input. */ - default void source(BufferedReader bufferedReader) throws IOException { + default void source(BufferedReader bufferedReader, String[] constants) throws IOException { + // init processor by use input if there is any + MacroProcessor macroProcessor = new MacroProcessor(constants); + StringBuilder builder = new StringBuilder(); String lineProcessed; String line = bufferedReader.readLine(); while (line != null) { // process macro here - lineProcessed = MacroProcessor.process(line); + lineProcessed = macroProcessor.process(line); builder.append(lineProcessed); builder.append("\n"); diff --git a/lphy/src/main/java/lphy/core/simulator/NamedRandomValueSimulator.java b/lphy/src/main/java/lphy/core/simulator/NamedRandomValueSimulator.java index bd9d083f..976aa7d3 100644 --- a/lphy/src/main/java/lphy/core/simulator/NamedRandomValueSimulator.java +++ b/lphy/src/main/java/lphy/core/simulator/NamedRandomValueSimulator.java @@ -24,27 +24,19 @@ public NamedRandomValueSimulator() { simulatorListener = new ValueFileLoggerListener(); } - public Map> simulateAndSaveResults(File lphyFile, int numReplicates, Long seed) throws IOException { - simulatorListener.start(numReplicates, lphyFile); - return simulate(lphyFile, numReplicates, seed); - } - - public Map> simulateAndSaveResults(FileConfig fileConfig) throws IOException { + // must have File lphyFile, int numReplicates, Long seed + public Map> simulate(FileConfig fileConfig, String[] constants) throws IOException { simulatorListener.start(fileConfig); File lphyFile = fileConfig.lphyInputFile; int numReplicates = fileConfig.numReplicates; Long seed = fileConfig.seed; - return simulate(lphyFile, numReplicates, seed); - } - - private Map> simulate(File lphyFile, int numReplicates, Long seed) throws IOException { // TODO duplicate to maps in ValueFileLoggerListener Map> simResMap = new HashMap<>(); // create Sampler given a lphy script file - sampler = Sampler.createSampler(lphyFile); + sampler = Sampler.createSampler(lphyFile, constants); long start = System.currentTimeMillis(); diff --git a/lphy/src/main/java/lphy/core/simulator/SLPhy.java b/lphy/src/main/java/lphy/core/simulator/SLPhy.java index 1f719cb5..5df12103 100644 --- a/lphy/src/main/java/lphy/core/simulator/SLPhy.java +++ b/lphy/src/main/java/lphy/core/simulator/SLPhy.java @@ -41,6 +41,11 @@ public class SLPhy implements Callable { "usually to create data for well-calibrated study.") int numReps = 1; @CommandLine.Option(names = {"-seed", "--seed"}, description = "the seed.") Long seed; + @CommandLine.Option(names = {"-D", "--data"}, split = ";", + description = "Replace the constant value in the lphy script, multiple constants can be split by ';', " + + "but no ';' at the last: e.g. -D \"n=12;L=100\" or -D n=20") + String[] lphyConst = null; + // enum SPI { loggers } //TODO functions, gendists // // arity = "0" not working // @CommandLine.Option(names = {"-ls", "--list"}, @@ -62,8 +67,9 @@ public Integer call() throws PicocliException { .createSimulationFileConfig(infile.toFile(), outDir, numReps, seed); simulator = new NamedRandomValueSimulator(); - simulator.simulateAndSaveResults(fileConfig); - + // must provide File lphyFile, int numReplicates, Long seed + simulator.simulate(fileConfig, lphyConst); + // TODO save Map> simResMap ? } catch (IOException e) { throw new PicocliException(e.getMessage(), e); } diff --git a/lphy/src/main/java/lphy/core/simulator/Sampler.java b/lphy/src/main/java/lphy/core/simulator/Sampler.java index c876b9ac..91af3e58 100644 --- a/lphy/src/main/java/lphy/core/simulator/Sampler.java +++ b/lphy/src/main/java/lphy/core/simulator/Sampler.java @@ -30,13 +30,14 @@ public Sampler(LPhyParserDictionary parser) { /** * @param lphyFile a File containing LPhy script. + * @param constants constants inputted by user using macro * @return a Sampler created by the given LPhy script. * @throws IOException */ - public static Sampler createSampler(File lphyFile) throws IOException { + public static Sampler createSampler(File lphyFile, String[] constants) throws IOException { //*** Parse LPhy file ***// LPhyParserDictionary parser = new REPL(); - parser.source(lphyFile); + parser.source(lphyFile, constants); // Sampler requires GraphicalLPhyParser Sampler sampler = new Sampler(parser); diff --git a/lphy/src/main/java/lphy/core/simulator/Simulate.java b/lphy/src/main/java/lphy/core/simulator/Simulate.java index 4d0b1d93..996d837f 100644 --- a/lphy/src/main/java/lphy/core/simulator/Simulate.java +++ b/lphy/src/main/java/lphy/core/simulator/Simulate.java @@ -78,8 +78,9 @@ public Value> apply() { FileConfig fileConfig = FileConfig.Utils .createSimulationFileConfig(infile, outDir, SimulatorListener.REPLICATES_START_INDEX, seed); - values = simulator.simulateAndSaveResults(fileConfig) - .get(SimulatorListener.REPLICATES_START_INDEX); + Map> allReps = simulator.simulate(fileConfig, null); + // only take the last replicate + values = allReps.get(SimulatorListener.REPLICATES_START_INDEX); } catch (IOException e) { LoggerUtils.log.severe("Cannot parse LPhy script file ! " + infile.getAbsolutePath());