/** * */ package org.js.model.feature.csp; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.log4j.Logger; import org.eclipse.emf.common.util.EList; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.util.EcoreUtil; import org.js.model.feature.Attribute; import org.js.model.feature.AttributeConstraint; import org.js.model.feature.AttributeOperand; import org.js.model.feature.AttributeReference; import org.js.model.feature.AttributeValue; import org.js.model.feature.DiscreteDomain; import org.js.model.feature.Domain; import org.js.model.feature.DomainValue; import org.js.model.feature.Exclude; import org.js.model.feature.Feature; import org.js.model.feature.FeatureConstraint; import org.js.model.feature.FeatureModel; import org.js.model.feature.FeatureState; import org.js.model.feature.Group; import org.js.model.feature.Imply; import org.js.model.feature.Interval; import org.js.model.feature.NumericalDomain; import org.js.model.feature.Relop; import org.js.model.feature.edit.FeatureModelHelper; import choco.Choco; import choco.cp.model.CPModel; import choco.kernel.model.Model; import choco.kernel.model.constraints.Constraint; import choco.kernel.model.variables.integer.IntegerExpressionVariable; import choco.kernel.model.variables.integer.IntegerVariable; /** * Class to translate an attributed group cardinality based feature model into a constraint satisfaction problem. * * @author <a href="mailto:julia.schroeter@tu-dresden.de">Julia Schroeter</a> * */ public class TranslateFM2CSP { private static Logger log = Logger.getLogger(TranslateFM2CSP.class); public static String attributeDelimiter = "~"; public static String attributeValue = "value"; public static String attributeEnablement = "enablement"; public static int attributeDisabled = -1; private CPModel cspModel; private Map<String, IntegerVariable> nodeVariables = new HashMap<String, IntegerVariable>(); private Map<String, int[]> discreteDomainValues = new HashMap<String, int[]>(); public CPModel getModel() { if (cspModel == null) { cspModel = new CPModel(); } return cspModel; } /** * Transform a cardinality-based feature model into a constraint model. * * @param featuremodel */ public Model getCSPModel(FeatureModel featuremodel) { Feature rootFeature = featuremodel.getRoot(); // according to Benavides2005 transform fm into csp model transformFeature(rootFeature); transformConstraints(featuremodel); return cspModel; } private void transformFeature(Feature feature) { createFeatureConstraint(feature); EList<Group> groups = feature.getGroups(); for (Group group : groups) { transformGroup(group); } EList<Attribute> attributes = feature.getAttributes(); for (Attribute attribute : attributes) { transformAttribute(attribute); // createAbstractAttributeConstraint(attribute); } } private void transformAttribute(Attribute attribute) { // 0: attribute is disabled if feature is disabled // 1: attribute is enabled if feature is selected IntegerVariable featureVariable = getOrCreateVariable(attribute.getFeature()); Constraint memberConstraint = getMemberConstraint(attribute); // if feature is enabled, then attribute value must be in bounds Constraint featureEnabled = Choco.eq(featureVariable, 1); Constraint checkAttribute = Choco.implies(featureEnabled, memberConstraint); getModel().addConstraint(checkAttribute); IntegerVariable attributeVariable = getOrCreateVariable(attribute); Constraint featureDisabled = Choco.eq(featureVariable, 0); Constraint attrDisabled = Choco.eq(attributeVariable, attributeDisabled); Constraint disabledAttr = Choco.ifOnlyIf(featureDisabled, attrDisabled); getModel().addConstraint(disabledAttr); } private Constraint getMemberConstraint(Attribute attribute) { Constraint memberConstraint = null; IntegerVariable attributeValueVariable = getOrCreateVariable(attribute); // if attribute value is set, then the according attribute value must be // set if (FeatureModelHelper.isAttributeValueSet(attribute)) { int value = FeatureModelHelper.getAttributeValue(attribute); memberConstraint = Choco.eq(value, attributeValueVariable); } else { // check domain values Domain domain = attribute.getDomain(); if (domain instanceof NumericalDomain) { NumericalDomain numericalDomain = (NumericalDomain) domain; // value of attribute must be in one of the intervals. int[] values = getNumericalValues(numericalDomain, attribute); memberConstraint = Choco.member(attributeValueVariable, values); } else if (domain instanceof DiscreteDomain) { DiscreteDomain discreteDomain = (DiscreteDomain) domain; int[] domainValues = discreteDomainValues.get(discreteDomain.getId()); memberConstraint = Choco.member(attributeValueVariable, domainValues); } } return memberConstraint; } private int[] getNumericalValues(NumericalDomain numericalDomain, Attribute attribute) { EList<Interval> intervals = numericalDomain.getIntervals(); Set<Integer> domainValues = new HashSet<Integer>(); for (Interval interval : intervals) { int lowerBound = interval.getLowerBound(); int upperBound = interval.getUpperBound(); int diff = upperBound - lowerBound; int current = lowerBound; for (int i = 0; i <= diff; i++) { domainValues.add(Integer.valueOf(current)); current++; } } // remove deselected domain values removeDeselectedDomainValues(attribute, domainValues); int[] values = new int[domainValues.size()]; int i = 0; for (Integer v : domainValues) { values[i++] = v.intValue(); } return values; } private String concatenate(String[] strings, String delimiter) { StringBuffer buffer = new StringBuffer(); for (int i = 0; i < strings.length; i++) { String string = strings[i]; if (buffer.length() > 0) { buffer.append(delimiter); } buffer.append(string); } return buffer.toString(); } private IntegerVariable getOrCreateVariable(Attribute attribute) { String attributeName = attribute.getName(); String featureId = attribute.getFeature().getId(); String identifier = attributeValue; String[] parts = new String[] { identifier, featureId, attributeName }; String attributeId = concatenate(parts, attributeDelimiter); IntegerVariable integerVariable = nodeVariables.get(attributeId); if (integerVariable == null) { integerVariable = createAttributeValueVariable(attribute, attributeId); nodeVariables.put(attributeId, integerVariable); } return integerVariable; } private IntegerVariable createAttributeValueVariable(Attribute attribute, String attributeId) { // String attrOptions = "cp:no_decision"; String attrOptions = ""; IntegerVariable attributeVariable = null; // (1) if attribute value is set, then attribute can either be this value or the disabled attribute value if (FeatureModelHelper.isAttributeValueSet(attribute)) { int value = FeatureModelHelper.getAttributeValue(attribute); int[] attributeValues = new int[] { value, attributeDisabled }; attributeVariable = Choco.makeIntVar(attributeId, attributeValues, attrOptions); } else { Domain attributeDomain = attribute.getDomain(); // (2) if attribute domain is discrete if (attributeDomain instanceof DiscreteDomain) { DiscreteDomain discreteDomain = (DiscreteDomain) attributeDomain; int[] domainValues = getDomainValues(attribute, discreteDomain); attributeVariable = Choco.makeIntVar(attributeId, domainValues, attrOptions); // todo: check numerical domain and use intervals instead // (3) if attribute domain is integer } else if (attributeDomain instanceof NumericalDomain) { NumericalDomain numericalDomain = (NumericalDomain) attributeDomain; int lowestBoundofNumericalDomain = getLowestBoundofNumericalDomain(numericalDomain); int highestBoundofNumericalDomain = getHighestBoundofNumericalDomain(numericalDomain); if (lowestBoundofNumericalDomain > attributeDisabled) { lowestBoundofNumericalDomain = attributeDisabled; } attributeVariable = Choco.makeIntVar(attributeId, lowestBoundofNumericalDomain, highestBoundofNumericalDomain, attrOptions); } } return attributeVariable; } private void removeDeselectedDomainValues(Attribute attribute, Set<Integer> values) { EList<String> deselectedDomainValues = attribute.getDeselectedDomainValues(); Domain domain = attribute.getDomain(); for (String valueString : deselectedDomainValues) { int deselectedValue = FeatureModelHelper.getDomainValueForString(valueString, domain); Integer deselected = Integer.valueOf(deselectedValue); values.remove(deselected); } } private int[] getDomainValues(Attribute attribute, DiscreteDomain discreteDomain) { int[] domainValues = getOrCreateDomainValues(discreteDomain); Set<Integer> values = new HashSet<Integer>(domainValues.length); for (int i = 0; i < domainValues.length; i++) { values.add(Integer.valueOf(domainValues[i])); } // if domain values are deselected, remove them from domain array removeDeselectedDomainValues(attribute, values); // insert Integer representing disabled attribute values.add(attributeDisabled); // convert arraylist back to array domainValues = new int[values.size()]; int i = 0; for (Integer v : values) { domainValues[i++] = v.intValue(); } return domainValues; } private void transformGroup(Group group) { createGroupConstraint(group); EList<Feature> childFeatures = group.getChildFeatures(); for (Feature feature : childFeatures) { transformFeature(feature); } } private void transformConstraints(FeatureModel model) { EList<org.js.model.feature.Constraint> constraints = model.getConstraints(); for (org.js.model.feature.Constraint constraint : constraints) { Constraint chocoConstraint = createCrossTreeConstraint(constraint); if (chocoConstraint != null) { getModel().addConstraint(chocoConstraint); } } } private Constraint createFeatureConstraint(FeatureConstraint featureConstraint) { Constraint result = null; Feature leftFeature = featureConstraint.getLeftOperand(); Feature rightFeature = featureConstraint.getRightOperand(); if (featureConstraint instanceof Imply) { result = createRequiresConstraint(leftFeature, rightFeature); } else if (featureConstraint instanceof Exclude) { result = createExcludesConstraint(leftFeature, rightFeature); } return result; } private Constraint createAttributeConstraint(AttributeConstraint attributeConstraint) { Constraint result = null; Constraint constraint = null; AttributeOperand leftOperand = attributeConstraint.getLeftOperand(); IntegerVariable leftOperandVariable = getVariableForAttributeOperand(leftOperand); AttributeOperand rightOperand = attributeConstraint.getRightOperand(); IntegerVariable rightOperandVariable = getVariableForAttributeOperand(rightOperand); Relop operator = attributeConstraint.getOperator(); switch (operator) { case EQUAL: constraint = Choco.eq(leftOperandVariable, rightOperandVariable); break; case GREATER_THAN: constraint = Choco.gt(leftOperandVariable, rightOperandVariable); break; case GREATER_THAN_OR_EQUAL: constraint = Choco.geq(leftOperandVariable, rightOperandVariable); break; case LESS_THAN: constraint = Choco.lt(leftOperandVariable, rightOperandVariable); break; case LESS_THAN_OR_EQUAL: constraint = Choco.leq(leftOperandVariable, rightOperandVariable); break; case UNEQUAL: constraint = Choco.neq(leftOperandVariable, rightOperandVariable); break; default: break; } IntegerVariable leftFeatureVariable = getFeatureVariableForOperand(leftOperand); IntegerVariable rightFeatureVariable = getFeatureVariableForOperand(rightOperand); // if both are features and both are selected, then the attribute constraint must be evaluated if (leftFeatureVariable != null && rightFeatureVariable != null) { Constraint leftFeatureSelected = Choco.eq(leftFeatureVariable, 1); Constraint rightFeatureSelected = Choco.eq(rightFeatureVariable, 1); Constraint and = Choco.and(leftFeatureSelected, rightFeatureSelected); result = Choco.implies(and, constraint); } else { // if one of the two operand is a constant,the constraint will be evaluated if the host feature is selected if (leftFeatureVariable != null){ result = getFeatureAttributeValueConstraint(leftFeatureVariable, constraint); } else if (rightFeatureVariable != null){ result = getFeatureAttributeValueConstraint(rightFeatureVariable, constraint); } } return result; } private Constraint getFeatureAttributeValueConstraint(IntegerVariable featureVariable, Constraint valueConstraint){ Constraint result = null; Constraint featureSelected = Choco.eq(featureVariable, 1); Constraint implies = Choco.implies(featureSelected, valueConstraint); result = Choco.implies(featureSelected, implies); return result; } private IntegerVariable getFeatureVariableForOperand(AttributeOperand operand) { IntegerVariable variable = null; Feature feature = FeatureModelHelper.getAttributeOperandFeature(operand); if (feature != null) { variable = getOrCreateVariable(feature); } return variable; } private Constraint createCrossTreeConstraint(org.js.model.feature.Constraint constraint) { Constraint result = null; if (constraint instanceof FeatureConstraint) { FeatureConstraint featureConstraint = (FeatureConstraint) constraint; result = createFeatureConstraint(featureConstraint); } else if (constraint instanceof AttributeConstraint) { AttributeConstraint attributeConstraint = (AttributeConstraint) constraint; result = createAttributeConstraint(attributeConstraint); } return result; } private IntegerVariable getVariableForAttributeOperand(AttributeOperand operand) { IntegerVariable variable = null; if (operand instanceof AttributeReference) { AttributeReference attRef = (AttributeReference) operand; Attribute attribute = attRef.getAttribute(); variable = getOrCreateVariable(attribute); } else if (operand instanceof AttributeValue) { AttributeValue value = (AttributeValue) operand; int valueInt = value.getInt(); String valueName = value.getName(); if (valueName != null && valueName.length() > 0) { AttributeOperand other = getOtherOperand(operand); if (other instanceof AttributeReference) { AttributeReference attRef = (AttributeReference) other; Domain domain = attRef.getAttribute().getDomain(); if (domain instanceof DiscreteDomain) { DiscreteDomain discreteDomain = (DiscreteDomain) domain; EList<DomainValue> values = discreteDomain.getValues(); for (DomainValue domainValue : values) { String name = domainValue.getName(); if (valueName.equals(name)) { valueInt = domainValue.getInt(); break; } } } else if (domain instanceof NumericalDomain) { valueInt = Integer.decode(valueName); } } } variable = Choco.constant(valueInt); } return variable; } private AttributeOperand getOtherOperand(AttributeOperand operand) { AttributeOperand other = null; EObject container = operand.eContainer(); if (container instanceof AttributeConstraint) { AttributeConstraint attConstraint = (AttributeConstraint) container; AttributeOperand leftOperand = attConstraint.getLeftOperand(); AttributeOperand rightOperand = attConstraint.getRightOperand(); if (EcoreUtil.equals(operand, leftOperand)) { other = rightOperand; } else { other = leftOperand; } } return other; } private Constraint createExcludesConstraint(Feature lf, Feature rf) { IntegerVariable leftFeature = getOrCreateVariable(lf); IntegerVariable rightFeature = getOrCreateVariable(rf); Constraint leftSelected = Choco.gt(leftFeature, 0); Constraint rightNotSelected = Choco.eq(rightFeature, 0); Constraint leftRight = Choco.implies(leftSelected, rightNotSelected); Constraint rightSelected = Choco.gt(rightFeature, 0); Constraint leftNotSelected = Choco.eq(leftFeature, 0); Constraint rightLeft = Choco.implies(rightSelected, leftNotSelected); Constraint excludeConstraint = Choco.or(leftRight, rightLeft); return excludeConstraint; } private Constraint createRequiresConstraint(Feature lf, Feature rf) { IntegerVariable leftFeature = getOrCreateVariable(lf); IntegerVariable rightFeature = getOrCreateVariable(rf); Constraint leftConstraint = Choco.gt(leftFeature, 0); Constraint rightConstraint = Choco.gt(rightFeature, 0); Constraint impliesConstraint = Choco.implies(leftConstraint, rightConstraint); return impliesConstraint; } private void createFeatureConstraint(Feature feature) { IntegerVariable childVariable = getOrCreateVariable(feature); int minCardinality = 0; int maxCardinality = 1; Constraint greaterThan = Choco.geq(childVariable, minCardinality); Constraint smallerThan = Choco.leq(childVariable, maxCardinality); Constraint thenConstraint = Choco.and(greaterThan, smallerThan); EObject featureContainer = feature.eContainer(); if (featureContainer instanceof Group) { Group parentGroup = (Group) featureContainer; EObject groupContainer = parentGroup.eContainer(); if (groupContainer instanceof Feature) { Feature parentFeature = (Feature) groupContainer; IntegerVariable parentVariable = getOrCreateVariable(parentFeature); // feature value must be in feature cardinality boundaries Constraint parentSelected = Choco.gt(parentVariable, 0); Constraint parentSelectedAndChildCardinality = Choco.implies(parentSelected, thenConstraint); getModel().addConstraint(parentSelectedAndChildCardinality); Constraint childSelected = Choco.gt(childVariable, 0); Constraint impliesConstraint = Choco.implies(childSelected, parentSelected); getModel().addConstraint(impliesConstraint); } } else { // handle rootgroup Constraint greater = Choco.gt(childVariable, minCardinality); getModel().addConstraint(greater); } } private void createGroupConstraint(Group group) { // group with cardinality {n,m} represented as // ifThen(ParentFeature>0;sum(ChildFeature A, ChildFeature // B) in {n,m};) // if group cardinality is n=m, then // ifThen(ParentFeature>0;sum(ChildFeature A, ChildFeature B) = n) log.debug("Create constraint for group " + group); IntegerVariable parentFeatureVariable = getOrCreateVariable((Feature) group.eContainer()); IntegerExpressionVariable childFeatureSum = createChildFeatureVariable(group); int minCardinality = getMinChocoCardinality(group); int maxCardinality = getMaxChocoCardinality(group); Constraint ifConstraint = Choco.gt(parentFeatureVariable, 0); Constraint greaterThan = Choco.geq(childFeatureSum, minCardinality); Constraint smallerThan = Choco.leq(childFeatureSum, maxCardinality); Constraint thenConstraint = Choco.and(greaterThan, smallerThan); Constraint groupCardinalityConstraint = Choco.implies(ifConstraint, thenConstraint); getModel().addConstraint(groupCardinalityConstraint); } private int getMaxChocoCardinality(Group group) { int maxCardinality = group.getMaxCardinality(); int cardinality = maxCardinality; if (maxCardinality == -1) { // cardinality = Choco.MAX_UPPER_BOUND; cardinality = group.getChildFeatures().size(); } return cardinality; } private int getMinChocoCardinality(Group group) { return group.getMinCardinality(); } private IntegerVariable getOrCreateVariable(Feature feature) { String id = EcoreUtil.getID(feature); IntegerVariable integerVariable = nodeVariables.get(id); if (integerVariable == null) { integerVariable = createFeatureVariable(feature); } return integerVariable; } private int getLowestBoundofNumericalDomain(NumericalDomain domain) { int lowestbound = 0; EList<Interval> intervals = domain.getIntervals(); if (intervals.size() >= 1) { lowestbound = intervals.get(0).getLowerBound(); for (Interval interval : intervals) { int lowerBound = interval.getLowerBound(); if (lowerBound < lowestbound) { lowestbound = lowerBound; } } } return lowestbound; } private int getHighestBoundofNumericalDomain(NumericalDomain domain) { int highestBound = 0; EList<Interval> intervals = domain.getIntervals(); if (intervals.size() >= 1) { highestBound = intervals.get(0).getUpperBound(); for (Interval interval : intervals) { int higherBound = interval.getUpperBound(); if (higherBound > highestBound) { highestBound = higherBound; } } } return highestBound; } private int[] getOrCreateDomainValues(DiscreteDomain domain) { String id = domain.getId(); int[] values = discreteDomainValues.get(id); if (values == null) { values = initDomainValues(domain); discreteDomainValues.put(id, values); } return values; } private int[] initDomainValues(DiscreteDomain domain) { EList<DomainValue> values = domain.getValues(); int[] valueArray = new int[values.size()]; Iterator<DomainValue> iterator = values.iterator(); for (int i = 0; i < valueArray.length; i++) { valueArray[i] = iterator.next().getInt(); } return valueArray; } private IntegerVariable createFeatureVariable(Feature feature) { String id = feature.getId(); // an unbound feature has cardinality [0..1], // a selected feature has cardinality [1..1], // a deselected feature has cardinality [0..0] int minCardinality = (FeatureState.SELECTED.equals(feature.getConfigurationState())) ? 1 : 0; int maxCardinality = (FeatureState.DESELECTED.equals(feature.getConfigurationState())) ? 0 : 1; log.debug("Create IntegerVariable for '" + id + "' [" + minCardinality + "," + maxCardinality + "]."); IntegerVariable intNodeVariable = Choco.makeIntVar(id, minCardinality, maxCardinality); getModel().addVariable(intNodeVariable); nodeVariables.put(id, intNodeVariable); return intNodeVariable; } private IntegerExpressionVariable createChildFeatureVariable(Group group) { EList<Feature> childFeatures = group.getChildFeatures(); List<IntegerVariable> childFeatureVariables = new ArrayList<IntegerVariable>(childFeatures.size()); for (Feature feature : childFeatures) { IntegerVariable childFeatureVariable = getOrCreateVariable(feature); childFeatureVariables.add(childFeatureVariable); } IntegerVariable[] childFeatureVariablesArray = new IntegerVariable[childFeatureVariables.size()]; childFeatureVariablesArray = childFeatureVariables.toArray(childFeatureVariablesArray); IntegerExpressionVariable childFeatureSum = Choco.sum(childFeatureVariablesArray); log.debug("Create IntegerExpressionVariable for child features of group " + group); return childFeatureSum; } }