From f7c9414c4bcc8a48cbc1c25879e9c8eafe772320 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Sun, 24 Dec 2023 17:28:11 +0100 Subject: feat: command line model generator --- subprojects/generator-cli/build.gradle.kts | 24 ++++ .../tools/refinery/generator/cli/RefineryCli.java | 64 ++++++++++ .../generator/cli/commands/GenerateCommand.java | 141 +++++++++++++++++++++ .../tools/refinery/generator/ModelGenerator.java | 30 +++-- .../refinery/generator/ModelGeneratorFactory.java | 7 +- .../tools/refinery/generator/ModelSemantics.java | 2 +- .../tools/refinery/generator/ProblemLoader.java | 18 ++- .../store/dse/strategy/BestFirstExplorer.java | 4 +- .../store/dse/strategy/BestFirstStoreManager.java | 2 +- 9 files changed, 274 insertions(+), 18 deletions(-) create mode 100644 subprojects/generator-cli/build.gradle.kts create mode 100644 subprojects/generator-cli/src/main/java/tools/refinery/generator/cli/RefineryCli.java create mode 100644 subprojects/generator-cli/src/main/java/tools/refinery/generator/cli/commands/GenerateCommand.java (limited to 'subprojects') diff --git a/subprojects/generator-cli/build.gradle.kts b/subprojects/generator-cli/build.gradle.kts new file mode 100644 index 00000000..6c681222 --- /dev/null +++ b/subprojects/generator-cli/build.gradle.kts @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ + +plugins { + id("tools.refinery.gradle.java-application") +} + +dependencies { + implementation(project(":refinery-generator")) + implementation(libs.jcommander) + implementation(libs.slf4j.api) +} + +application { + mainClass.set("tools.refinery.generator.cli.RefineryCli") +} + +tasks.shadowJar { + // Silence Xtext warning. + append("plugin.properties") +} diff --git a/subprojects/generator-cli/src/main/java/tools/refinery/generator/cli/RefineryCli.java b/subprojects/generator-cli/src/main/java/tools/refinery/generator/cli/RefineryCli.java new file mode 100644 index 00000000..5de579e6 --- /dev/null +++ b/subprojects/generator-cli/src/main/java/tools/refinery/generator/cli/RefineryCli.java @@ -0,0 +1,64 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.generator.cli; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.ParameterException; +import com.google.inject.Inject; +import tools.refinery.generator.cli.commands.GenerateCommand; +import tools.refinery.generator.standalone.StandaloneRefinery; + +import java.io.IOException; + +public class RefineryCli { + private static final String GENERATE_COMMAND = "generate"; + + @Inject + private GenerateCommand generateCommand; + + private JCommander jCommander; + + public String parseArguments(String... args) { + var jc = getJCommander(); + jc.parse(args); + return jc.getParsedCommand(); + } + + public void run(String command) throws IOException { + switch (command) { + case GENERATE_COMMAND -> generateCommand.run(); + case null, default -> showUsageAndExit(); + } + } + + public void showUsageAndExit() { + getJCommander().usage(); + System.exit(1); + } + + private JCommander getJCommander() { + if (jCommander == null) { + jCommander = JCommander.newBuilder() + .programName("refinery") + .addObject(this) + .addCommand(GENERATE_COMMAND, generateCommand) + .build(); + } + return jCommander; + } + + public static void main(String[] args) throws IOException { + var cli = StandaloneRefinery.getInjector().getInstance(RefineryCli.class); + String command = null; + try { + command = cli.parseArguments(args); + } catch (ParameterException e) { + System.err.println(e.getMessage()); + cli.showUsageAndExit(); + } + cli.run(command); + } +} diff --git a/subprojects/generator-cli/src/main/java/tools/refinery/generator/cli/commands/GenerateCommand.java b/subprojects/generator-cli/src/main/java/tools/refinery/generator/cli/commands/GenerateCommand.java new file mode 100644 index 00000000..6c1d105d --- /dev/null +++ b/subprojects/generator-cli/src/main/java/tools/refinery/generator/cli/commands/GenerateCommand.java @@ -0,0 +1,141 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.generator.cli.commands; + +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; +import com.google.inject.Inject; +import org.eclipse.emf.ecore.resource.Resource; +import tools.refinery.generator.ModelGeneratorFactory; +import tools.refinery.generator.ProblemLoader; +import tools.refinery.language.model.problem.Problem; +import tools.refinery.language.model.problem.Relation; +import tools.refinery.language.model.problem.ScopeDeclaration; + +import java.io.ByteArrayOutputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +@Parameters(commandDescription = "Generate a model from a partial model") +public class GenerateCommand { + @Inject + private ProblemLoader loader; + + @Inject + private ModelGeneratorFactory generatorFactory; + + private String problemPath; + private String outputPath = "-"; + private List scopes = new ArrayList<>(); + private List overrideScopes = new ArrayList<>(); + private long randomSeed = 1; + + @Parameter(description = "Input path", required = true) + public void setProblemPath(String problemPath) { + this.problemPath = problemPath; + } + + @Parameter(names = {"-output", "-o"}, description = "Output path") + public void setOutputPath(String outputPath) { + this.outputPath = outputPath; + } + + @Parameter(names = {"-scope", "-s"}, description = "Extra scope constraints") + public void setScopes(List scopes) { + this.scopes = scopes; + } + + @Parameter(names = {"-scope-override", "-S"}, description = "Override scope constraints") + public void setOverrideScopes(List overrideScopes) { + this.overrideScopes = overrideScopes; + } + + @Parameter(names = {"-random-seed", "-r"}, description = "Random seed") + public void setRandomSeed(long randomSeed) { + this.randomSeed = randomSeed; + } + + public void run() throws IOException { + var problem = addScopeConstraints(loader.loadFile(problemPath)); + var generator = generatorFactory.createGenerator(problem); + generator.setRandomSeed(randomSeed); + generator.generate(); + var solution = generator.serializeSolution(); + var solutionResource = solution.eResource(); + var saveOptions = Map.of(); + if (outputPath == null || outputPath.equals("-")) { + printSolution(solutionResource, saveOptions); + } else { + try (var outputStream = new FileOutputStream(outputPath)) { + solutionResource.save(outputStream, saveOptions); + } + } + } + + private Problem addScopeConstraints(Problem problem) throws IOException { + var allScopes = new ArrayList<>(scopes); + allScopes.addAll(overrideScopes); + if (allScopes.isEmpty()) { + return problem; + } + int originalStatementCount = problem.getStatements().size(); + var builder = new StringBuilder(); + var problemResource = problem.eResource(); + try (var outputStream = new ByteArrayOutputStream()) { + problemResource.save(outputStream, Map.of()); + builder.append(outputStream.toString(StandardCharsets.UTF_8)); + } + builder.append('\n'); + for (var scope : allScopes) { + builder.append("scope ").append(scope).append(".\n"); + } + var modifiedProblem = loader.loadString(builder.toString(), problemResource.getURI()); + var modifiedStatements = modifiedProblem.getStatements(); + int modifiedStatementCount = modifiedStatements.size(); + if (modifiedStatementCount != originalStatementCount + allScopes.size()) { + throw new IllegalStateException("Failed to parse scope constraints"); + } + // Override scopes remove any scope constraint from the original problem with the same target type. + var overriddenScopes = new HashSet(); + for (int i = modifiedStatementCount - overrideScopes.size(); i < modifiedStatementCount; i++) { + var statement = modifiedStatements.get(i); + if (!(statement instanceof ScopeDeclaration scopeDeclaration)) { + throw new IllegalStateException("Invalid scope constraint: " + statement); + } + for (var typeScope : scopeDeclaration.getTypeScopes()) { + overriddenScopes.add(typeScope.getTargetType()); + } + } + int statementIndex = 0; + var iterator = modifiedStatements.iterator(); + // Scope overrides only affect type scopes from the original problem and leave type scopes added on the + // command line intact. + while (statementIndex < originalStatementCount && iterator.hasNext()) { + var statement = iterator.next(); + if (statement instanceof ScopeDeclaration scopeDeclaration) { + var typeScopes = scopeDeclaration.getTypeScopes(); + typeScopes.removeIf(typeScope -> overriddenScopes.contains(typeScope.getTargetType())); + // Scope declarations with no type scopes are invalid, so we have to remove them. + if (typeScopes.isEmpty()) { + iterator.remove(); + } + } + statementIndex++; + } + return modifiedProblem; + } + + // We deliberately write to the standard output if no output path is specified. + @SuppressWarnings("squid:S106") + private void printSolution(Resource solutionResource, Map saveOptions) throws IOException { + solutionResource.save(System.out, saveOptions); + } +} diff --git a/subprojects/generator/src/main/java/tools/refinery/generator/ModelGenerator.java b/subprojects/generator/src/main/java/tools/refinery/generator/ModelGenerator.java index 5b44c10a..1515dceb 100644 --- a/subprojects/generator/src/main/java/tools/refinery/generator/ModelGenerator.java +++ b/subprojects/generator/src/main/java/tools/refinery/generator/ModelGenerator.java @@ -5,7 +5,10 @@ */ package tools.refinery.generator; +import com.google.inject.Provider; +import tools.refinery.language.model.problem.Problem; import tools.refinery.language.semantics.ProblemTrace; +import tools.refinery.language.semantics.SolutionSerializer; import tools.refinery.store.dse.strategy.BestFirstStoreManager; import tools.refinery.store.map.Version; import tools.refinery.store.model.ModelStore; @@ -16,21 +19,22 @@ import tools.refinery.store.reasoning.seed.ModelSeed; public class ModelGenerator extends ModelFacade { private final Version initialVersion; - - private int randomSeed = 0; - + private final Provider solutionSerializerProvider; + private long randomSeed = 1; private boolean lastGenerationSuccessful; - public ModelGenerator(ProblemTrace problemTrace, ModelStore store, ModelSeed modelSeed) { + ModelGenerator(ProblemTrace problemTrace, ModelStore store, ModelSeed modelSeed, + Provider solutionSerializerProvider) { super(problemTrace, store, modelSeed, Concreteness.CANDIDATE); + this.solutionSerializerProvider = solutionSerializerProvider; initialVersion = getModel().commit(); } - public int getRandomSeed() { + public long getRandomSeed() { return randomSeed; } - public void setRandomSeed(int randomSeed) { + public void setRandomSeed(long randomSeed) { this.randomSeed = randomSeed; this.lastGenerationSuccessful = false; } @@ -50,7 +54,7 @@ public class ModelGenerator extends ModelFacade { if (solutions.isEmpty()) { return false; } - getModel().restore(solutions.get(0).version()); + getModel().restore(solutions.getFirst().version()); lastGenerationSuccessful = true; return true; } @@ -63,9 +67,19 @@ public class ModelGenerator extends ModelFacade { @Override public PartialInterpretation getPartialInterpretation(PartialSymbol partialSymbol) { + checkSuccessfulGeneration(); + return super.getPartialInterpretation(partialSymbol); + } + + public Problem serializeSolution() { + checkSuccessfulGeneration(); + var serializer = solutionSerializerProvider.get(); + return serializer.serializeSolution(getProblemTrace(), getModel()); + } + + private void checkSuccessfulGeneration() { if (!lastGenerationSuccessful) { throw new IllegalStateException("No generated model is available"); } - return super.getPartialInterpretation(partialSymbol); } } diff --git a/subprojects/generator/src/main/java/tools/refinery/generator/ModelGeneratorFactory.java b/subprojects/generator/src/main/java/tools/refinery/generator/ModelGeneratorFactory.java index 6642d591..587601f2 100644 --- a/subprojects/generator/src/main/java/tools/refinery/generator/ModelGeneratorFactory.java +++ b/subprojects/generator/src/main/java/tools/refinery/generator/ModelGeneratorFactory.java @@ -9,6 +9,7 @@ import com.google.inject.Inject; import com.google.inject.Provider; import tools.refinery.language.model.problem.Problem; import tools.refinery.language.semantics.ModelInitializer; +import tools.refinery.language.semantics.SolutionSerializer; import tools.refinery.store.dse.propagation.PropagationAdapter; import tools.refinery.store.dse.transition.DesignSpaceExplorationAdapter; import tools.refinery.store.model.ModelStore; @@ -25,6 +26,9 @@ public final class ModelGeneratorFactory { @Inject private Provider initializerProvider; + @Inject + private Provider solutionSerializerProvider; + private CancellationToken cancellationToken = CancellationToken.NONE; private boolean debugPartialInterpretations; @@ -53,7 +57,8 @@ public final class ModelGeneratorFactory { .requiredInterpretations(getRequiredInterpretations())); initializer.configureStoreBuilder(storeBuilder); var store = storeBuilder.build(); - return new ModelGenerator(initializer.getProblemTrace(), store, initializer.getModelSeed()); + return new ModelGenerator(initializer.getProblemTrace(), store, initializer.getModelSeed(), + solutionSerializerProvider); } private Collection getRequiredInterpretations() { diff --git a/subprojects/generator/src/main/java/tools/refinery/generator/ModelSemantics.java b/subprojects/generator/src/main/java/tools/refinery/generator/ModelSemantics.java index bc02c887..7207a509 100644 --- a/subprojects/generator/src/main/java/tools/refinery/generator/ModelSemantics.java +++ b/subprojects/generator/src/main/java/tools/refinery/generator/ModelSemantics.java @@ -11,7 +11,7 @@ import tools.refinery.store.reasoning.literal.Concreteness; import tools.refinery.store.reasoning.seed.ModelSeed; public class ModelSemantics extends ModelFacade { - public ModelSemantics(ProblemTrace problemTrace, ModelStore store, ModelSeed modelSeed) { + ModelSemantics(ProblemTrace problemTrace, ModelStore store, ModelSeed modelSeed) { super(problemTrace, store, modelSeed, Concreteness.PARTIAL); } } diff --git a/subprojects/generator/src/main/java/tools/refinery/generator/ProblemLoader.java b/subprojects/generator/src/main/java/tools/refinery/generator/ProblemLoader.java index 2d93a5ad..abfbe7e6 100644 --- a/subprojects/generator/src/main/java/tools/refinery/generator/ProblemLoader.java +++ b/subprojects/generator/src/main/java/tools/refinery/generator/ProblemLoader.java @@ -48,21 +48,29 @@ public class ProblemLoader { return this; } - public Problem loadString(String problemString) throws IOException { + public Problem loadString(String problemString, URI uri) throws IOException { try (var stream = new LazyStringInputStream(problemString)) { - return loadStream(stream); + return loadStream(stream, uri); } } - public Problem loadStream(InputStream inputStream) throws IOException { + public Problem loadString(String problemString) throws IOException { + return loadString(problemString, null); + } + + public Problem loadStream(InputStream inputStream, URI uri) throws IOException { var resourceSet = resourceSetProvider.get(); - var uri = URI.createFileURI("__synthetic." + fileExtension); - var resource = resourceFactory.createResource(uri); + var resourceUri = uri == null ? URI.createFileURI("__synthetic." + fileExtension) : uri; + var resource = resourceFactory.createResource(resourceUri); resourceSet.getResources().add(resource); resource.load(inputStream, Map.of()); return loadResource(resource); } + public Problem loadStream(InputStream inputStream) throws IOException { + return loadStream(inputStream, null); + } + public Problem loadFile(File file) throws IOException { return loadFile(file.getAbsolutePath()); } diff --git a/subprojects/store-dse/src/main/java/tools/refinery/store/dse/strategy/BestFirstExplorer.java b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/strategy/BestFirstExplorer.java index ce3efb21..22f38ca9 100644 --- a/subprojects/store-dse/src/main/java/tools/refinery/store/dse/strategy/BestFirstExplorer.java +++ b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/strategy/BestFirstExplorer.java @@ -10,10 +10,10 @@ import tools.refinery.store.model.Model; import java.util.Random; public class BestFirstExplorer extends BestFirstWorker { - final int id; + final long id; Random random; - public BestFirstExplorer(BestFirstStoreManager storeManager, Model model, int id) { + public BestFirstExplorer(BestFirstStoreManager storeManager, Model model, long id) { super(storeManager, model); this.id = id; // The use of a non-cryptographic random generator is safe here, because we only use it to direct the state diff --git a/subprojects/store-dse/src/main/java/tools/refinery/store/dse/strategy/BestFirstStoreManager.java b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/strategy/BestFirstStoreManager.java index 3d32f84c..c20ab9a0 100644 --- a/subprojects/store-dse/src/main/java/tools/refinery/store/dse/strategy/BestFirstStoreManager.java +++ b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/strategy/BestFirstStoreManager.java @@ -79,7 +79,7 @@ public class BestFirstStoreManager { startExploration(initial, 1); } - public void startExploration(Version initial, int randomSeed) { + public void startExploration(Version initial, long randomSeed) { BestFirstExplorer bestFirstExplorer = new BestFirstExplorer(this, modelStore.createModelForState(initial), randomSeed); bestFirstExplorer.explore(); -- cgit v1.2.3-70-g09d2