package splar.plugins.configuration.sat.sat4j;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.sat4j.core.VecInt;
import org.sat4j.minisat.core.Solver;
import org.sat4j.specs.ContradictionException;
import splar.core.fm.FTTraversalNodeSelector;
import splar.core.fm.FTTraversals;
import splar.core.fm.FeatureGroup;
import splar.core.fm.FeatureModel;
import splar.core.fm.FeatureTreeNode;
import splar.core.fm.configuration.ConfigurationEngine;
import splar.core.fm.configuration.ConfigurationEngineException;
import splar.core.fm.configuration.ConfigurationStep;
import splar.plugins.reasoners.sat.sat4j.FMReasoningWithSAT;
import splar.plugins.reasoners.sat.sat4j.StaticVariableOrderSAT;
public class SATConfigurationEngine extends ConfigurationEngine {
protected FMReasoningWithSAT reasoner; // SAT solver
public SATConfigurationEngine(String featureModelURL) throws ConfigurationEngineException {
super(featureModelURL);
reasoner = null;
// enforces that model is satisfiable
try {
// model must be satisfiable otherwise raise exception
FMReasoningWithSAT tempReasoner = new FMReasoningWithSAT("MiniSAT", model, 60000);
tempReasoner.init();
if ( !tempReasoner.isConsistent() ) {
throw new ConfigurationEngineException("Model is inconsistent and thus cannot be configured ");
}
}
catch (ContradictionException contExc) {
throw new ConfigurationEngineException("Model is inconsistent and thus cannot be configured ");
}
catch (Exception e) {
throw new ConfigurationEngineException("Problems loading model. Location might be wrong or model does not follow SXFM specification");
}
}
void addClauseToSolver(FMReasoningWithSAT reasoner, String decisionVar, int decisionValue ) throws Exception {
// Add user decision as a clause to the SAT problem
Solver satSolver = (Solver)reasoner.getSolver();
int varIndex = reasoner.getVariableIndex(decisionVar);
VecInt vectInt = new VecInt(1);
vectInt.push(decisionValue == 1 ? varIndex : -varIndex);
satSolver.addClause(vectInt);
}
/******************************************************************************************************
* RESET
*******************************************************************************************************/
protected synchronized ConfigurationStep resetConfiguration() throws ConfigurationEngineException {
ConfigurationStep newConfStep = null;
try {
reasoner = createSATReasoner(model);
createConfigurationStep(model.getRoot().getID(), 1, "propagated");
}
catch (Exception e) {
e.printStackTrace();
throw new ConfigurationEngineException("Problems reseting configuration", e);
}
return getLastStep();
}
/******************************************************************************************************
* UNDO
*******************************************************************************************************/
public List<ConfigurationStep> undo(int undoStep) throws ConfigurationEngineException {
try {
List<ConfigurationStep> undoneSteps = super.undo(undoStep);
reasoner = createSATReasoner(model);
return undoneSteps;
}
catch (ConfigurationEngineException e1) {
throw e1;
}
catch (Exception e2) {
throw new ConfigurationEngineException("Problems undoing configuration step " + undoStep, e2);
}
}
/******************************************************************************************************
* CREATE SAT REASONER
*******************************************************************************************************/
protected FMReasoningWithSAT createSATReasoner(FeatureModel model) throws Exception {
FMReasoningWithSAT satReasoner = new FMReasoningWithSAT("MiniSAT", model, 60000);
satReasoner.init();
return satReasoner;
}
/******************************************************************************************************
* AUTO COMPLETE
*******************************************************************************************************/
public synchronized ConfigurationStep autoComplete(boolean valueOrder) throws ConfigurationEngineException {
ConfigurationStep newConfStep = null;
try {
int curConfStep = steps.size()+1;
// save feature model state prior to performing the configuration step
model.saveState("state_step" + curConfStep);
String satVarOrder[] = new String[model.countFeatures()];//featureModel.countFeatures()];
FTTraversalNodeSelector selector = new FTTraversalNodeSelector() {
public boolean select(FeatureTreeNode node) {
if ( node instanceof FeatureGroup )
return false;
return true;
}
};
int index=0;
for( FeatureTreeNode node : FTTraversals.dfs(model.getRoot(), selector)) {
satVarOrder[index++] = node.getID();
}
StaticVariableOrderSAT satOrderObj = new StaticVariableOrderSAT(satVarOrder, valueOrder, reasoner.getVarName2IndexMap(), reasoner.getVarIndex2NameMap());
reasoner.setVariableOrderObject(satOrderObj);
Solver satSolver = (Solver)reasoner.getSolver();
if( satSolver.isSatisfiable() ) {
long time = System.currentTimeMillis();
int[] solution = satSolver.model();
time = System.currentTimeMillis() - time;
// Create new configuration step
newConfStep = new ConfigurationStep(""+curConfStep);
// newConfStep.setDecisionFeature(null);
for( int value : solution ) {
String featureId = reasoner.getVariableName(Math.abs(value));
FeatureTreeNode completedFeature = model.getNodeByID(featureId);
if ( !completedFeature.isInstantiated() ) {
// assign value to feature
model.assignValue(completedFeature, value > 0 ? 1 : 0);
// add feature properties
completedFeature.setProperty("decisionStep", ""+curConfStep);
completedFeature.setProperty("decisionType", "auto-completion");
newConfStep.addPropagatedFeature(completedFeature);
}
}
// Add stats data to step
newConfStep.addAttribute("step_Stat", "1");
newConfStep.addAttribute("step_runTime", ""+time);
ConfigurationStep.computeStepAttributes(newConfStep, steps, model);
steps.add(newConfStep);
}
else {
throw new ConfigurationEngineException("Problems autocompleting configuration: isSatisfiable()");
}
}
catch (Exception e) {
e.printStackTrace();
throw new ConfigurationEngineException("Problems autocompleting configuration", e);
}
return newConfStep;
}
/******************************************************************************************************
* Utility method: Create a configuration step for a given <feature,decision> pair
* Returns the valid domains table for the created configuration step
*******************************************************************************************************/
@Override
protected Map<String,Boolean[]> createConfigurationStep( String featureId, int featureValue, String decisionType ) throws Exception {
Map<String,Boolean[]> domainTable = null;
try {
addClauseToSolver(reasoner, featureId, featureValue);
domainTable = super.createConfigurationStep(featureId, featureValue, decisionType);
// Add stats data to step
ConfigurationStep newConfStep = getLastStep();
newConfStep.addAttribute("step_Stat", satStats.get("sat-checks"));
newConfStep.addAttribute("step_runTime", satStats.get("processing-time"));
} catch (Exception e) {
throw e;
// TODO: handle exception
}
return domainTable;
}
/******************************************************************************************************
* COMPUTE VALID DOMAINS
*******************************************************************************************************/
Map<String,String> satStats = null;
protected Map<String,Boolean[]> computeValidDomains() throws ConfigurationEngineException{
try {
if ( satStats == null ) {
satStats = new HashMap<String,String>();
}
else {
satStats.clear();
}
return reasoner.allValidDomains(satStats);
}
catch (Exception e) {
e.printStackTrace();
throw new ConfigurationEngineException("Problems computing valid domains for SAT: " + e.getMessage());
}
}
/******************************************************************************************************
* GET VARIABLE NAME
*******************************************************************************************************/
protected String getVariableName(int varIndex) {
return reasoner.getVariableName(varIndex+1);
}
/******************************************************************************************************
* GET VARIABLE INDEX
*******************************************************************************************************/
protected int getVariableIndex(String varName) {
return reasoner.getVariableIndex(varName);
}
/******************************************************************************************************
* DETECT CONFLICTS
*******************************************************************************************************/
public synchronized List<FeatureTreeNode> detectConflicts(String featureId) throws ConfigurationEngineException {
// only features manually decided are considered
List<FeatureTreeNode> conflictingFeatures = new LinkedList<FeatureTreeNode>();
try {
model.saveState("detect_conflicts");
// identify data about feature that will be toggled
FeatureTreeNode toggleFeatureNode = model.getNodeByID(featureId);
if ( !toggleFeatureNode.isInstantiated() ) {
throw new ConfigurationEngineException("Cannot toggle the value of an uninstantiated feature");
}
if ( toggleFeatureNode.isImmutable() ) {
throw new ConfigurationEngineException("Cannot toggle the value of an immutable feature");
}
int toggleFeatureOriginalValue = toggleFeatureNode.getValue();
int toggleFeatureStep = Integer.valueOf((String)toggleFeatureNode.getProperty("decisionStep"));
// creates list of relevant decisions starting with the toggle feature and followed by other manual decisions
Map<String,Integer> newDecisionSequence = new LinkedHashMap<String,Integer>();
newDecisionSequence.put(toggleFeatureNode.getID(), 1-toggleFeatureOriginalValue);
// relevantDecisions.add(toggleFeatureNode);
// add manual decisions to list of relevant decisions (decisions that should be kept)
// and remove respective configuration steps
for( int i = toggleFeatureStep-1 ; i < steps.size() ; i++ ) {
for( FeatureTreeNode manualDecision : steps.get(i).getDecisions() ) {
if ( !manualDecision.equals(toggleFeatureNode) ) {
newDecisionSequence.put(manualDecision.getID(), manualDecision.getValue());
// relevantDecisions.add(manualDecision);
}
}
}
// restore model to state immediately before toggle feature original decision
// and creates SAT solver
model.restoreState("state_step" + toggleFeatureStep, false);
FMReasoningWithSAT tempReasoner = createSATReasoner(model);
// check features that conflict with new decision sequence
int index = 0;
String [] newDecisionSequenceArray = newDecisionSequence.keySet().toArray(new String[0]);
for( String decisionNodeId : newDecisionSequenceArray ) {
int decisionNodeValue = newDecisionSequence.get(decisionNodeId);
// skip previously-detected conflicting nodes (-1)
if ( decisionNodeValue != -1 ) {
// new decision
addClauseToSolver(tempReasoner, decisionNodeId, newDecisionSequence.get(decisionNodeId));
// Compute valid domains
Map<String,String> satStats = new HashMap<String,String>();
Map<String,Boolean[]> domainTable = tempReasoner.allValidDomains(satStats);
// try to find conflicts
for( int i = index+1 ; i < newDecisionSequenceArray.length ; i++ ) {
String varName = newDecisionSequenceArray[i];
int varValue = newDecisionSequence.get(varName);
Boolean domain[] = domainTable.get(varName);
// node can only be true (or false) which conflicts with its current value
if ( (domain.length == 1 && domain[0] == true && varValue == 0) ||
(domain.length == 1 && domain[0] == false && varValue == 1) ) {
// eliminate node from further processing
newDecisionSequence.put(varName, -1); // eliminate node from further processing
// add inverse of node value as clause to the to SAT solver
addClauseToSolver(tempReasoner, varName, 1-varValue);
// add node to conflict list
conflictingFeatures.add(model.getNodeByID(varName));
}
// node can only be true (or false) which coincides with its current value
else if ( (domain.length == 1 && domain[0] == true && varValue == 1) ||
(domain.length == 1 && domain[0] == false && varValue == 0) ) {
// eliminate node from further processing
newDecisionSequence.put(varName, -1);
// add node value as clause to the to SAT solver
addClauseToSolver(tempReasoner, varName, varValue);
}
// // node can only be true (or false) which conflicts with its current value
// if ( (entry[0] == FTReasoningWithSAT.YES && entry[1] == FTReasoningWithSAT.NO && varValue == 1) ||
// (entry[0] == FTReasoningWithSAT.NO && entry[1] == FTReasoningWithSAT.YES && varValue == 0) ) {
// // eliminate node from further processing
// newDecisionSequence.put(varName, -1); // eliminate node from further processing
// // add inverse of node value as clause to the to SAT solver
// addClauseToSolver(tempReasoner, varName, 1-varValue);
// // add node to conflict list
// conflictingFeatures.add(model.getNodeByID(varName));
// }
// // node can only be true (or false) which coincides with its current value
// else if ( (entry[0] == FTReasoningWithSAT.YES && entry[1] == FTReasoningWithSAT.NO && varValue == 0) ||
// (entry[0] == FTReasoningWithSAT.NO && entry[1] == FTReasoningWithSAT.YES && varValue == 1)) {
// // eliminate node from further processing
// newDecisionSequence.put(varName, -1);
// // add node value as clause to the to SAT solver
// addClauseToSolver(tempReasoner, varName, varValue);
// }
}
}
index++;
}
}
catch (Exception e) {
e.printStackTrace();
throw new ConfigurationEngineException("Problems configuring model: " + e.getMessage());
}
finally {
model.restoreState("detect_conflicts", true);
}
return conflictingFeatures;
}
/******************************************************************************************************
* TOGGLE DECISION
*******************************************************************************************************/
public synchronized List<ConfigurationStep> toggleDecision(String featureId) throws ConfigurationEngineException {
// only features manually decided are considered
List<ConfigurationStep> newConfSteps = new LinkedList<ConfigurationStep>();
try {
model.saveState("toggle_decision");
// identify data about feature that will be toggled
FeatureTreeNode toggleFeatureNode = model.getNodeByID(featureId);
if ( !toggleFeatureNode.isInstantiated() ) {
throw new ConfigurationEngineException("Cannot toggle the value of an uninstantiated feature");
}
if ( toggleFeatureNode.isImmutable() ) {
throw new ConfigurationEngineException("Cannot toggle the value of an immutable feature");
}
int toggleFeatureOriginalValue = toggleFeatureNode.getValue();
int toggleFeatureStep = Integer.valueOf((String)toggleFeatureNode.getProperty("decisionStep"));
// creates list of relevant decisions starting with the toggle feature and followed by other manual decisions
Map<String,Integer> newDecisionSequence = new LinkedHashMap<String,Integer>();
newDecisionSequence.put(toggleFeatureNode.getID(), 1-toggleFeatureOriginalValue);
// add manual decisions to list of relevant decisions (decisions that should be kept)
// and remove respective configuration steps
int initialStep = toggleFeatureStep-1;
while( steps.size() > initialStep ) {
for( FeatureTreeNode manualDecision : steps.get(initialStep).getDecisions() ) {
if ( !manualDecision.equals(toggleFeatureNode) ) {
newDecisionSequence.put(manualDecision.getID(), manualDecision.getValue());
}
}
// remove step from steps list
steps.remove(initialStep);
}
// restore model to state immediately before toggle feature's original decision
// and creates SAT solver to reflect that state
model.restoreState("state_step" + toggleFeatureStep, false);
reasoner = createSATReasoner(model);
// applies sequence of relevant decisions and creates new configuration steps
int index = 0;
String [] newDecisionSequenceArray = newDecisionSequence.keySet().toArray(new String[0]);
for( String decisionNodeId : newDecisionSequenceArray ) {
int decisionNodeValue = newDecisionSequence.get(decisionNodeId);
// skip previously-detected conflicting nodes (-1)
if ( decisionNodeValue != -1 ) {
Map<String,Boolean[]> domainTable = createConfigurationStep(decisionNodeId, decisionNodeValue, "manual");
ConfigurationStep newConfStep = getLastStep();
newConfSteps.add(newConfStep);
// try to find conflicts
for( int i = index+1 ; i < newDecisionSequenceArray.length ; i++ ) {
String varName = newDecisionSequenceArray[i];
int varValue = newDecisionSequence.get(varName);
Boolean domain[] = domainTable.get(varName);
// new and old values for this node CONFLICTS - remove node from being processed
if ( (domain.length == 1 && domain[0] == true && varValue == 0) ||
(domain.length == 1 && domain[0] == false && varValue == 1) ) {
// eliminate node from further processing
newDecisionSequence.put(varName, -1); // eliminate node from further processing
}
// new and old values for this node are the SAME - add node to configuration step as decision
else if ( (domain.length == 1 && domain[0] == true && varValue == 1) ||
(domain.length == 1 && domain[0] == false && varValue == 0)) {
// eliminate node from further processing
newDecisionSequence.put(varName, -1);
// found out that a given propagation is actually a manual feature of a future step
// add propagated feature as a manual decision in the step
FeatureTreeNode propagatedFeature = model.getNodeByID(varName);
newConfStep.removePropagatedDecision(propagatedFeature);
propagatedFeature.setProperty("decisionType", "manual");
newConfStep.addManualDecisionFeature(propagatedFeature);
}
else {
System.out.println(">> " + varName);
}
}
}
index++;
}
}
catch (Exception e) {
model.restoreState("toggle_decision", true);
e.printStackTrace();
throw new ConfigurationEngineException("Problems toggling feature value: " + e.getMessage());
}
return newConfSteps;
}
}