From c93d455d2459c2fbe0cfce7693d592986f65d44c Mon Sep 17 00:00:00 2001 From: nagilooh Date: Wed, 2 Aug 2023 14:07:35 +0200 Subject: Move DSE to new subproject --- .../tools/refinery/store/dse/ActionFactory.java | 9 + .../store/dse/DesignSpaceExplorationAdapter.java | 61 +++++ .../store/dse/DesignSpaceExplorationBuilder.java | 43 ++++ .../dse/DesignSpaceExplorationStoreAdapter.java | 6 + .../java/tools/refinery/store/dse/Strategy.java | 8 + .../refinery/store/dse/internal/Activation.java | 9 + .../DesignSpaceExplorationAdapterImpl.java | 283 +++++++++++++++++++++ .../DesignSpaceExplorationBuilderImpl.java | 62 +++++ .../DesignSpaceExplorationStoreAdapterImpl.java | 57 +++++ .../store/dse/internal/TransformationRule.java | 84 ++++++ .../AlwaysSatisfiedDummyHardObjective.java | 51 ++++ .../store/dse/objectives/BaseObjective.java | 131 ++++++++++ .../refinery/store/dse/objectives/Comparators.java | 25 ++ .../refinery/store/dse/objectives/Fitness.java | 21 ++ .../refinery/store/dse/objectives/Objective.java | 100 ++++++++ .../dse/objectives/ObjectiveComparatorHelper.java | 58 +++++ .../store/dse/strategy/BestFirstStrategy.java | 190 ++++++++++++++ .../store/dse/strategy/DepthFirstStrategy.java | 108 ++++++++ 18 files changed, 1306 insertions(+) create mode 100644 subprojects/store-dse/src/main/java/tools/refinery/store/dse/ActionFactory.java create mode 100644 subprojects/store-dse/src/main/java/tools/refinery/store/dse/DesignSpaceExplorationAdapter.java create mode 100644 subprojects/store-dse/src/main/java/tools/refinery/store/dse/DesignSpaceExplorationBuilder.java create mode 100644 subprojects/store-dse/src/main/java/tools/refinery/store/dse/DesignSpaceExplorationStoreAdapter.java create mode 100644 subprojects/store-dse/src/main/java/tools/refinery/store/dse/Strategy.java create mode 100644 subprojects/store-dse/src/main/java/tools/refinery/store/dse/internal/Activation.java create mode 100644 subprojects/store-dse/src/main/java/tools/refinery/store/dse/internal/DesignSpaceExplorationAdapterImpl.java create mode 100644 subprojects/store-dse/src/main/java/tools/refinery/store/dse/internal/DesignSpaceExplorationBuilderImpl.java create mode 100644 subprojects/store-dse/src/main/java/tools/refinery/store/dse/internal/DesignSpaceExplorationStoreAdapterImpl.java create mode 100644 subprojects/store-dse/src/main/java/tools/refinery/store/dse/internal/TransformationRule.java create mode 100644 subprojects/store-dse/src/main/java/tools/refinery/store/dse/objectives/AlwaysSatisfiedDummyHardObjective.java create mode 100644 subprojects/store-dse/src/main/java/tools/refinery/store/dse/objectives/BaseObjective.java create mode 100644 subprojects/store-dse/src/main/java/tools/refinery/store/dse/objectives/Comparators.java create mode 100644 subprojects/store-dse/src/main/java/tools/refinery/store/dse/objectives/Fitness.java create mode 100644 subprojects/store-dse/src/main/java/tools/refinery/store/dse/objectives/Objective.java create mode 100644 subprojects/store-dse/src/main/java/tools/refinery/store/dse/objectives/ObjectiveComparatorHelper.java create mode 100644 subprojects/store-dse/src/main/java/tools/refinery/store/dse/strategy/BestFirstStrategy.java create mode 100644 subprojects/store-dse/src/main/java/tools/refinery/store/dse/strategy/DepthFirstStrategy.java (limited to 'subprojects/store-dse/src/main') diff --git a/subprojects/store-dse/src/main/java/tools/refinery/store/dse/ActionFactory.java b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/ActionFactory.java new file mode 100644 index 00000000..2af22963 --- /dev/null +++ b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/ActionFactory.java @@ -0,0 +1,9 @@ +package tools.refinery.store.dse; + +import org.eclipse.collections.api.block.procedure.Procedure; +import tools.refinery.store.model.Model; +import tools.refinery.store.tuple.Tuple; + +public interface ActionFactory { + Procedure prepare(Model model); +} diff --git a/subprojects/store-dse/src/main/java/tools/refinery/store/dse/DesignSpaceExplorationAdapter.java b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/DesignSpaceExplorationAdapter.java new file mode 100644 index 00000000..729a6fc9 --- /dev/null +++ b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/DesignSpaceExplorationAdapter.java @@ -0,0 +1,61 @@ +package tools.refinery.store.dse; + +import tools.refinery.store.adapter.ModelAdapter; +import tools.refinery.store.map.Version; +import tools.refinery.store.dse.internal.Activation; +import tools.refinery.store.dse.internal.DesignSpaceExplorationBuilderImpl; +import tools.refinery.store.dse.objectives.Fitness; +import tools.refinery.store.dse.objectives.ObjectiveComparatorHelper; +import tools.refinery.store.tuple.Tuple; +import tools.refinery.store.tuple.Tuple1; + +import java.util.Collection; +import java.util.List; +import java.util.Random; + +public interface DesignSpaceExplorationAdapter extends ModelAdapter { + @Override + DesignSpaceExplorationStoreAdapter getStoreAdapter(); + + static DesignSpaceExplorationBuilder builder() { + return new DesignSpaceExplorationBuilderImpl(); + } + + Collection explore(); + + public int getModelSize(); + + public Tuple1 createObject(); + + public Tuple deleteObject(Tuple tuple); + + public boolean checkGlobalConstraints(); + + public boolean backtrack(); + + public Fitness calculateFitness(); + + public void newSolution(); + + public int getDepth(); + + public Collection getUntraversedActivations(); + + public boolean fireActivation(Activation activation); + + public void fireRandomActivation(); + + public boolean isCurrentInTrajectory(); + + public List getTrajectory(); + + public boolean isCurrentStateAlreadyTraversed(); + + public ObjectiveComparatorHelper getObjectiveComparatorHelper(); + + public void restoreTrajectory(List trajectory); + + public void setRandom(Random random); + + public void setRandom(long seed); +} diff --git a/subprojects/store-dse/src/main/java/tools/refinery/store/dse/DesignSpaceExplorationBuilder.java b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/DesignSpaceExplorationBuilder.java new file mode 100644 index 00000000..8ca0037d --- /dev/null +++ b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/DesignSpaceExplorationBuilder.java @@ -0,0 +1,43 @@ +package tools.refinery.store.dse; + +import tools.refinery.store.adapter.ModelAdapterBuilder; +import tools.refinery.store.query.dnf.RelationalQuery; +import tools.refinery.store.dse.internal.TransformationRule; +import tools.refinery.store.dse.objectives.Objective; + +import java.util.Collection; +import java.util.List; + +public interface DesignSpaceExplorationBuilder extends ModelAdapterBuilder { + default DesignSpaceExplorationBuilder transformations(TransformationRule... transformationRules) { + return transformations(List.of(transformationRules)); + } + + default DesignSpaceExplorationBuilder transformations(Collection transformationRules) { + transformationRules.forEach(this::transformation); + return this; + } + + default DesignSpaceExplorationBuilder globalConstraints(RelationalQuery... globalConstraints) { + return globalConstraints(List.of(globalConstraints)); + } + + default DesignSpaceExplorationBuilder globalConstraints(Collection globalConstraints) { + globalConstraints.forEach(this::globalConstraint); + return this; + } + + default DesignSpaceExplorationBuilder objectives(Objective... objectives) { + return objectives(List.of(objectives)); + } + + default DesignSpaceExplorationBuilder objectives(Collection objectives) { + objectives.forEach(this::objective); + return this; + } + + DesignSpaceExplorationBuilder transformation(TransformationRule transformationRule); + DesignSpaceExplorationBuilder globalConstraint(RelationalQuery globalConstraint); + DesignSpaceExplorationBuilder objective(Objective objective); + DesignSpaceExplorationBuilder strategy(Strategy strategy); +} diff --git a/subprojects/store-dse/src/main/java/tools/refinery/store/dse/DesignSpaceExplorationStoreAdapter.java b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/DesignSpaceExplorationStoreAdapter.java new file mode 100644 index 00000000..5964cd82 --- /dev/null +++ b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/DesignSpaceExplorationStoreAdapter.java @@ -0,0 +1,6 @@ +package tools.refinery.store.dse; + +import tools.refinery.store.adapter.ModelStoreAdapter; + +public interface DesignSpaceExplorationStoreAdapter extends ModelStoreAdapter { +} diff --git a/subprojects/store-dse/src/main/java/tools/refinery/store/dse/Strategy.java b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/Strategy.java new file mode 100644 index 00000000..cef43386 --- /dev/null +++ b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/Strategy.java @@ -0,0 +1,8 @@ +package tools.refinery.store.dse; + +public interface Strategy { + + public void initStrategy(DesignSpaceExplorationAdapter designSpaceExplorationAdapter); + + public void explore(); +} diff --git a/subprojects/store-dse/src/main/java/tools/refinery/store/dse/internal/Activation.java b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/internal/Activation.java new file mode 100644 index 00000000..f1de00e6 --- /dev/null +++ b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/internal/Activation.java @@ -0,0 +1,9 @@ +package tools.refinery.store.dse.internal; + +import tools.refinery.store.tuple.Tuple; + +public record Activation(TransformationRule transformationRule, Tuple activation) { + public boolean fire() { + return transformationRule.fireActivation(activation); + } +} diff --git a/subprojects/store-dse/src/main/java/tools/refinery/store/dse/internal/DesignSpaceExplorationAdapterImpl.java b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/internal/DesignSpaceExplorationAdapterImpl.java new file mode 100644 index 00000000..5fb54da9 --- /dev/null +++ b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/internal/DesignSpaceExplorationAdapterImpl.java @@ -0,0 +1,283 @@ +package tools.refinery.store.dse.internal; + +import tools.refinery.store.map.Version; +import tools.refinery.store.model.Interpretation; +import tools.refinery.store.model.Model; +import tools.refinery.store.query.ModelQueryAdapter; +import tools.refinery.store.query.dnf.RelationalQuery; +import tools.refinery.store.dse.DesignSpaceExplorationAdapter; +import tools.refinery.store.dse.DesignSpaceExplorationStoreAdapter; +import tools.refinery.store.dse.Strategy; +import tools.refinery.store.dse.objectives.Fitness; +import tools.refinery.store.dse.objectives.Objective; +import tools.refinery.store.dse.objectives.ObjectiveComparatorHelper; +import tools.refinery.store.query.resultset.ResultSet; +import tools.refinery.store.representation.Symbol; +import tools.refinery.store.tuple.Tuple; +import tools.refinery.store.tuple.Tuple1; +import tools.refinery.visualization.ModelVisualizerAdapter; + +import java.util.*; + +public class DesignSpaceExplorationAdapterImpl implements DesignSpaceExplorationAdapter { + static final Symbol NODE_COUNT_SYMBOL = Symbol.of("MODEL_SIZE", 0, Integer.class, 0); + private final Model model; + private final ModelQueryAdapter queryEngine; + private final DesignSpaceExplorationStoreAdapterImpl storeAdapter; + private final LinkedHashSet transformationRules; + private final LinkedHashSet globalConstraints; + private final List objectives; + private final LinkedHashSet> globalConstraintResultSets = new LinkedHashSet<>(); + private final Interpretation sizeInterpretation; + private final Strategy strategy; + + private ObjectiveComparatorHelper objectiveComparatorHelper; + private List trajectory = new LinkedList<>(); + private Fitness lastFitness; + private final LinkedHashSet solutions = new LinkedHashSet<>(); + private Map> statesAndUntraversedActivations; + private Map> statesAndTraversedActivations; + private Random random = new Random(); + private boolean isNewState = false; + private final boolean isVisualizationEnabled; + private final ModelVisualizerAdapter modelVisualizerAdapter; + + public List getTrajectory() { + return new LinkedList<>(trajectory); + } + + public DesignSpaceExplorationAdapterImpl(Model model, DesignSpaceExplorationStoreAdapterImpl storeAdapter) { + this.model = model; + this.storeAdapter = storeAdapter; + this.sizeInterpretation = model.getInterpretation(NODE_COUNT_SYMBOL); + queryEngine = model.getAdapter(ModelQueryAdapter.class); + + globalConstraints = storeAdapter.getGlobalConstraints(); + for (var constraint : globalConstraints) { + globalConstraintResultSets.add(queryEngine.getResultSet(constraint)); + } + + transformationRules = storeAdapter.getTransformationSpecifications(); + for (var rule : transformationRules) { + rule.prepare(model, queryEngine); + } + + objectives = storeAdapter.getObjectives(); + statesAndUntraversedActivations = new HashMap<>(); + statesAndTraversedActivations = new HashMap<>(); + strategy = storeAdapter.getStrategy(); + modelVisualizerAdapter = model.tryGetAdapter(ModelVisualizerAdapter.class).orElse(null); + isVisualizationEnabled = modelVisualizerAdapter != null; + + } + + @Override + public Model getModel() { + return model; + } + + @Override + public DesignSpaceExplorationStoreAdapter getStoreAdapter() { + return storeAdapter; + } + + @Override + public LinkedHashSet explore() { + var state = model.commit(); + trajectory.add(state); + statesAndUntraversedActivations.put(state, getAllActivations()); + statesAndTraversedActivations.put(state, new LinkedHashSet<>()); + strategy.initStrategy(this); + strategy.explore(); + return solutions; + } + + @Override + public int getModelSize() { + return sizeInterpretation.get(Tuple.of()); + } + + @Override + public Tuple1 createObject() { + var newNodeId = getModelSize(); + sizeInterpretation.put(Tuple.of(), newNodeId + 1); + return Tuple.of(newNodeId); + } + + @Override + public Tuple deleteObject(Tuple tuple) { + if (tuple.getSize() != 1) { + throw new IllegalArgumentException("Tuple size must be 1"); + } +// TODO: implement more efficient deletion +// if (tuple.get(0) == getModelSize() - 1) { +// sizeInterpretation.put(Tuple.of(), getModelSize() - 1); +// } + return tuple; + } + + @Override + public boolean checkGlobalConstraints() { + for (var resultSet : globalConstraintResultSets) { + if (resultSet.size() > 0) { + return false; + } + } + return true; + } + + @Override + public boolean backtrack() { + if (trajectory.size() < 2) { + return false; + } + if (isVisualizationEnabled) { + modelVisualizerAdapter.addTransition(trajectory.get(trajectory.size() - 1), + trajectory.get(trajectory.size() - 2), "backtrack"); + } + model.restore(trajectory.get(trajectory.size() - 2)); + trajectory.remove(trajectory.size() - 1); + return true; + } + + @Override + public void restoreTrajectory(List trajectory) { + model.restore(trajectory.get(trajectory.size() - 1)); +// if (isVisualizationEnabled) { +// modelVisualizerAdapter.addTransition(this.trajectory.get(trajectory.size() - 1), +// trajectory.get(trajectory.size() - 1), "restore"); +// } + this.trajectory = trajectory; + + } + + @Override + public void setRandom(Random random) { + this.random = random; + } + + @Override + public void setRandom(long seed) { + this.random = new Random(seed); + } + + @Override + public Fitness calculateFitness() { + Fitness result = new Fitness(); + boolean satisfiesHardObjectives = true; + for (Objective objective : objectives) { + var fitness = objective.getFitness(this); + result.put(objective.getName(), fitness); + if (objective.isHardObjective() && !objective.satisfiesHardObjective(fitness)) { + satisfiesHardObjectives = false; + } + } + result.setSatisfiesHardObjectives(satisfiesHardObjectives); + + lastFitness = result; + + return result; + } + + @Override + public void newSolution() { + var state = model.getState(); + solutions.add(state); + if (isVisualizationEnabled) { + modelVisualizerAdapter.addSolution(state); + } + } + + @Override + public int getDepth() { + return trajectory.size() - 1; + } + + public LinkedHashSet getUntraversedActivations() { +// return statesAndUntraversedActivations.get(model.getState()); + LinkedHashSet untraversedActivations = new LinkedHashSet<>(); + for (Activation activation : getAllActivations()) { + if (!statesAndTraversedActivations.get(model.getState()).contains(activation)) { + untraversedActivations.add(activation); + } + } + + return untraversedActivations; + } + + @Override + public boolean fireActivation(Activation activation) { + if (activation == null) { + return false; + } + var previousState = model.getState(); + if (!statesAndUntraversedActivations.get(previousState).contains(activation)) { +// TODO: throw exception? + return false; + } + if (!activation.fire()) { + return false; + } + statesAndUntraversedActivations.get(previousState).remove(activation); + statesAndTraversedActivations.get(previousState).add(activation); + var newState = model.commit(); + trajectory.add(newState); + isNewState = !statesAndUntraversedActivations.containsKey(newState); + statesAndUntraversedActivations.put(newState, getAllActivations()); + statesAndTraversedActivations.put(newState, new LinkedHashSet<>()); + if (isVisualizationEnabled) { + if (isNewState) { + modelVisualizerAdapter.addState(newState); + } + modelVisualizerAdapter.addTransition(trajectory.get(trajectory.size() - 2), + trajectory.get(trajectory.size() - 1), activation.transformationRule().getName(), + activation.activation()); + } + return true; + } + + @Override + public void fireRandomActivation() { + var activations = getUntraversedActivations(); + if (activations.isEmpty()) { +// TODO: throw exception + return; + } + int index = random.nextInt(activations.size()); + var iterator = activations.iterator(); + while (index-- > 0) { + iterator.next(); + } + var activationId = iterator.next(); + fireActivation(activationId); + } + + @Override + public boolean isCurrentInTrajectory() { + return trajectory.contains(model.getState()); + } + + public LinkedHashSet getAllActivations() { + LinkedHashSet result = new LinkedHashSet<>(); + for (var rule : transformationRules) { + result.addAll(rule.getAllActivations()); + } + return result; + } + + public boolean isCurrentStateAlreadyTraversed() { +// TODO: check isomorphism? + return !isNewState; + } + + public Fitness getLastFitness() { + return lastFitness; + } + + public ObjectiveComparatorHelper getObjectiveComparatorHelper() { + if (objectiveComparatorHelper == null) { + objectiveComparatorHelper = new ObjectiveComparatorHelper(objectives); + } + return objectiveComparatorHelper; + } +} diff --git a/subprojects/store-dse/src/main/java/tools/refinery/store/dse/internal/DesignSpaceExplorationBuilderImpl.java b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/internal/DesignSpaceExplorationBuilderImpl.java new file mode 100644 index 00000000..03508adc --- /dev/null +++ b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/internal/DesignSpaceExplorationBuilderImpl.java @@ -0,0 +1,62 @@ +package tools.refinery.store.dse.internal; + +import tools.refinery.store.adapter.AbstractModelAdapterBuilder; +import tools.refinery.store.model.ModelStore; +import tools.refinery.store.model.ModelStoreBuilder; +import tools.refinery.store.query.dnf.RelationalQuery; +import tools.refinery.store.dse.DesignSpaceExplorationBuilder; +import tools.refinery.store.dse.Strategy; +import tools.refinery.store.dse.objectives.Objective; + +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; + +public class DesignSpaceExplorationBuilderImpl + extends AbstractModelAdapterBuilder + implements DesignSpaceExplorationBuilder { + private final LinkedHashSet transformationSpecifications = new LinkedHashSet<>(); + private final LinkedHashSet globalConstraints = new LinkedHashSet<>(); + private final List objectives = new LinkedList<>(); + private Strategy strategy; + + @Override + protected DesignSpaceExplorationStoreAdapterImpl doBuild(ModelStore store) { + return new DesignSpaceExplorationStoreAdapterImpl(store, transformationSpecifications, globalConstraints, + objectives, strategy); + } + + @Override + public DesignSpaceExplorationBuilder transformation(TransformationRule transformationRule) { + checkNotConfigured(); + transformationSpecifications.add(transformationRule); + return this; + } + + @Override + public DesignSpaceExplorationBuilder globalConstraint(RelationalQuery globalConstraint) { + checkNotConfigured(); + globalConstraints.add(globalConstraint); + return this; + } + + @Override + public DesignSpaceExplorationBuilder objective(Objective objective) { + checkNotConfigured(); + objectives.add(objective); + return this; + } + + @Override + public DesignSpaceExplorationBuilder strategy(Strategy strategy) { + checkNotConfigured(); + this.strategy = strategy; + return this; + } + + @Override + protected void doConfigure(ModelStoreBuilder storeBuilder) { + storeBuilder.symbols(DesignSpaceExplorationAdapterImpl.NODE_COUNT_SYMBOL); + super.doConfigure(storeBuilder); + } +} diff --git a/subprojects/store-dse/src/main/java/tools/refinery/store/dse/internal/DesignSpaceExplorationStoreAdapterImpl.java b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/internal/DesignSpaceExplorationStoreAdapterImpl.java new file mode 100644 index 00000000..b06462ce --- /dev/null +++ b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/internal/DesignSpaceExplorationStoreAdapterImpl.java @@ -0,0 +1,57 @@ +package tools.refinery.store.dse.internal; + +import tools.refinery.store.adapter.ModelAdapter; +import tools.refinery.store.model.Model; +import tools.refinery.store.model.ModelStore; +import tools.refinery.store.query.dnf.RelationalQuery; +import tools.refinery.store.dse.DesignSpaceExplorationStoreAdapter; +import tools.refinery.store.dse.Strategy; +import tools.refinery.store.dse.objectives.Objective; + +import java.util.LinkedHashSet; +import java.util.List; + +public class DesignSpaceExplorationStoreAdapterImpl implements DesignSpaceExplorationStoreAdapter { + private final ModelStore store; + private final LinkedHashSet transformationSpecifications; + private final LinkedHashSet globalConstraints; + private final List objectives; + private final Strategy strategy; + + public DesignSpaceExplorationStoreAdapterImpl(ModelStore store, + LinkedHashSet transformationSpecifications, + LinkedHashSet globalConstraints, + List objectives, Strategy strategy) { + this.store = store; + this.transformationSpecifications = transformationSpecifications; + this.globalConstraints = globalConstraints; + this.objectives = objectives; + this.strategy = strategy; + } + + @Override + public ModelStore getStore() { + return store; + } + + @Override + public ModelAdapter createModelAdapter(Model model) { + return new DesignSpaceExplorationAdapterImpl(model, this); + } + + public LinkedHashSet getTransformationSpecifications() { + return transformationSpecifications; + } + + public LinkedHashSet getGlobalConstraints() { + return globalConstraints; + } + + public List getObjectives() { + return objectives; + } + + public Strategy getStrategy() { + return strategy; + } +} diff --git a/subprojects/store-dse/src/main/java/tools/refinery/store/dse/internal/TransformationRule.java b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/internal/TransformationRule.java new file mode 100644 index 00000000..ed2e77f1 --- /dev/null +++ b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/internal/TransformationRule.java @@ -0,0 +1,84 @@ +package tools.refinery.store.dse.internal; + +import org.eclipse.collections.api.block.procedure.Procedure; +import tools.refinery.store.model.Model; +import tools.refinery.store.query.ModelQueryAdapter; +import tools.refinery.store.query.dnf.RelationalQuery; +import tools.refinery.store.dse.ActionFactory; +import tools.refinery.store.query.resultset.OrderedResultSet; +import tools.refinery.store.query.resultset.ResultSet; +import tools.refinery.store.tuple.Tuple; + +import java.util.LinkedHashSet; +import java.util.Random; + +public class TransformationRule { + + private final String name; + private final RelationalQuery precondition; + private final ActionFactory actionFactory; + private Procedure action; + private OrderedResultSet activations; + private Random random; + private ModelQueryAdapter queryEngine; + + public TransformationRule(String name, RelationalQuery precondition, ActionFactory actionFactory) { + this(name, precondition, actionFactory, new Random()); + } + + public TransformationRule(String name, RelationalQuery precondition, ActionFactory actionFactory, long seed) { + this(name, precondition, actionFactory, new Random(seed)); + } + + public TransformationRule(String name, RelationalQuery precondition, ActionFactory actionFactory, Random random) { + this.name = name; + this.precondition = precondition; + this.actionFactory = actionFactory; + this.random = random; + } + public boolean prepare(Model model, ModelQueryAdapter queryEngine) { + action = actionFactory.prepare(model); + this.queryEngine = queryEngine; + activations = new OrderedResultSet<>(queryEngine.getResultSet(precondition)); + return true; + } + + public boolean fireActivation(Tuple activation) { + action.accept(activation); + queryEngine.flushChanges(); + return true; + } + + public boolean fireRandomActivation() { + return getRandomActivation().fire(); + } + + public String getName() { + return name; + } + + public RelationalQuery getPrecondition() { + return precondition; + } + + public ResultSet getAllActivationsAsSets() { + return activations; + } + + public LinkedHashSet getAllActivations() { + var result = new LinkedHashSet(); + var cursor = activations.getAll(); + while (cursor.move()) { + result.add(new Activation(this, cursor.getKey())); + } + return result; + } + + public Activation getRandomActivation() { + return new Activation(this, activations.getKey(random.nextInt(activations.size()))); + } + + public Activation getActivation(int index) { + return new Activation(this, activations.getKey(index)); + } +} diff --git a/subprojects/store-dse/src/main/java/tools/refinery/store/dse/objectives/AlwaysSatisfiedDummyHardObjective.java b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/objectives/AlwaysSatisfiedDummyHardObjective.java new file mode 100644 index 00000000..82695704 --- /dev/null +++ b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/objectives/AlwaysSatisfiedDummyHardObjective.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * Copyright (c) 2010-2016, Andras Szabolcs Nagy, Zoltan Ujhelyi and Daniel Varro + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-v20.html. + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package tools.refinery.store.dse.objectives; + +import tools.refinery.store.dse.DesignSpaceExplorationAdapter; + +/** + * This hard objective is fulfilled in any circumstances. Use it if all states should be regarded as a valid solution. + * + * @author Andras Szabolcs Nagy + * + */ +public class AlwaysSatisfiedDummyHardObjective extends BaseObjective { + + private static final String DEFAULT_NAME = "AlwaysSatisfiedDummyHardObjective"; + + public AlwaysSatisfiedDummyHardObjective() { + super(DEFAULT_NAME); + } + + public AlwaysSatisfiedDummyHardObjective(String name) { + super(name); + } + + @Override + public Double getFitness(DesignSpaceExplorationAdapter context) { + return 0d; + } + + @Override + public boolean isHardObjective() { + return true; + } + + @Override + public boolean satisfiesHardObjective(Double fitness) { + return true; + } + + @Override + public Objective createNew() { + return this; + } + +} diff --git a/subprojects/store-dse/src/main/java/tools/refinery/store/dse/objectives/BaseObjective.java b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/objectives/BaseObjective.java new file mode 100644 index 00000000..24e3280d --- /dev/null +++ b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/objectives/BaseObjective.java @@ -0,0 +1,131 @@ +/******************************************************************************* + * Copyright (c) 2010-2015, Andras Szabolcs Nagy, Abel Hegedus, Akos Horvath, Zoltan Ujhelyi and Daniel Varro + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-v20.html. + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package tools.refinery.store.dse.objectives; + +import tools.refinery.store.dse.DesignSpaceExplorationAdapter; + +import java.util.Comparator; +import java.util.Objects; + +/** + * This abstract class implements the basic functionality of an objective ({@link Objective} namely its name, + * comparator, level and fitness hard constraint. + * + * @author Andras Szabolcs Nagy + * + */ +public abstract class BaseObjective implements Objective { + + protected final String name; + protected Comparator comparator = Comparators.HIGHER_IS_BETTER; + + protected double fitnessConstraint; + protected boolean isThereFitnessConstraint = false; + protected Comparator fitnessConstraintComparator; + + public BaseObjective(String name) { + Objects.requireNonNull(name, "Name of the objective cannot be null."); + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public void setComparator(Comparator comparator) { + this.comparator = comparator; + } + + @Override + public Comparator getComparator() { + return comparator; + } + + public BaseObjective withComparator(Comparator comparator) { + setComparator(comparator); + return this; + } + + /** + * Adds a hard constraint on the fitness value. For example, the fitness value must be better than 10 to accept the + * current state as a solution. + * + * @param fitnessConstraint + * Solutions should be better than this value. + * @param fitnessConstraintComparator + * {@link Comparator} to determine if the current state is better than the given value. + * @return The actual instance to enable builder pattern like usage. + */ + public BaseObjective withHardConstraintOnFitness(double fitnessConstraint, + Comparator fitnessConstraintComparator) { + this.fitnessConstraint = fitnessConstraint; + this.fitnessConstraintComparator = fitnessConstraintComparator; + this.isThereFitnessConstraint = true; + return this; + } + + /** + * Adds a hard constraint on the fitness value. For example, the fitness value must be better than 10 to accept the + * current state as a solution. The provided comparator will be used. + * + * @param fitnessConstraint + * Solutions should be better than this value. + * @return The actual instance to enable builder pattern like usage. + */ + public BaseObjective withHardConstraintOnFitness(double fitnessConstraint) { + return withHardConstraintOnFitness(fitnessConstraint, null); + } + + @Override + public void init(DesignSpaceExplorationAdapter context) { + if (fitnessConstraintComparator == null) { + fitnessConstraintComparator = comparator; + } + } + + @Override + public boolean isHardObjective() { + return isThereFitnessConstraint; + } + + @Override + public boolean satisfiesHardObjective(Double fitness) { + if (isThereFitnessConstraint) { + int compare = fitnessConstraintComparator.compare(fitness, fitnessConstraint); + if (compare < 0) { + return false; + } + } + return true; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof BaseObjective baseObjective) { + return name.equals(baseObjective.getName()); + } + return false; + } + + @Override + public String toString() { + return name; + } + +} diff --git a/subprojects/store-dse/src/main/java/tools/refinery/store/dse/objectives/Comparators.java b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/objectives/Comparators.java new file mode 100644 index 00000000..e64e04e8 --- /dev/null +++ b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/objectives/Comparators.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * Copyright (c) 2010-2015, Andras Szabolcs Nagy, Abel Hegedus, Akos Horvath, Zoltan Ujhelyi and Daniel Varro + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-v20.html. + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package tools.refinery.store.dse.objectives; + +import java.util.Comparator; + +public class Comparators { + + private Comparators() { /*Utility class constructor*/ } + + public static final Comparator HIGHER_IS_BETTER = (o1, o2) -> o1.compareTo(o2); + + public static final Comparator LOWER_IS_BETTER = (o1, o2) -> o2.compareTo(o1); + + private static final Double ZERO = (double) 0; + + public static final Comparator DIFFERENCE_TO_ZERO_IS_BETTER = (o1, o2) -> ZERO.compareTo(Math.abs(o1)-Math.abs(o2)); + +} diff --git a/subprojects/store-dse/src/main/java/tools/refinery/store/dse/objectives/Fitness.java b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/objectives/Fitness.java new file mode 100644 index 00000000..16caed85 --- /dev/null +++ b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/objectives/Fitness.java @@ -0,0 +1,21 @@ +package tools.refinery.store.dse.objectives; + +import java.util.HashMap; + +public class Fitness extends HashMap { + + private boolean satisfiesHardObjectives; + + public boolean isSatisfiesHardObjectives() { + return satisfiesHardObjectives; + } + + public void setSatisfiesHardObjectives(boolean satisfiesHardObjectives) { + this.satisfiesHardObjectives = satisfiesHardObjectives; + } + + @Override + public String toString() { + return super.toString() + " hardObjectives=" + satisfiesHardObjectives; + } +} diff --git a/subprojects/store-dse/src/main/java/tools/refinery/store/dse/objectives/Objective.java b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/objectives/Objective.java new file mode 100644 index 00000000..4e14c9a3 --- /dev/null +++ b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/objectives/Objective.java @@ -0,0 +1,100 @@ +/******************************************************************************* + * Copyright (c) 2010-2014, Miklos Foldenyi, Andras Szabolcs Nagy, Abel Hegedus, Akos Horvath, Zoltan Ujhelyi and Daniel Varro + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-v20.html. + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package tools.refinery.store.dse.objectives; + +import tools.refinery.store.dse.DesignSpaceExplorationAdapter; + +import java.util.Comparator; + +/** + * + * Implementation of this interface represents a single objective of the DSE problem, which can assess a solution + * (trajectory) in a single number. It has a name and a comparator which orders two solution based on the calculated + * value. + *

+ * Objectives can be either hard or soft objectives. Hard objectives can be satisfied or unsatisfied. If all of the hard + * objectives are satisfied on a single solution, then it is considered to be a valid (or goal) solution. + *

+ * Certain objectives can have inner state for calculating the fitness value. In this case a new instance is necessary + * for every new thread, and the {@code createNew} method should not return the same instance more than once. + * + * @author Andras Szabolcs Nagy + * + */ +public interface Objective { + + /** + * Returns the name of the objective. + * + * @return The name of the objective. + */ + String getName(); + + /** + * Sets the {@link Comparator} which is used to compare fitness (doubles). It determines whether the objective is to + * minimize or maximize (or minimize or maximize a delta from a given number). + * + * @param comparator The comparator. + */ + void setComparator(Comparator comparator); + + /** + * Returns a {@link Comparator} which is used to compare fitness (doubles). It determines whether the objective is + * to minimize or maximize (or minimize or maximize a delta from a given number). + * + * @return The comparator. + */ + Comparator getComparator(); + + /** + * Calculates the value of the objective on a given solution (trajectory). + * + * @param context + * The {@link DesignSpaceExplorationAdapter} + * @return The objective value in double. + */ + Double getFitness(DesignSpaceExplorationAdapter context); + + /** + * Initializes the objective. It is called exactly once for every thread starts. + * + * @param context + * The {@link DesignSpaceExplorationAdapter}. + */ + void init(DesignSpaceExplorationAdapter context); + + /** + * Returns an instance of the {@link Objective}. If it returns the same instance, all the methods has to be thread + * save as they are called concurrently. + * + * @return An instance of the objective. + */ + Objective createNew(); + + /** + * Returns true if the objective is a hard objective. In such a case the method + * {@link Objective#satisfiesHardObjective(Double)} is called. + * + * @return True if the objective is a hard objective. + * @see Objective#satisfiesHardObjective(Double) + * @see Objective + */ + boolean isHardObjective(); + + /** + * Determines if the given fitness value satisfies the hard objective. + * + * @param fitness + * The fitness value of a solution. + * @return True if it satisfies the hard objective or it is a soft constraint. + * @see Objective + */ + boolean satisfiesHardObjective(Double fitness); + +} diff --git a/subprojects/store-dse/src/main/java/tools/refinery/store/dse/objectives/ObjectiveComparatorHelper.java b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/objectives/ObjectiveComparatorHelper.java new file mode 100644 index 00000000..3184b8c4 --- /dev/null +++ b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/objectives/ObjectiveComparatorHelper.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * Copyright (c) 2010-2015, Andras Szabolcs Nagy, Abel Hegedus, Akos Horvath, Zoltan Ujhelyi and Daniel Varro + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-v20.html. + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package tools.refinery.store.dse.objectives; + +import java.util.List; + +/** + * This class is responsible to compare and sort fitness values. + * + * @author AndrĂ¡s Szabolcs Nagy + */ +public class ObjectiveComparatorHelper { + + private final List objectives; + + public ObjectiveComparatorHelper(List objectives) { + this.objectives = objectives; + } + + /** + * Compares two fitnesses based on dominance. Returns -1 if the second parameter {@code o2} is a better + * solution ({@code o2} dominates {@code o1}), 1 if the first parameter {@code o1} is better ({@code o1} dominates + * {@code o2}) and returns 0 if they are non-dominating each other. + */ + public int compare(Fitness o1, Fitness o2) { + + boolean o1HasBetterFitness = false; + boolean o2HasBetterFitness = false; + + for (Objective objective : objectives) { + String objectiveName = objective.getName(); + int sgn = objective.getComparator().compare(o1.get(objectiveName), o2.get(objectiveName)); + + if (sgn < 0) { + o2HasBetterFitness = true; + } + if (sgn > 0) { + o1HasBetterFitness = true; + } + if (o1HasBetterFitness && o2HasBetterFitness) { + break; + } + } + if (o2HasBetterFitness) { + } else if (o1HasBetterFitness) { + return 1; + } + + return 0; + + } +} diff --git a/subprojects/store-dse/src/main/java/tools/refinery/store/dse/strategy/BestFirstStrategy.java b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/strategy/BestFirstStrategy.java new file mode 100644 index 00000000..05cc5bac --- /dev/null +++ b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/strategy/BestFirstStrategy.java @@ -0,0 +1,190 @@ +package tools.refinery.store.dse.strategy; + +import tools.refinery.store.map.Version; +import tools.refinery.store.dse.DesignSpaceExplorationAdapter; +import tools.refinery.store.dse.Strategy; +import tools.refinery.store.dse.internal.Activation; +import tools.refinery.store.dse.objectives.Fitness; +import tools.refinery.store.dse.objectives.ObjectiveComparatorHelper; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.PriorityQueue; + +public class BestFirstStrategy implements Strategy { + + private DesignSpaceExplorationAdapter dseAdapter; + + private int maxDepth; + private boolean backTrackIfSolution = true; + private boolean onlyBetterFirst = false; + + private PriorityQueue trajectoriesToExplore; + + private static class TrajectoryWithFitness { + + public List trajectory; + public Fitness fitness; + + public TrajectoryWithFitness(List trajectory, Fitness fitness) { + super(); + this.trajectory = trajectory; + this.fitness = fitness; + } + + @Override + public String toString() { + return trajectory.toString() + fitness.toString(); + } + + } + + public BestFirstStrategy() { + this(-1); + } + + public BestFirstStrategy(int maxDepth) { + if (maxDepth < 0) { + this.maxDepth = Integer.MAX_VALUE; + } else { + this.maxDepth = maxDepth; + } + } + + public BestFirstStrategy continueIfHardObjectivesFulfilled() { + backTrackIfSolution = false; + return this; + } + + public BestFirstStrategy goOnOnlyIfFitnessIsBetter() { + onlyBetterFirst = true; + return this; + } + + @Override + public void initStrategy(DesignSpaceExplorationAdapter designSpaceExplorationAdapter) { + this.dseAdapter = designSpaceExplorationAdapter; + final ObjectiveComparatorHelper objectiveComparatorHelper = dseAdapter.getObjectiveComparatorHelper(); + + trajectoriesToExplore = new PriorityQueue(11, + (o1, o2) -> objectiveComparatorHelper.compare(o2.fitness, o1.fitness)); + } + + @Override + public void explore() { + final ObjectiveComparatorHelper objectiveComparatorHelper = dseAdapter.getObjectiveComparatorHelper(); + + boolean globalConstraintsAreSatisfied = dseAdapter.checkGlobalConstraints(); + if (!globalConstraintsAreSatisfied) { + // "Global constraint is not satisfied in the first state. Terminate."); + return; + } + + final Fitness firstFitness = dseAdapter.calculateFitness(); + if (firstFitness.isSatisfiesHardObjectives()) { + dseAdapter.newSolution(); + // "First state is a solution. Terminate."); + if (backTrackIfSolution) { + return; + } + } + + if (maxDepth == 0) { + return; + } + + final List firstTrajectory = dseAdapter.getTrajectory(); + TrajectoryWithFitness currentTrajectoryWithFitness = new TrajectoryWithFitness(firstTrajectory, firstFitness); + trajectoriesToExplore.add(currentTrajectoryWithFitness); + + mainLoop: while (true) { + + if (currentTrajectoryWithFitness == null) { + if (trajectoriesToExplore.isEmpty()) { + // "State space is fully traversed."); + return; + } else { + currentTrajectoryWithFitness = trajectoriesToExplore.element(); +// if (logger.isDebugEnabled()) { +// "New trajectory is chosen: " + currentTrajectoryWithFitness); +// } + dseAdapter.restoreTrajectory(currentTrajectoryWithFitness.trajectory); + } + } + + Collection activations = dseAdapter.getUntraversedActivations(); + Iterator iterator = activations.iterator(); + + + + while (iterator.hasNext()) { + final Activation nextActivation = iterator.next(); + if (!iterator.hasNext()) { + // "Last untraversed activation of the state."); + trajectoriesToExplore.remove(currentTrajectoryWithFitness); + } + +// if (logger.isDebugEnabled()) { +// "Executing new activation: " + nextActivation); +// } + dseAdapter.fireActivation(nextActivation); + if (dseAdapter.isCurrentStateAlreadyTraversed()) { + // "The new state is already visited."); + dseAdapter.backtrack(); + } else if (!dseAdapter.checkGlobalConstraints()) { + // "Global constraint is not satisfied."); + dseAdapter.backtrack(); + } else { + final Fitness nextFitness = dseAdapter.calculateFitness(); + if (nextFitness.isSatisfiesHardObjectives()) { + dseAdapter.newSolution(); + // "Found a solution."); + if (backTrackIfSolution) { + dseAdapter.backtrack(); + continue; + } + } + if (dseAdapter.getDepth() >= maxDepth) { + // "Reached max depth."); + dseAdapter.backtrack(); + continue; + } + + TrajectoryWithFitness nextTrajectoryWithFitness = new TrajectoryWithFitness( + dseAdapter.getTrajectory(), nextFitness); + trajectoriesToExplore.add(nextTrajectoryWithFitness); + + int compare = objectiveComparatorHelper.compare(currentTrajectoryWithFitness.fitness, + nextTrajectoryWithFitness.fitness); + if (compare < 0) { + // "Better fitness, moving on: " + nextFitness); + currentTrajectoryWithFitness = nextTrajectoryWithFitness; + continue mainLoop; + } else if (compare == 0) { + if (onlyBetterFirst) { + // "Equally good fitness, backtrack: " + nextFitness); + dseAdapter.backtrack(); + continue; + } else { + // "Equally good fitness, moving on: " + nextFitness); + currentTrajectoryWithFitness = nextTrajectoryWithFitness; + continue mainLoop; + } + } else { + // "Worse fitness."); + currentTrajectoryWithFitness = null; + continue mainLoop; + } + } + } + + // "State is fully traversed."); + trajectoriesToExplore.remove(currentTrajectoryWithFitness); + currentTrajectoryWithFitness = null; + + } + // "Interrupted."); + + } +} diff --git a/subprojects/store-dse/src/main/java/tools/refinery/store/dse/strategy/DepthFirstStrategy.java b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/strategy/DepthFirstStrategy.java new file mode 100644 index 00000000..42985013 --- /dev/null +++ b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/strategy/DepthFirstStrategy.java @@ -0,0 +1,108 @@ +package tools.refinery.store.dse.strategy; + +import tools.refinery.store.dse.DesignSpaceExplorationAdapter; +import tools.refinery.store.dse.Strategy; +import tools.refinery.store.dse.internal.Activation; +import tools.refinery.store.dse.objectives.Fitness; + +import java.util.Collection; + +public class DepthFirstStrategy implements Strategy { + + private DesignSpaceExplorationAdapter dseAdapter; + + private int maxDepth; + private boolean backTrackIfSolution = true; + + public DepthFirstStrategy() { + this(-1); + } + + public DepthFirstStrategy(int maxDepth) { + if (maxDepth < 0) { + this.maxDepth = Integer.MAX_VALUE; + } else { + this.maxDepth = maxDepth; + } + } + + public DepthFirstStrategy continueIfHardObjectivesFulfilled() { + backTrackIfSolution = false; + return this; + } + + @Override + public void initStrategy(DesignSpaceExplorationAdapter designSpaceExplorationAdapter) { + this.dseAdapter = designSpaceExplorationAdapter; + } + + @Override + public void explore() { + mainloop: while (true) { + var globalConstraintsAreSatisfied = dseAdapter.checkGlobalConstraints(); + if (!globalConstraintsAreSatisfied) { + var isSuccessfulUndo = dseAdapter.backtrack(); + if (!isSuccessfulUndo) { +// "Global constraint is not satisfied and cannot backtrack." + break; + } + else { +// "Global constraint is not satisfied, backtrack." + continue; + } + } + + Fitness fitness = dseAdapter.calculateFitness(); + if (fitness.isSatisfiesHardObjectives()) { + dseAdapter.newSolution(); + if (backTrackIfSolution) { + var isSuccessfulUndo = dseAdapter.backtrack(); + if (!isSuccessfulUndo) { +// "Found a solution but cannot backtrack." + break; + } else { +// "Found a solution, backtrack." + continue; + } + } + } + + var depth = dseAdapter.getDepth(); + if (dseAdapter.getDepth() >= maxDepth) { + var isSuccessfulUndo = dseAdapter.backtrack(); + if (!isSuccessfulUndo) { +// "Reached max depth but cannot backtrack." + break; + } + } + + Collection activations; + do { + activations = dseAdapter.getUntraversedActivations(); + if (activations.isEmpty()) { + if (!dseAdapter.backtrack()) { + // "No more transitions from current state and cannot backtrack." + break mainloop; + } + else { + // "No more transitions from current state, backtrack." + continue; + } + } + } while (activations.isEmpty()); + + dseAdapter.fireRandomActivation(); +// if (dseAdapter.isCurrentInTrajectory()) { +// if (!dseAdapter.backtrack()) { +//// TODO: throw exception +//// "The new state is present in the trajectory but cannot backtrack. Should never happen!" +// break; +// } +// else { +//// "The new state is already visited in the trajectory, backtrack." +// continue; +// } +// } + } + } +} -- cgit v1.2.3-54-g00ecf