diff --git a/pom.xml b/pom.xml
index afac763a..55f1db95 100644
--- a/pom.xml
+++ b/pom.xml
@@ -218,11 +218,6 @@
de.learnlib
learnlib-util
-
- org.apache.commons
- commons-lang3
- ${commons-lang.version}
-
tools.aqua
jconstraints-core
diff --git a/src/main/java/de/learnlib/ralib/automata/Assignment.java b/src/main/java/de/learnlib/ralib/automata/Assignment.java
index 55840509..1c777d16 100644
--- a/src/main/java/de/learnlib/ralib/automata/Assignment.java
+++ b/src/main/java/de/learnlib/ralib/automata/Assignment.java
@@ -17,6 +17,7 @@
package de.learnlib.ralib.automata;
import java.util.List;
+import java.util.Map;
import java.util.Map.Entry;
import de.learnlib.ralib.data.Constants;
@@ -42,6 +43,31 @@ public Assignment(VarMapping assignment)
this.assignment = assignment;
}
+ public RegisterValuation valuation(RegisterValuation registers, ParameterValuation parameters, Constants consts) {
+ RegisterValuation val = new RegisterValuation();
+ for (Map.Entry e : assignment.entrySet()) {
+ Register x = e.getKey();
+ SymbolicDataValue sdv = e.getValue();
+ DataValue d = sdv.isParameter() ? parameters.get((Parameter) sdv) :
+ sdv.isRegister() ? registers.get((Register) sdv) :
+ sdv.isConstant() ? consts.get((Constant) sdv) :
+ null;
+ if (d == null) {
+ throw new IllegalStateException("Illegal assignment: " + x + " := " + sdv);
+ }
+ val.put(x, d);
+ }
+ return val;
+ }
+
+ /*
+ * @deprecated method is unsafe, use {@link #valuation()} instead
+ * Method is unsafe because it keeps registers that are not given a new assignment, which can cause
+ * a discrepancy in the number of registers a location has, depending on the path to the location.
+ * Method is deprecated rather than removed because the functionality is used by XML automata models.
+ * Removal of method requires refactoring of XML models.
+ */
+ @Deprecated
public RegisterValuation compute(RegisterValuation registers, ParameterValuation parameters, Constants consts) {
RegisterValuation val = new RegisterValuation();
List rNames = assignment.keySet().stream().map(k -> k.getName()).toList();
@@ -56,6 +82,12 @@ public RegisterValuation compute(RegisterValuation registers, ParameterValuation
val.put(e.getKey(), registers.get((Register) valp));
}
else if (valp.isParameter()) {
+ DataValue dv = parameters.get((Parameter) valp);
+ for (Map.Entry ep : parameters.entrySet()) {
+ if (ep.getKey().equals(valp)) {
+ dv = ep.getValue();
+ }
+ }
val.put(e.getKey(), parameters.get((Parameter) valp));
}
//TODO: check if we want to copy constant values into vars
diff --git a/src/main/java/de/learnlib/ralib/automata/MutableRegisterAutomaton.java b/src/main/java/de/learnlib/ralib/automata/MutableRegisterAutomaton.java
index 9604c225..797f7fdd 100644
--- a/src/main/java/de/learnlib/ralib/automata/MutableRegisterAutomaton.java
+++ b/src/main/java/de/learnlib/ralib/automata/MutableRegisterAutomaton.java
@@ -28,7 +28,6 @@
import de.learnlib.ralib.words.PSymbolInstance;
import de.learnlib.ralib.words.ParameterizedSymbol;
import net.automatalib.automaton.MutableDeterministic;
-import net.automatalib.common.util.Pair;
import net.automatalib.word.Word;
/**
@@ -119,39 +118,6 @@ protected List getTransitions(Word dw) {
return tseq;
}
- protected List> getTransitionsAndValuations(Word dw) {
- RegisterValuation vars = RegisterValuation.copyOf(getInitialRegisters());
- RALocation current = initial;
- List> tvseq = new ArrayList<>();
- for (PSymbolInstance psi : dw) {
-
- ParameterValuation pars = ParameterValuation.fromPSymbolInstance(psi);
-
- Collection candidates =
- current.getOut(psi.getBaseSymbol());
-
- if (candidates == null) {
- return null;
- }
-
- boolean found = false;
- for (Transition t : candidates) {
- if (t.isEnabled(vars, pars, constants)) {
- vars = t.execute(vars, pars, constants);
- current = t.getDestination();
- tvseq.add(Pair.of(t, RegisterValuation.copyOf(vars)));
- found = true;
- break;
- }
- }
-
- if (!found) {
- return null;
- }
- }
- return tvseq;
- }
-
@Override
public RALocation getLocation(Word dw) {
List tseq = getTransitions(dw);
@@ -300,4 +266,43 @@ public Transition copyTransition(Transition t, RALocation s) {
throw new UnsupportedOperationException("Not supported yet.");
}
+ public RARun getRun(Word word) {
+ int n = word.length();
+ RALocation[] locs = new RALocation[n+1];
+ RegisterValuation[] vals = new RegisterValuation[n+1];
+ PSymbolInstance[] symbols = new PSymbolInstance[n];
+ Transition[] transitions = new Transition[n];
+
+ locs[0] = getInitialState();
+ vals[0] = new RegisterValuation();
+
+ for (int i = 0; i < n; i++) {
+ symbols[i] = word.getSymbol(i);
+ ParameterValuation pars = ParameterValuation.fromPSymbolInstance(symbols[i]);
+
+ Collection candidates = locs[i].getOut(symbols[i].getBaseSymbol());
+ if (candidates == null) {
+ return null;
+ }
+
+ boolean found = false;
+
+ for (Transition t : candidates) {
+ if (t.isEnabled(vals[i], pars, constants)) {
+ transitions[i] = t;
+ vals[i+1] = t.execute(vals[i], pars, constants);
+ locs[i+1] = t.getDestination();
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ return null;
+ }
+ }
+
+ return new RARun(locs, vals, symbols, transitions);
+ }
+
}
diff --git a/src/main/java/de/learnlib/ralib/automata/RARun.java b/src/main/java/de/learnlib/ralib/automata/RARun.java
new file mode 100644
index 00000000..8a0e509d
--- /dev/null
+++ b/src/main/java/de/learnlib/ralib/automata/RARun.java
@@ -0,0 +1,158 @@
+package de.learnlib.ralib.automata;
+
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import de.learnlib.ralib.automata.output.OutputMapping;
+import de.learnlib.ralib.automata.output.OutputTransition;
+import de.learnlib.ralib.data.Constants;
+import de.learnlib.ralib.data.DataValue;
+import de.learnlib.ralib.data.Mapping;
+import de.learnlib.ralib.data.RegisterValuation;
+import de.learnlib.ralib.data.SymbolicDataValue;
+import de.learnlib.ralib.data.SymbolicDataValue.Parameter;
+import de.learnlib.ralib.smt.VarsValuationVisitor;
+import de.learnlib.ralib.words.PSymbolInstance;
+import gov.nasa.jpf.constraints.api.Expression;
+import gov.nasa.jpf.constraints.expressions.NumericBooleanExpression;
+import gov.nasa.jpf.constraints.expressions.NumericComparator;
+import gov.nasa.jpf.constraints.util.ExpressionUtil;
+
+/**
+ * Data structure containing the locations, register valuations, symbol instances
+ * and transitions at each step of a run of a hypothesis over a data word.
+ *
+ * @author fredrik
+ */
+public class RARun {
+
+ private final RALocation[] locations;
+ private final RegisterValuation[] valuations;
+ private final PSymbolInstance[] symbols;
+ private final Transition[] transitions;
+
+ public RARun(RALocation[] locations, RegisterValuation[] valuations, PSymbolInstance[] symbols, Transition[] transitions) {
+ this.locations = locations;
+ this.valuations = valuations;
+ this.symbols = symbols;
+ this.transitions = transitions;
+ }
+
+ public RALocation getLocation(int i) {
+ return locations[i];
+ }
+
+ public RegisterValuation getValuation(int i) {
+ return valuations[i];
+ }
+
+ public PSymbolInstance getTransitionSymbol(int i) {
+ return symbols[i-1];
+ }
+
+ public Transition getRATransition(int i) {
+ return transitions[i-1];
+ }
+
+ /**
+ * Get the guard of the {@code Transition} at index {@code i}. If the {@code Transition}
+ * is an {@code OutputTransition}, the guard is computed from the transition's
+ * {@code OutputMapping}.
+ *
+ * @param i
+ * @return
+ */
+ public Expression getGuard(int i) {
+ Transition transition = getRATransition(i);
+ if (transition == null) {
+ return null;
+ }
+ return transition instanceof OutputTransition ?
+ outputGuard((OutputTransition) transition) :
+ transition.getGuard();
+ }
+
+ /**
+ * Get the guard of the {@code Transition} at index {@code i}. If the {@code Transition}
+ * is an {@code OutputTransition}, the guard is computed from the transition's
+ * {@code OutputMapping}. Registers of the guard will be evaluated according to the
+ * data values from the register valuation at index {@code i}, and constants will be
+ * evaluated according to {@code consts}.
+ *
+ * @param i
+ * @return
+ */
+ public Expression getGuard(int i, Constants consts) {
+ Expression guard = getGuard(i);
+ VarsValuationVisitor vvv = new VarsValuationVisitor();
+ Mapping vals = new Mapping<>();
+ vals.putAll(getValuation(i));
+ vals.putAll(consts);
+ return vvv.apply(guard, vals);
+ }
+
+ private Expression outputGuard(OutputTransition t) {
+ OutputMapping out = t.getOutput();
+
+ Set params = new LinkedHashSet<>();
+ params.addAll(out.getFreshParameters());
+ params.addAll(out.getOutput().keySet());
+ Set regs = new LinkedHashSet<>();
+ regs.addAll(out.getOutput().values());
+
+ Expression[] expressions = new Expression[params.size()];
+ int index = 0;
+
+ // fresh parameters
+ List prior = new ArrayList<>();
+ List fresh = new ArrayList<>(out.getFreshParameters());
+ Collections.sort(fresh, (a,b) -> Integer.compare(a.getId(), b.getId()));
+ for (Parameter p : fresh) {
+ Expression[] diseq = new Expression[prior.size() + regs.size()];
+ int i = 0;
+ for (Parameter prev : prior) {
+ diseq[i++] = new NumericBooleanExpression(p, NumericComparator.NE, prev);
+ }
+ for (SymbolicDataValue s : regs) {
+ diseq[i++] = new NumericBooleanExpression(p, NumericComparator.NE, s);
+ }
+ expressions[index++] = ExpressionUtil.and(diseq);
+ prior.add(p);
+ }
+
+ // mapped parameters
+ for (Map.Entry e : out.getOutput().entrySet()) {
+ Parameter p = e.getKey();
+ SymbolicDataValue s = e.getValue();
+ expressions[index++] = new NumericBooleanExpression(p, NumericComparator.EQ, s);
+ }
+
+ return ExpressionUtil.and(expressions);
+ }
+
+ @Override
+ public String toString() {
+ if (locations.length == 0) {
+ return "ε";
+ }
+
+ String str = "<" + locations[0] + ", " + valuations[0] + ">";
+ for (int i = 1; i < locations.length; i++) {
+ str = str +
+ " -- " +
+ symbols[i-1] +
+ " -- <" +
+ locations[i] +
+ ", " +
+ valuations[i] +
+ ">";
+ }
+
+ return str;
+ }
+}
diff --git a/src/main/java/de/learnlib/ralib/automata/Transition.java b/src/main/java/de/learnlib/ralib/automata/Transition.java
index ef7d6f72..190a24b6 100644
--- a/src/main/java/de/learnlib/ralib/automata/Transition.java
+++ b/src/main/java/de/learnlib/ralib/automata/Transition.java
@@ -53,6 +53,18 @@ public boolean isEnabled(RegisterValuation registers, ParameterValuation paramet
return guard.evaluateSMT(SMTUtil.compose(registers, parameters, consts));
}
+ public RegisterValuation valuation(RegisterValuation registers, ParameterValuation parameters, Constants consts) {
+ return this.getAssignment().valuation(registers, parameters, consts);
+ }
+
+ /*
+ * @deprecated method is unsafe, use {@link #valuation()} instead
+ * Method is unsafe because it keeps registers that are not given a new assignment, which can cause
+ * a discrepancy in the number of registers a location has, depending on the path to the location.
+ * Method is deprecated rather than removed because the functionality is used by XML automata models.
+ * Removal of method requires refactoring of XML models.
+ */
+ @Deprecated
public RegisterValuation execute(RegisterValuation registers, ParameterValuation parameters, Constants consts) {
return this.getAssignment().compute(registers, parameters, consts);
}
diff --git a/src/main/java/de/learnlib/ralib/ceanalysis/PrefixFinder.java b/src/main/java/de/learnlib/ralib/ceanalysis/PrefixFinder.java
index 0f73dc13..0eacb885 100644
--- a/src/main/java/de/learnlib/ralib/ceanalysis/PrefixFinder.java
+++ b/src/main/java/de/learnlib/ralib/ceanalysis/PrefixFinder.java
@@ -1,290 +1,374 @@
package de.learnlib.ralib.ceanalysis;
-import java.util.*;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import de.learnlib.logging.Category;
-import de.learnlib.query.DefaultQuery;
-import de.learnlib.ralib.data.*;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import de.learnlib.ralib.automata.RALocation;
+import de.learnlib.ralib.automata.RARun;
+import de.learnlib.ralib.ct.CTHypothesis;
+import de.learnlib.ralib.ct.CTLeaf;
+import de.learnlib.ralib.ct.ClassificationTree;
+import de.learnlib.ralib.ct.Prefix;
+import de.learnlib.ralib.ct.ShortPrefix;
+import de.learnlib.ralib.data.Bijection;
+import de.learnlib.ralib.data.Constants;
+import de.learnlib.ralib.data.DataType;
+import de.learnlib.ralib.data.DataValue;
+import de.learnlib.ralib.data.Mapping;
+import de.learnlib.ralib.data.RegisterValuation;
+import de.learnlib.ralib.data.SymbolicDataValue;
import de.learnlib.ralib.data.SymbolicDataValue.Parameter;
-import de.learnlib.ralib.data.SymbolicDataValue.SuffixValue;
-import de.learnlib.ralib.data.util.RemappingIterator;
+import de.learnlib.ralib.data.SymbolicDataValue.Register;
+import de.learnlib.ralib.data.util.PermutationIterator;
import de.learnlib.ralib.data.util.SymbolicDataValueGenerator.ParameterGenerator;
-import de.learnlib.ralib.data.util.SymbolicDataValueGenerator.SuffixValueGenerator;
-import de.learnlib.ralib.learning.*;
-import de.learnlib.ralib.learning.rastar.CEAnalysisResult;
-import de.learnlib.ralib.oracles.SDTLogicOracle;
+import de.learnlib.ralib.learning.SymbolicSuffix;
+import de.learnlib.ralib.oracles.Branching;
import de.learnlib.ralib.oracles.TreeOracle;
import de.learnlib.ralib.oracles.mto.SymbolicSuffixRestrictionBuilder;
-import de.learnlib.ralib.smt.SMTUtil;
-import de.learnlib.ralib.theory.Memorables;
+import de.learnlib.ralib.smt.ConstraintSolver;
+import de.learnlib.ralib.smt.ReplacingValuesVisitor;
import de.learnlib.ralib.theory.SDT;
+import de.learnlib.ralib.theory.Theory;
import de.learnlib.ralib.words.PSymbolInstance;
import de.learnlib.ralib.words.ParameterizedSymbol;
import gov.nasa.jpf.constraints.api.Expression;
import gov.nasa.jpf.constraints.util.ExpressionUtil;
import net.automatalib.word.Word;
+/**
+ * Analyzes counterexamples according to the SLλ algorithm.
+ *
+ * @author fredrik
+ */
public class PrefixFinder {
- private final TreeOracle sulOracle;
+ public enum ResultType {
+ TRANSITION,
+ LOCATION
+ }
- private TreeOracle hypOracle;
+ /**
+ * Container for the result of a counterexample analysis
+ */
+ public record Result(Word prefix, ResultType result) {};
- private Hypothesis hypothesis;
+ private final CTHypothesis hyp;
+ private final ClassificationTree ct;
- private final SDTLogicOracle sdtOracle;
+ private final TreeOracle sulOracle;
+ private final Map teachers;
- private final SymbolicSuffixRestrictionBuilder restrictionBuilder;
+ private final SymbolicSuffixRestrictionBuilder restrBuilder;
- private final Constants consts;
+ private final ConstraintSolver solver;
- private SymbolicWord[] candidates;
+ private final Constants consts;
- private final Map candidateCEs = new LinkedHashMap();
- private final Map storedQueries = new LinkedHashMap();
+ public PrefixFinder(TreeOracle sulOracle, CTHypothesis hyp, ClassificationTree ct,
+ Map teachers, SymbolicSuffixRestrictionBuilder restrBuilder,
+ ConstraintSolver solver, Constants consts) {
+ this.hyp = hyp;
+ this.ct = ct;
+ this.sulOracle = sulOracle;
+ this.teachers = teachers;
+ this.restrBuilder = restrBuilder;
+ this.solver = solver;
+ this.consts = consts;
+ }
- private static final Logger LOGGER = LoggerFactory.getLogger(PrefixFinder.class);
+ /**
+ * Analyze counterexample {@code ce} from right to leaf to find a transition or
+ * location discrepancy. If a discrepancy is found, returns the prefix which reveals
+ * the discrepancy, along with a {@code ResultType} indicating the type of discrepancy.
+ *
+ * @param ce
+ * @return
+ */
+ public Result analyzeCounterExample(Word ce) {
+ RARun run = hyp.getRun(ce);
+ for (int i = ce.length(); i >= 1; i--) {
+ RALocation loc = run.getLocation(i-1);
+ RALocation locNext = run.getLocation(i);
+ PSymbolInstance symbol = run.getTransitionSymbol(i);
+ RegisterValuation runValuation = run.getValuation(i-1);
+ ParameterizedSymbol action = symbol.getBaseSymbol();
+
+ SymbolicSuffix vNext = new SymbolicSuffix(ce.prefix(i), ce.suffix(ce.length() - i), restrBuilder);
+ SymbolicSuffix v = new SymbolicSuffix(ce.prefix(i-1), ce.suffix(ce.length() - i + 1), restrBuilder);
+
+ Expression gHyp = run.getGuard(i, consts);
+
+ for (ShortPrefix u : hyp.getLeaf(loc).getShortPrefixes()) {
+ SDT sdt = sulOracle.treeQuery(u, v);
+
+ Set uVals = hyp.getLeaf(loc).getPrefix(u).getRegisters();
+ Mapping uToRunRenaming = valuationRenaming(u, runValuation);
+ Set> uToRunExtendedRenamings = extendedValuationRenamings(sdt, uVals, run, i);
+
+ Branching branching = sulOracle.getInitialBranching(u, action, sdt);
+ for (Expression gSul : branching.guardSet()) {
+ for (Mapping renaming : uToRunExtendedRenamings) {
+ renaming.putAll(uToRunRenaming);
+ if (isGuardSatisfied(gSul, renaming, symbol)) {
+ Optional res = checkTransition(locNext, u, action, vNext, gHyp, gSul);
+ if (res.isEmpty()) {
+ res = checkLocation(locNext, u, action, vNext);
+ }
+ if (res.isPresent()) {
+ return res.get();
+ }
+ }
+ }
+ }
+ }
+ }
- public PrefixFinder(TreeOracle sulOracle, TreeOracle hypOracle,
- Hypothesis hypothesis, SDTLogicOracle sdtOracle,
- Constants consts) {
+ throw new IllegalStateException("Found no counterexample in " + ce);
+ }
- this.sulOracle = sulOracle;
- this.hypOracle = hypOracle;
- this.hypothesis = hypothesis;
- this.sdtOracle = sdtOracle;
- this.consts = consts;
- this.restrictionBuilder = sulOracle.getRestrictionBuilder();
- }
+ /**
+ * @param u
+ * @param val
+ * @return a mapping from the register valuation of {@code u} on the hypothesis to the {@code val}
+ */
+ private Mapping valuationRenaming(Word u, RegisterValuation val) {
+ // get register mapping for u when run over the hypothesis
+ RARun uRun = hyp.getRun(u);
+ RegisterValuation uVal = uRun.getValuation(u.size());
+ // create mapping from u's valuation to val
+ Mapping ret = new Mapping<>();
+ for (Map.Entry e : uVal.entrySet()) {
+ DataValue replace = e.getValue();
+ DataValue by = val.get(e.getKey());
+ if (by == null) {
+ by = replace;
+ }
+ ret.put(replace, by);
+ }
+ return ret;
+ }
- public CEAnalysisResult analyzeCounterexample(Word ce) {
- int idx = findIndex(ce);
- SymbolicWord sw = new SymbolicWord(candidates[idx].getPrefix(), candidates[idx].getSuffix());
- SDT tqr = null; //storedQueries.get(sw);
- if (tqr == null) {
- // THIS CAN (possibly) BE DONE WITHOUT A NEW TREE QUERY
- tqr = sulOracle.treeQuery(sw.getPrefix(), sw.getSuffix());
- }
- CEAnalysisResult result = new CEAnalysisResult(candidates[idx].getPrefix(),
- candidates[idx].getSuffix(),
- tqr);
-
- candidateCEs.put(candidates[idx], tqr);
- storeCandidateCEs(ce, idx);
-
- return result;
- }
-
- private int findIndex(Word ce) {
- candidates = new SymbolicWord[ce.length()];
- int max = ce.length() - 1;
- for (int idx=max; idx>=0; idx = idx-1) {
-
- Word prefix = ce.prefix(idx);
- Word nextPrefix = ce.prefix(idx+1);
-
- LOGGER.trace(Category.DATASTRUCTURE, "idx: {} ce: {}", idx, ce);
- LOGGER.trace(Category.DATASTRUCTURE, "idx: {} prefix: {}", idx, prefix);
- LOGGER.trace(Category.DATASTRUCTURE, "idx: {} next: {}", idx, nextPrefix);
-
- // check for location counterexample ...
- //
- Word suffix = ce.suffix(ce.length() - nextPrefix.length());
- SymbolicSuffix symSuffix = new SymbolicSuffix(nextPrefix, suffix, restrictionBuilder);
- LOC_CHECK: for (Word u : hypothesis.possibleAccessSequences(prefix)) {
- Word uAlpha = hypothesis.transformTransitionSequence(nextPrefix, u);
- SDT uAlphaResult = sulOracle.treeQuery(uAlpha, symSuffix);
- storedQueries.put(new SymbolicWord(uAlpha, symSuffix), uAlphaResult);
-
- // check if the word is inequivalent to all access sequences
- //
- for (Word uPrime : hypothesis.possibleAccessSequences(nextPrefix)) {
- SDT uPrimeResult = sulOracle.treeQuery(uPrime, symSuffix);
- storedQueries.put(new SymbolicWord(uPrime, symSuffix), uPrimeResult);
-
- LOGGER.trace(Category.DATASTRUCTURE, "idx: {} u: {}", idx, u);
- LOGGER.trace(Category.DATASTRUCTURE, "idx: {} ua: {}", idx, uAlpha);
- LOGGER.trace(Category.DATASTRUCTURE, "idx: {} u': {}", idx, uPrime);
- LOGGER.trace(Category.DATASTRUCTURE, "idx: {} v: {}", idx, symSuffix);
-
- // different sizes
- //
- if (!Memorables.typedSize(uPrimeResult.getDataValues()).equals(
- Memorables.typedSize(uAlphaResult.getDataValues()))) {
- continue;
- }
+ /**
+ * Create extensions of the valuation from the hypothesis at index {@code id}, and map
+ * data values from {@code uSDT} to these extended valuations.
+ * The returned remappings do not include parameters present in the valuation at {@code id}
+ * (which should be mapped to parameters in {@code uVals}, except for duplicate parameters.
+ * For example, if the parameters of {@code run} at index {@code id} contain the data values
+ * 5,5,7 and the valuation contains 5, then the 7 and a single 5 will be considered for the
+ * extension of the valuation. Similarly, if {@code uSDT} has data values 0,1,2 with 0 in
+ * {@code uValuation}, then 1,2 will be considered. In this example, this method would
+ * return the mappings {1->5, 2->7} and {1->7, 2->5}.
+ *
+ * @param uSDT sdt for prefix {@code u}
+ * @param uVals memorable data values of {@code u}
+ * @param run
+ * @param id
+ * @return
+ */
+ private Set> extendedValuationRenamings(SDT uSDT, Set uVals, RARun run, int id) {
+ Set> empty = new LinkedHashSet<>();
+ empty.add(new Mapping<>());
+ if (id < 1) {
+ return empty;
+ }
- // remapping
- //
- RemappingIterator iterator = new RemappingIterator<>(
- uPrimeResult.getDataValues(), uAlphaResult.getDataValues());
+ // gather data values from uSDT, and remove values from uValuation
+ Set sdtVals = new LinkedHashSet<>(uSDT.getDataValues());
+ for (DataValue d : uVals) {
+ sdtVals.remove(d);
+ }
+ if (sdtVals.isEmpty()) {
+ return empty;
+ }
+ DataValue[] sdtValsArr = sdtVals.toArray(new DataValue[sdtVals.size()]);
- for (Bijection m : iterator) {
- if (uAlphaResult.isEquivalent(uPrimeResult, m)) {
- continue LOC_CHECK;
- }
- }
+ // gather data values from prefix of run at index id
+ ArrayList runVals = new ArrayList<>();
+ for (int i = 1; i <= id-1; i++) {
+ for (DataValue d : run.getTransitionSymbol(i).getParameterValues()) {
+ runVals.add(d);
+ }
+ }
+ /* remove data values from valuation.
+ * may have multiple copies of same data value, which may be mapped to different
+ * data values in uSDT, so only remove one instance of data values in valuation
+ */
+ for (DataValue d : run.getValuation(id-1).values()) {
+ runVals = removeFirst(runVals, d);
+ }
+
+ // compute all possible permutations of mappings between extended uSDT values and run values
+ Set> renamings = new LinkedHashSet<>();
+ PermutationIterator permit = new PermutationIterator(runVals.size());
+ for (int[] order : permit) {
+ Mapping remapping = new Mapping<>();
+ boolean valid = true;
+ for (int i = 0; i < sdtValsArr.length; i++) {
+ DataValue sdtVal = sdtValsArr[i];
+ DataValue runVal = runVals.get(order[i]);
+ if (!sdtVal.getDataType().equals(runVal.getDataType())) {
+ valid = false;
+ break;
}
- // found a counterexample!
- candidates[idx] = new SymbolicWord(uAlpha, symSuffix);
- LOGGER.trace(Category.COUNTEREXAMPLE, "Counterexample for location");
- return idx;
+ remapping.put(sdtVal, runVal);
}
-
- // check for transition counterexample ...
- //
- if (transitionHasCE(ce, idx-1)) {
- LOGGER.trace(Category.COUNTEREXAMPLE, "Counterexample for transition");
- return idx;
+ if (valid) {
+ renamings.add(remapping);
}
}
- throw new RuntimeException("should not reach here");
+
+ return renamings;
}
-// private Pair checkForCE(Word prefix, SymbolicSuffix suffix, Word transition) {
-// SymbolicWord symWord = new SymbolicWord(prefix, suffix);
-// SDT resHyp = hypOracle.treeQuery(prefix, suffix);
-// SDT resSul;
-// if (storedQueries.containsKey(symWord))
-// resSul = storedQueries.get(symWord);
-// else {
-// resSul = sulOracle.treeQuery(prefix, suffix);
-// storedQueries.put(symWord, resSul);
-// }
-//
-// boolean hasCE = sdtOracle.hasCounterexample(prefix,
-// resHyp.getSdt(), resHyp.getPiv(),
-// resSul.getSdt(), resSul.getPiv(),
-// new TransitionGuard(), transition);
-//
-// return hasCE ? new ImmutablePair(resHyp, resSul) : null;
-// }
-
- private boolean transitionHasCE(Word ce, int idx) {
- if (idx+1 >= ce.length())
- return false;
-
- Word prefix = ce.prefix(idx+1);
-
- Word suffix = ce.suffix(ce.length() - (idx+1));
- SymbolicSuffix symSuffix = new SymbolicSuffix(prefix, suffix, restrictionBuilder);
-
- Set> locations = hypothesis.possibleAccessSequences(prefix);
- for (Word location : locations) {
- Word transition = hypothesis.transformTransitionSequence(
- ce.prefix(idx+2), location);
-
- SDT resHyp = hypOracle.treeQuery(location, symSuffix);
- SDT resSul;
- SymbolicWord symWord = new SymbolicWord(location, symSuffix);
- if (storedQueries.containsKey(symWord))
- resSul = storedQueries.get(symWord);
- else {
- resSul = sulOracle.treeQuery(location, symSuffix);
- storedQueries.put(symWord, resSul);
- }
-
- boolean hasCE = sdtOracle.hasCounterexample(location,
- resHyp,
- resSul,
- ExpressionUtil.TRUE, transition);
-
- if (hasCE) {
- SymbolicWord sw = candidate(location, symSuffix, resSul, resHyp, ce);
- // new by falk
- candidates[idx+1] = sw;
- return true;
+ /**
+ * @param list
+ * @param d
+ * @return array containing data values of {@code list}, with one occurrence of {@code d} removed
+ */
+ private ArrayList removeFirst(ArrayList list, DataValue d) {
+ ArrayList ret = new ArrayList<>();
+ ret.addAll(list);
+ for (int i = 0; i < list.size(); i++) {
+ if (list.get(i).equals(d)) {
+ ret.remove(i);
+ break;
}
- }
- return false;
- }
-
- private void storeCandidateCEs(Word ce, int idx) {
- if (idx+1 >= ce.length())
- return;
- Word prefix = ce.prefix(idx+1);
-
- Word suffix = ce.suffix(ce.length() - (idx+1));
- SymbolicSuffix symSuffix = new SymbolicSuffix(prefix, suffix, restrictionBuilder);
-
- Set> locations = hypothesis.possibleAccessSequences(prefix);
- for (Word location : locations) {
- SymbolicWord symWord = new SymbolicWord(location, symSuffix);
- SDT tqr = storedQueries.get(symWord);
-
- assert tqr != null;
-
- candidateCEs.put(symWord, tqr);
- }
- }
-
- private SymbolicWord candidate(Word prefix,
- SymbolicSuffix symSuffix, SDT sdtSul,
- SDT sdtHyp, Word ce) {
- Word candidate = null;
-
- Expression expr = sdtOracle.getCEGuard(prefix, sdtSul, sdtHyp);
-
- Map, Boolean> sulPaths = sulOracle.instantiate(prefix, symSuffix, sdtSul);
- for (Word path : sulPaths.keySet()) {
- ParameterGenerator parGen = new ParameterGenerator();
- for (PSymbolInstance psi : prefix) {
- for (DataType dt : psi.getBaseSymbol().getPtypes())
- parGen.next(dt);
- }
+ }
+ return ret;
+ }
- VarMapping renaming = new VarMapping<>();
- SuffixValueGenerator svGen = new SuffixValueGenerator();
- for (ParameterizedSymbol ps : symSuffix.getActions()) {
- for (DataType dt : ps.getPtypes()) {
- Parameter p = parGen.next(dt);
- SuffixValue sv = svGen.next(dt);
- renaming.put(sv, p);
- }
+ /**
+ * Check for a transition discrepancy. This is done by checking whether there exists no
+ * {@code action}-extension of {@code u} in the leaf of {@code loc} that is equivalent
+ * to the {@code (hypGuard && sulGuard)} extension of {@code u} after {@code v}.
+ *
+ * @param loc the source location
+ * @param u short prefix from leaf of {@code loc}
+ * @param action the symbol of the next transition
+ * @param v the suffix after {@code u} and {@code action}
+ * @param hypGuard guard of {@code action} after {@code u} on the hypothesis
+ * @param sulGuard guard of {@code action} after {@code u} on the SUL
+ * @return an {@code Optional} containing the result if there is a transition discrepancy, or an empty {@code Optional} otherwise
+ */
+ private Optional checkTransition(RALocation loc,
+ ShortPrefix u,
+ ParameterizedSymbol action,
+ SymbolicSuffix v,
+ Expression hypGuard,
+ Expression sulGuard) {
+ // rename hyp guard to match RP
+ ReplacingValuesVisitor rvv = new ReplacingValuesVisitor();
+ Expression hypGuardRenamed = rvv.apply(hypGuard, u.getRpBijection().inverse().toVarMapping());
+ Expression conjunction = ExpressionUtil.and(hypGuardRenamed, sulGuard);
+
+ // instantiate a representative data value for the conjunction
+ DataType[] types = action.getPtypes();
+ DataValue[] reprDataVals = new DataValue[types.length];
+ for (int i = 0; i < types.length; i++) {
+ Optional reprDataVal = teachers.get(types[i]).instantiate(u, action, conjunction, i+1, consts, solver);
+ if (reprDataVal.isEmpty()) {
+ // guard unsat
+ return Optional.empty();
}
- Expression exprR = SMTUtil.renameVars(expr, renaming);
+ reprDataVals[i] = reprDataVal.get();
+ }
+ PSymbolInstance psi = new PSymbolInstance(action, reprDataVals);
+ Word uExtSul = u.append(psi);
+
+ // check whether leaf of loc contains an extension of u that is equivalent to uExtSul after v
+ CTLeaf leaf = hyp.getLeaf(loc);
+ Iterator> extensions = ct.getExtensions(u, action)
+ .stream()
+ .filter(w -> leaf.getPrefixes().contains(w))
+ .iterator();
+ while (extensions.hasNext()) {
+ Word uExtHyp = extensions.next();
+ SDT uExtHypSDT = sulOracle.treeQuery(uExtHyp, v).toRegisterSDT(uExtHyp, consts);
+ SDT uExtSulSDT = sulOracle.treeQuery(uExtSul, v).toRegisterSDT(uExtSul, consts);
+
+ if (SDT.equivalentUnderId(uExtHypSDT, uExtSulSDT)) {
+ return Optional.empty(); // there is an equivalent extension, so no discrepancy
+ }
+ }
- ParameterValuation pars = ParameterValuation.fromPSymbolWord(path);
- Mapping vals = new Mapping<>();
- vals.putAll(pars);
- vals.putAll(consts);
+ // no equivalent extension exists
+ Result res = new Result(uExtSul, ResultType.TRANSITION);
+ return Optional.of(res);
+ }
- if (exprR.evaluateSMT(SMTUtil.compose(vals))) {
- candidate = path.prefix(prefix.length() + 1);
- SymbolicSuffix suffix = new SymbolicSuffix(candidate, ce.suffix(symSuffix.length() - 1), restrictionBuilder);
- return new SymbolicWord(candidate, suffix);
- }
- }
- throw new IllegalStateException("No CE transition found");
- }
-
- public Set> getCounterExamples() {
- Set> ces = new LinkedHashSet>();
- for (Map.Entry e : candidateCEs.entrySet()) {
- SymbolicWord sw = e.getKey();
- SDT tqr = e.getValue();
- Map, Boolean> cemaps = sulOracle.instantiate(sw.getPrefix(), sw.getSuffix(), tqr);
- for (Map.Entry, Boolean> c : cemaps.entrySet()) {
- ces.add(new DefaultQuery(c.getKey(), c.getValue()));
- }
- }
-
- return ces;
- }
-
- public void setHypothesisTreeOracle(TreeOracle hypOracle) {
- this.hypOracle = hypOracle;
- }
-
- public void setHypothesis(Hypothesis hyp) {
- this.hypothesis = hyp;
- }
-
- //public void setComponents(Map, LocationComponent> components) {
- // this.components = components;
- //}
+ /**
+ * Check for a location discrepancy. This is done by checking whether there is some
+ * {@code action}-extension of {@code u} in the leaf of {@code locNext} such that there
+ * does not exist some short prefix in the leaf of {@code locNext} that is equivalent
+ * to the {@code action}-extension of {@code u} after {@code v}.
+ *
+ * @param locNext the destination location
+ * @param u short prefix in leaf prior to {@code locNext} in the run
+ * @param action the symbol of the next transition
+ * @param v the suffix after {@code u} and {@code action}
+ * @return an {@code Optional} containing the result if there is a location discrepancy, or an empty {@code Optional} otherwise
+ */
+ private Optional checkLocation(RALocation locNext,
+ Word u,
+ ParameterizedSymbol action,
+ SymbolicSuffix v) {
+ CTLeaf leafNext = hyp.getLeaf(locNext);
+ Iterator extensions = ct.getExtensions(u, action)
+ .stream()
+ .filter(w -> leafNext.getPrefixes().contains(w))
+ .map(w -> leafNext.getPrefix(w))
+ .iterator();
+ while (extensions.hasNext()) {
+ Prefix uExtended = extensions.next();
+ Bijection uExtBijection = uExtended.getRpBijection();
+ boolean noEquivU = true;
+ for (Prefix uNext : leafNext.getShortPrefixes()) {
+ Bijection uNextBijection = uNext.getRpBijection();
+ Bijection gamma = uNextBijection.compose(uExtBijection.inverse());
+ SDT uExtSDT = sulOracle.treeQuery(uExtended, v);
+ SDT uNextSDT = sulOracle.treeQuery(uNext, v);
+ if (SDT.equivalentUnderBijection(uNextSDT, uExtSDT, gamma) != null) {
+ noEquivU = false;
+ break;
+ }
+ }
+ if (noEquivU) {
+ Result res = new Result(uExtended, ResultType.LOCATION);
+ return Optional.of(res);
+ }
+ }
+
+ return Optional.empty();
+ }
+
+ /**
+ * Check whether {@code guard} is satisfied by the parameters of {@code symbol}, after renaming
+ * the parameters of {@code guard} according to {@code renaming}.
+ *
+ * @param guard
+ * @param renaming
+ * @param symbol
+ * @return {@code true} if {@code symbol} satisfies {@code guard}, renamed according to {@code renaming}
+ */
+ private boolean isGuardSatisfied(Expression guard, Mapping renaming, PSymbolInstance symbol) {
+ Mapping mapping = new Mapping<>();
+ DataValue[] vals = symbol.getParameterValues();
+ ParameterGenerator pgen = new ParameterGenerator();
+
+ ReplacingValuesVisitor rvv = new ReplacingValuesVisitor();
+ Expression guardRenamed = rvv.apply(guard, renaming);
+
+ for (int i = 0; i < vals.length; i++) {
+ Parameter p = pgen.next(vals[i].getDataType());
+ mapping.put(p, vals[i]);
+ }
+ mapping.putAll(consts);
+
+ return solver.isSatisfiable(guardRenamed, mapping);
+ }
}
diff --git a/src/main/java/de/learnlib/ralib/ceanalysis/PrefixFinderResult.java b/src/main/java/de/learnlib/ralib/ceanalysis/PrefixFinderResult.java
new file mode 100644
index 00000000..ac7a5180
--- /dev/null
+++ b/src/main/java/de/learnlib/ralib/ceanalysis/PrefixFinderResult.java
@@ -0,0 +1,6 @@
+package de.learnlib.ralib.ceanalysis;
+
+public enum PrefixFinderResult {
+ TRANSITION,
+ LOCATION
+}
diff --git a/src/main/java/de/learnlib/ralib/ct/CTAutomatonBuilder.java b/src/main/java/de/learnlib/ralib/ct/CTAutomatonBuilder.java
new file mode 100644
index 00000000..ab6d2ade
--- /dev/null
+++ b/src/main/java/de/learnlib/ralib/ct/CTAutomatonBuilder.java
@@ -0,0 +1,278 @@
+package de.learnlib.ralib.ct;
+
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import de.learnlib.ralib.automata.Assignment;
+import de.learnlib.ralib.automata.RALocation;
+import de.learnlib.ralib.automata.Transition;
+import de.learnlib.ralib.automata.output.OutputMapping;
+import de.learnlib.ralib.automata.output.OutputTransition;
+import de.learnlib.ralib.data.Bijection;
+import de.learnlib.ralib.data.Constants;
+import de.learnlib.ralib.data.DataType;
+import de.learnlib.ralib.data.DataValue;
+import de.learnlib.ralib.data.Mapping;
+import de.learnlib.ralib.data.RegisterAssignment;
+import de.learnlib.ralib.data.SymbolicDataValue;
+import de.learnlib.ralib.data.SymbolicDataValue.Parameter;
+import de.learnlib.ralib.data.SymbolicDataValue.Register;
+import de.learnlib.ralib.data.VarMapping;
+import de.learnlib.ralib.data.util.SymbolicDataValueGenerator.ParameterGenerator;
+import de.learnlib.ralib.learning.AutomatonBuilder;
+import de.learnlib.ralib.learning.rastar.RaStar;
+import de.learnlib.ralib.oracles.Branching;
+import de.learnlib.ralib.smt.ConstraintSolver;
+import de.learnlib.ralib.smt.ReplacingValuesVisitor;
+import de.learnlib.ralib.words.OutputSymbol;
+import de.learnlib.ralib.words.PSymbolInstance;
+import de.learnlib.ralib.words.ParameterizedSymbol;
+import gov.nasa.jpf.constraints.api.Expression;
+import gov.nasa.jpf.constraints.expressions.NumericBooleanExpression;
+import gov.nasa.jpf.constraints.expressions.NumericComparator;
+import gov.nasa.jpf.constraints.expressions.PropositionalCompound;
+import gov.nasa.jpf.constraints.util.ExpressionUtil;
+import net.automatalib.word.Word;
+
+/**
+ * Builder class for building a {@link CTHypothesis} from a {@link ClassificationTree}.
+ * This class implements similar functionality as {@link AutomatonBuilder} and {@link IOAutomatonBuilder},
+ * but tailored for the {@link SLLambda} and {@link SLCT} learning algorithms.
+ *
+ * {@code CTAutomatonBuilder} supports construction of automata from an incomplete classification tree,
+ * so long as the classification tree is closed and consistent.
+ * Multiple short prefixes in the same leaf, as well as one-symbol extensions without matching guards,
+ * will be ignored during construction.
+ * The access sequence for each location will be set to the representative prefix of the corresponding leaf.
+ *
+ * @author fredrik
+ */
+public class CTAutomatonBuilder {
+
+ private final ClassificationTree ct;
+
+ private final Map, RALocation> locations; // to keep track of which prefix maps to which location
+ private final Map leaves;
+
+ private final CTHypothesis hyp;
+
+ private final ConstraintSolver solver;
+
+ private final Constants consts;
+
+ private boolean ioMode;
+
+ public CTAutomatonBuilder(ClassificationTree ct, Constants consts, boolean ioMode, ConstraintSolver solver) {
+ this.ct = ct;
+ this.consts = consts;
+ this.ioMode = ioMode;
+ this.solver = solver;
+
+ locations = new LinkedHashMap<>();
+ leaves = new LinkedHashMap<>();
+ hyp = new CTHypothesis(consts, ct.getLeaves().size(), ioMode);
+ }
+
+ public CTHypothesis buildHypothesis() {
+ computeLocations();
+ computeTransitions();
+ hyp.putLeaves(leaves);
+ Optional sink = ct.getSink();
+ if (sink.isPresent()) {
+ hyp.setSink(hyp.getLocation(sink.get()));
+ }
+ return hyp;
+ }
+
+ private void computeLocations() {
+ // compute initial location
+ CTLeaf initial = ct.getLeaf(RaStar.EMPTY_PREFIX);
+ RALocation l0 = hyp.addInitialState(initial.isAccepting());
+ locations.put(RaStar.EMPTY_PREFIX, l0);
+ for (Word sp : initial.getShortPrefixes()) {
+ locations.put(sp, l0);
+ }
+ hyp.setAccessSequence(l0, RaStar.EMPTY_PREFIX);
+ leaves.put(initial, l0);
+
+ // compute non-initial locations
+ for (CTLeaf leaf : ct.getLeaves()) {
+ if (leaf != initial) {
+ RALocation l = hyp.addState(leaf.isAccepting());
+ hyp.setAccessSequence(l, leaf.getRepresentativePrefix());
+ for (Word sp : leaf.getShortPrefixes()) {
+ locations.put(sp, l);
+ }
+ locations.put(leaf.getRepresentativePrefix(), l);
+ leaves.put(leaf, l);
+ }
+ }
+ }
+
+ private void computeTransitions() {
+ for (CTLeaf leaf : ct.getLeaves()) {
+ for (Prefix prefix : leaf.getPrefixes()) {
+ computeTransition(leaf, prefix);
+ }
+ }
+ }
+
+ private void computeTransition(CTLeaf dest_l, Prefix prefix) {
+ if (prefix.length() < 1) {
+ // empty prefix has no transition
+ return;
+ }
+
+ Prefix dest_rp = dest_l.getRepresentativePrefix();
+ Word src_id = prefix.prefix(prefix.length() - 1);
+ CTLeaf src_l = ct.getLeaf(src_id);
+
+ assert src_l != null : "Source prefix not present in classification tree: " + src_id;
+ assert src_l.getPrefix(src_id) instanceof ShortPrefix : "Source prefix is not short: " + src_id;
+
+ RALocation src_loc = locations.get(src_id);
+ RALocation dest_loc = locations.get(dest_rp);
+
+ assert src_loc != null;
+ assert dest_loc != null;
+
+ ParameterizedSymbol action = prefix.lastSymbol().getBaseSymbol();
+
+ Prefix src_prefix = src_l.getPrefix(src_id);
+ ShortPrefix src_u = (ShortPrefix)(src_prefix instanceof ShortPrefix ?
+ src_prefix :
+ src_l.getRepresentativePrefix());
+
+ // guard
+ Branching b = src_u.getBranching(action);
+ Expression guard = b.getBranches().get(prefix);
+
+ // prefix may not yet have a guard if ct is incomplete, so find a corresponding guard
+ if (guard == null) {
+ for (Expression g : b.getBranches().values()) {
+ DataValue[] vals = prefix.lastSymbol().getParameterValues();
+ Mapping pars = new Mapping<>();
+ for (int i = 0; i < vals.length; i++) {
+ Parameter p = new Parameter(vals[i].getDataType(), i+1);
+ pars.put(p, vals[i]);
+ }
+ pars.putAll(consts);
+ if (solver.isSatisfiable(g, pars)) {
+ guard = g;
+ break;
+ }
+ }
+ }
+
+ assert guard != null : "No guard for prefix " + prefix;
+
+ for (Transition tr : hyp.getTransitions(src_loc, action)) {
+ if (tr.getGuard().equals(guard)) {
+ return;
+ }
+ }
+
+ // rename guard to use register mapping of predecessor prefix
+ ReplacingValuesVisitor rvv = new ReplacingValuesVisitor();
+ RegisterAssignment srcAssign = src_u.getAssignment();
+ RegisterAssignment rpAssign = src_l.getRepresentativePrefix().getAssignment();
+ RegisterAssignment srcAssignRemapped = srcAssign.relabel(registerRemapping(srcAssign, rpAssign, src_u.getRpBijection()));
+ guard = rvv.apply(guard, srcAssignRemapped);
+
+ // compute register assignment
+ RegisterAssignment destAssign = dest_rp.getAssignment();
+ Bijection remapping = prefix.getRpBijection();
+ Assignment assign = AutomatonBuilder.computeAssignment(prefix, srcAssignRemapped, destAssign, remapping);
+
+ Transition t = createTransition(action, guard, src_loc, dest_loc, assign);
+
+ if (t != null) {
+ hyp.addTransition(src_loc, action, t);
+ hyp.setTransitionSequence(t, prefix);
+ }
+ }
+
+ private Transition createTransition(ParameterizedSymbol action, Expression guard,
+ RALocation src_loc, RALocation dest_loc, Assignment assignment) {
+ if (ioMode && !dest_loc.isAccepting()) {
+ return null;
+ }
+
+ if (!ioMode || !(action instanceof OutputSymbol)) {
+ // create input transition
+ return new Transition(action, guard, src_loc, dest_loc, assignment);
+ }
+
+ // this is an output transition
+
+ Expression expr = guard;
+
+ VarMapping outmap = new VarMapping<>();
+ analyzeExpression(expr, outmap);
+
+ Set fresh = new LinkedHashSet<>();
+ ParameterGenerator pgen = new ParameterGenerator();
+ for (DataType t : action.getPtypes()) {
+ Parameter p = pgen.next(t);
+ if (!outmap.containsKey(p)) {
+ fresh.add(p);
+ }
+ }
+
+ OutputMapping outMap = new OutputMapping(fresh, outmap);
+
+ return new OutputTransition(ExpressionUtil.TRUE,
+ outMap, (OutputSymbol) action, src_loc, dest_loc, assignment);
+ }
+
+
+ private void analyzeExpression(Expression expr,
+ VarMapping outmap) {
+
+ if (expr instanceof PropositionalCompound pc) {
+ analyzeExpression(pc.getLeft(), outmap);
+ analyzeExpression(pc.getRight(), outmap);
+ }
+ else if (expr instanceof NumericBooleanExpression nbe) {
+ if (nbe.getComparator() == NumericComparator.EQ) {
+ // FIXME: this is unchecked!
+ SymbolicDataValue left = (SymbolicDataValue) nbe.getLeft();
+ SymbolicDataValue right = (SymbolicDataValue) nbe.getRight();
+
+ Parameter p = null;
+ SymbolicDataValue sv = null;
+
+ if (left instanceof Parameter) {
+ if (right instanceof Parameter) {
+ throw new UnsupportedOperationException("not implemented yet.");
+ }
+ else {
+ p = (Parameter) left;
+ sv = right;
+ }
+ }
+ else {
+ p = (Parameter) right;
+ sv = left;
+ }
+
+ outmap.put(p, sv);
+ }
+ }
+ }
+
+ private VarMapping registerRemapping(RegisterAssignment raa, RegisterAssignment rab, Bijection bijection) {
+ VarMapping ret = new VarMapping<>();
+
+ for (Map.Entry be : bijection.entrySet()) {
+ Register replace = raa.get(be.getKey());
+ Register by = rab.get(be.getValue());
+ ret.put(replace, by);
+ }
+
+ return ret;
+ }
+}
diff --git a/src/main/java/de/learnlib/ralib/ct/CTBranch.java b/src/main/java/de/learnlib/ralib/ct/CTBranch.java
new file mode 100644
index 00000000..0d087902
--- /dev/null
+++ b/src/main/java/de/learnlib/ralib/ct/CTBranch.java
@@ -0,0 +1,71 @@
+package de.learnlib.ralib.ct;
+
+import java.util.Set;
+
+import de.learnlib.ralib.data.Bijection;
+import de.learnlib.ralib.data.DataValue;
+import de.learnlib.ralib.data.util.RemappingIterator;
+import de.learnlib.ralib.smt.ConstraintSolver;
+
+/**
+ * Branch forming an edge between two nodes in a {@link ClassificationTree}.
+ * The branch is labeled by the representative path of the first prefix which
+ * was sifted through the branch.
+ *
+ * @author fredrik
+ *
+ * @see CTNode
+ * @see CTPath
+ */
+public class CTBranch {
+ private final CTNode child;
+
+ private final CTPath reprPath;
+
+ public CTBranch(CTPath path, CTNode child) {
+ this.reprPath = path;
+ this.child = child;
+ }
+
+ public CTPath getRepresentativePath() {
+ return reprPath;
+ }
+
+ public CTNode getChild() {
+ return child;
+ }
+
+ /**
+ * Check whether the SDTs of {@code other} are equivalent to the SDTs of {@code this}.
+ * under the same {@link Bijection}. If so, returns the {@code Bijection} under which the {@code SDT}s
+ * are equivalent.
+ *
+ * @param other the path to compare for equivalence
+ * @param solver constraint solver to use when comparing for equivalence
+ * @return {@code Bijection} under which the {@code SDT}s of {@code other} are equivalent to those of {@code this}, or {@code null} if the {@code SDT}s are not equivalent.
+ * @see SDT
+ */
+ public Bijection matches(CTPath other, ConstraintSolver solver) {
+ if (!reprPath.typeSizesMatch(other)) {
+ return null;
+ }
+
+ Set regs = reprPath.getMemorable();
+ Set otherRegs = other.getMemorable();
+
+ RemappingIterator it = new RemappingIterator<>(otherRegs, regs);
+
+ for (Bijection vars : it) {
+ if (reprPath.isEquivalent(other, vars, solver)) {
+ return vars;
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ return child.toString();
+ }
+}
diff --git a/src/main/java/de/learnlib/ralib/ct/CTHypothesis.java b/src/main/java/de/learnlib/ralib/ct/CTHypothesis.java
new file mode 100644
index 00000000..e2938965
--- /dev/null
+++ b/src/main/java/de/learnlib/ralib/ct/CTHypothesis.java
@@ -0,0 +1,181 @@
+package de.learnlib.ralib.ct;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+import de.learnlib.ralib.automata.Assignment;
+import de.learnlib.ralib.automata.InputTransition;
+import de.learnlib.ralib.automata.RALocation;
+import de.learnlib.ralib.automata.RARun;
+import de.learnlib.ralib.automata.Transition;
+import de.learnlib.ralib.automata.output.OutputMapping;
+import de.learnlib.ralib.automata.output.OutputTransition;
+import de.learnlib.ralib.data.Constants;
+import de.learnlib.ralib.data.ParameterValuation;
+import de.learnlib.ralib.data.RegisterValuation;
+import de.learnlib.ralib.data.VarMapping;
+import de.learnlib.ralib.learning.Hypothesis;
+import de.learnlib.ralib.words.InputSymbol;
+import de.learnlib.ralib.words.OutputSymbol;
+import de.learnlib.ralib.words.PSymbolInstance;
+import de.learnlib.ralib.words.ParameterizedSymbol;
+import gov.nasa.jpf.constraints.util.ExpressionUtil;
+import net.automatalib.word.Word;
+
+/**
+ * Hypothesis constructed from a {@link ClassificationTree}. Maintains a mapping between
+ * locations in the hypothesis and leaf nodes of the {@code ClassificationTree} from which the
+ * hypothesis was constructed.
+ *
+ * @author fredrik
+ * @see Hypothesis
+ */
+public class CTHypothesis extends Hypothesis {
+
+ private final BiMap leaves;
+ private RALocation sink = null;
+ private final boolean ioMode;
+
+ public CTHypothesis(Constants consts, int leaves, boolean ioMode) {
+ super(consts);
+ this.ioMode = ioMode;
+ this.leaves = HashBiMap.create(leaves);
+ }
+
+ public CTHypothesis(Constants consts, Map leaves, boolean ioMode) {
+ super(consts);
+ this.ioMode = ioMode;
+ this.leaves = HashBiMap.create(leaves.size());
+ this.leaves.putAll(leaves);
+ }
+
+ public void putLeaves(Map leaves) {
+ this.leaves.putAll(leaves);
+ }
+
+ public void setSink(RALocation sink) {
+ this.sink = sink;
+ }
+
+ public RALocation getSink() {
+ return sink;
+ }
+
+ @Override
+ public @Nullable RALocation getSuccessor(RALocation state, ParameterizedSymbol input) {
+ return super.getSuccessor(state, input);
+ }
+
+ /**
+ * Get location corresponding to {@code leaf}.
+ *
+ * @param leaf
+ * @return {@link RALocation} corresponding to {@code leaf}
+ */
+ public RALocation getLocation(CTLeaf leaf) {
+ return leaves.get(leaf);
+ }
+
+ /**
+ * Get leaf node of {@link ClassificationTree} from which this hypothesis was constructed.
+ * which corresponds to {@code location}.
+ *
+ * @param location
+ * @return leaf node corresponding to {@code location}
+ * @see CTLeaf
+ * @see RALocation
+ */
+ public CTLeaf getLeaf(RALocation location) {
+ return leaves.inverse().get(location);
+ }
+
+ @Override
+ protected List getTransitions(Word dw) {
+ List tseq = new LinkedList<>();
+ RARun run = getRun(dw);
+ for (int i = 1; i <= dw.size(); i++) {
+ tseq.add(run.getRATransition(i));
+ }
+ return tseq;
+ }
+
+ @Override
+ public RALocation getLocation(Word dw) {
+ return getRun(dw).getLocation(dw.size());
+ }
+
+ @Override
+ public RARun getRun(Word word) {
+ int n = word.length();
+ RALocation[] locs = new RALocation[n+1];
+ RegisterValuation[] vals = new RegisterValuation[n+1];
+ PSymbolInstance[] symbols = new PSymbolInstance[n];
+ Transition[] transitions = new Transition[n];
+
+ locs[0] = getInitialState();
+ vals[0] = new RegisterValuation();
+
+ for (int i = 0; i < n; i++) {
+ symbols[i] = word.getSymbol(i);
+ ParameterValuation pars = ParameterValuation.fromPSymbolInstance(symbols[i]);
+
+ Collection candidates = locs[i].getOut(symbols[i].getBaseSymbol());
+ if (candidates == null) {
+ return null;
+ }
+
+ boolean found = false;
+ boolean output = candidates.isEmpty() ? false :
+ candidates.iterator().next() instanceof OutputTransition;
+
+ for (Transition t : candidates) {
+ if (t.isEnabled(vals[i], pars, constants)) {
+ transitions[i] = t;
+ vals[i+1] = t.valuation(vals[i], pars, constants);
+ locs[i+1] = t.getDestination();
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ // in IO automata, invalid sequences go to sink
+ if (ioMode) {
+ if ((symbols[i].getBaseSymbol() instanceof OutputSymbol && (output || candidates.isEmpty()))
+ || locs[i].equals(sink)) {
+ vals[i+1] = new RegisterValuation();
+ locs[i+1] = getSink();
+ transitions[i] = createSinkTransition(locs[i], locs[i+1], symbols[i].getBaseSymbol());
+ }
+ } else {
+ return null;
+ }
+ }
+ }
+
+ return new RARun(locs, vals, symbols, transitions);
+ }
+
+ private Transition createSinkTransition(RALocation src, RALocation dest, ParameterizedSymbol ps) {
+ if (ps instanceof OutputSymbol) {
+ return new OutputTransition(new OutputMapping(),
+ (OutputSymbol) ps,
+ src, dest,
+ new Assignment(new VarMapping<>()));
+ }
+ if (ps instanceof InputSymbol) {
+ return new InputTransition(ExpressionUtil.TRUE,
+ (InputSymbol) ps,
+ src, dest,
+ new Assignment(new VarMapping<>()));
+ }
+ throw new IllegalArgumentException("Not input or output symbol: " + ps);
+ }
+}
diff --git a/src/main/java/de/learnlib/ralib/ct/CTInnerNode.java b/src/main/java/de/learnlib/ralib/ct/CTInnerNode.java
new file mode 100644
index 00000000..0ff533a8
--- /dev/null
+++ b/src/main/java/de/learnlib/ralib/ct/CTInnerNode.java
@@ -0,0 +1,151 @@
+package de.learnlib.ralib.ct;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import de.learnlib.ralib.data.Bijection;
+import de.learnlib.ralib.data.DataValue;
+import de.learnlib.ralib.learning.SymbolicSuffix;
+import de.learnlib.ralib.oracles.TreeOracle;
+import de.learnlib.ralib.smt.ConstraintSolver;
+import de.learnlib.ralib.words.PSymbolInstance;
+import de.learnlib.ralib.words.ParameterizedSymbol;
+import net.automatalib.word.Word;
+
+/**
+ * Inner node of a {@link ClassificationTree}, containing a {@link SymbolicSuffix}.
+ * Maintains a set of branches to child nodes.
+ *
+ * @author fredrik
+ * @see CTBranch
+ * @see CTNode
+ */
+public class CTInnerNode extends CTNode {
+ private final SymbolicSuffix suffix;
+ private final List branches;
+
+ public CTInnerNode(CTNode parent, SymbolicSuffix suffix) {
+ super(parent);
+ this.suffix = suffix;
+ branches = new ArrayList<>();
+ }
+
+ public SymbolicSuffix getSuffix() {
+ return suffix;
+ }
+
+ public List getBranches() {
+ return branches;
+ }
+
+ protected CTBranch getBranch(CTNode child) {
+ for (CTBranch b : getBranches()) {
+ if (b.getChild() == child) {
+ return b;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected CTLeaf sift(Prefix prefix, TreeOracle oracle, ConstraintSolver solver, boolean ioMode) {
+ CTPath path = CTPath.computePath(oracle, prefix, getSuffixes(), ioMode);
+
+ // find a matching branch and sift to child
+ for (CTBranch b : branches) {
+ Bijection vars = b.matches(path, solver);
+ if (vars != null) {
+ prefix = new Prefix(prefix, vars, path);
+ return b.getChild().sift(prefix, oracle, solver, ioMode);
+ }
+ }
+
+ // no child with equivalent SDTs, create a new leaf
+ prefix = new Prefix(prefix, path);
+ CTLeaf leaf = new CTLeaf(prefix, this);
+ CTBranch branch = new CTBranch(path, leaf);
+ branches.add(branch);
+ return leaf;
+ }
+
+ /**
+ * Replace {@code leaf} with a new {@link CTInnerNode} containing {@code suffix}.
+ * The prefixes in {@code leaf} will be sifted into this new inner node.
+ * If this sifting creates a new {@link CTLeaf}, the first prefix to be sifted
+ * into that leaf node will be made the representative prefix.
+ *
+ * @param leaf
+ * @param suffix
+ * @param oracle
+ * @param solver
+ * @param ioMode
+ * @param inputs
+ * @return a mapping of prefixes in {@code leaf} to their new leaf nodes
+ */
+ protected Map, CTLeaf> refine(CTLeaf leaf, SymbolicSuffix suffix, TreeOracle oracle, ConstraintSolver solver, boolean ioMode, ParameterizedSymbol[] inputs) {
+ CTBranch b = getBranch(leaf);
+ assert b != null : "Node is not the parent of leaf " + leaf;
+ assert !getSuffixes().contains(suffix) : "Duplicate suffix: " + suffix;
+
+ Set shorts = leaf.getShortPrefixes();
+
+ // replace leaf with a new inner node, with same path as leaf
+ CTInnerNode newNode = new CTInnerNode(this, suffix);
+ CTBranch newBranch = new CTBranch(b.getRepresentativePath(), newNode);
+ branches.remove(b);
+ branches.add(newBranch);
+
+ // sift leaf's RP into the new inner node
+ Map, CTLeaf> leaves = new LinkedHashMap<>();
+ CTLeaf l = sift(leaf.getRepresentativePrefix(), oracle, solver, ioMode);
+ leaves.put(leaf.getRepresentativePrefix(), l);
+
+ // resift leaf's prefixes into this node (except the RP, which was already sifted)
+ Set prefixes = new LinkedHashSet<>(leaf.getPrefixes());
+ prefixes.remove(leaf.getRepresentativePrefix());
+
+ for (Prefix u : prefixes) {
+ l = sift(u, oracle, solver, ioMode);
+ leaves.put(u, l);
+ }
+
+ // make sure all short prefixes of leaf are still short
+ for (ShortPrefix u : shorts) {
+ if (!(u instanceof ShortPrefix)) {
+ leaves.get(u).elevatePrefix(u, oracle, inputs);
+ }
+ }
+
+ return leaves;
+ }
+
+ @Override
+ public List getSuffixes() {
+ List suffixes = new ArrayList<>();
+ suffixes.add(suffix);
+ if (getParent() == null) {
+ return suffixes;
+ }
+
+ suffixes.addAll(getParent().getSuffixes());
+ return suffixes;
+ }
+
+ /**
+ * @return {@code false}
+ */
+ @Override
+ public boolean isLeaf() {
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "(" + suffix + ")";
+ }
+
+}
diff --git a/src/main/java/de/learnlib/ralib/ct/CTLeaf.java b/src/main/java/de/learnlib/ralib/ct/CTLeaf.java
new file mode 100644
index 00000000..10621ed6
--- /dev/null
+++ b/src/main/java/de/learnlib/ralib/ct/CTLeaf.java
@@ -0,0 +1,213 @@
+package de.learnlib.ralib.ct;
+
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+import de.learnlib.ralib.data.Bijection;
+import de.learnlib.ralib.data.DataValue;
+import de.learnlib.ralib.learning.LocationComponent;
+import de.learnlib.ralib.learning.PrefixContainer;
+import de.learnlib.ralib.learning.SymbolicSuffix;
+import de.learnlib.ralib.oracles.Branching;
+import de.learnlib.ralib.oracles.TreeOracle;
+import de.learnlib.ralib.smt.ConstraintSolver;
+import de.learnlib.ralib.words.PSymbolInstance;
+import de.learnlib.ralib.words.ParameterizedSymbol;
+import net.automatalib.word.Word;
+
+/**
+ * Leaf node of a {@link ClassificationTree}, containing a number of prefixes.
+ * A leaf node must contain at least one prefix, which is the representative
+ * prefix of the leaf. Any number of prefixes may also be short prefixes.
+ *
+ * @author fredrik
+ * @see Prefix
+ * @see ShortPrefix
+ */
+public class CTLeaf extends CTNode implements LocationComponent {
+ private Prefix rp;
+ private final Set shortPrefixes;
+ private final Set prefixes;
+
+ public CTLeaf(Prefix rp, CTNode parent) {
+ super(parent);
+ if (parent == null) {
+ throw new IllegalArgumentException("A leaf must have a parent");
+ }
+ this.rp = rp;
+ shortPrefixes = new LinkedHashSet<>();
+ prefixes = new LinkedHashSet<>();
+ prefixes.add(rp);
+ if (rp instanceof ShortPrefix) {
+ ((ShortPrefix) rp).updateBranching();
+ }
+ }
+
+ @Override
+ public List getSuffixes() {
+ return getParent().getSuffixes();
+ }
+
+ /**
+ * @return all prefixes contained within this leaf
+ */
+ public Set getPrefixes() {
+ return prefixes;
+ }
+
+ /**
+ * Helper method for retrieving the {@link Prefix} representation of a {@code Word}.
+ *
+ * @param u
+ * @return the {@code Prefix} form of {@code u} if {@code u} is in this leaf, or {@code null} otherwise
+ */
+ public Prefix getPrefix(Word u) {
+ for (Prefix p : prefixes) {
+ if (p.equals(u)) {
+ return p;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @return all short prefixes in this leaf
+ */
+ public Set getShortPrefixes() {
+ return shortPrefixes;
+ }
+
+ /**
+ * @return the representative prefix of this leaf
+ */
+ public Prefix getRepresentativePrefix() {
+ return rp;
+ }
+
+ /**
+ * @return {@code true}
+ */
+ @Override
+ public boolean isLeaf() {
+ return true;
+ }
+
+ /**
+ * Returns {@code true} if this leaf is accepting, i.e., if a tree query for this
+ * leaf's representative prefix with the empty suffix (ε) is accepting.
+ *
+ * @return {@code true} if this leaf corresponds to an accepting location
+ */
+ public boolean isAccepting() {
+ return rp.getPath().isAccepting();
+ }
+
+ /**
+ * Adds {@code prefix} to this leaf. If {@code prefix} is a short prefix, this method updates
+ * the branching of {@code prefix} to include initial guards from the conjunction of all SDTs
+ * along its path.
+ *
+ * @param prefix that will be added to this leaf
+ * @param oracle unused
+ * @param solver unused
+ * @param ioMode unused
+ * @return {@code this}
+ */
+ @Override
+ protected CTLeaf sift(Prefix prefix, TreeOracle oracle, ConstraintSolver solver, boolean ioMode) {
+ prefixes.add(prefix);
+ if (prefix instanceof ShortPrefix) {
+ ShortPrefix sp = (ShortPrefix) prefix;
+ shortPrefixes.add(sp);
+ sp.updateBranching();
+ }
+ return this;
+ }
+
+ /**
+ * Elevate {@code u} to a short prefix by converting it to a {@link ShortPrefix}. The branching
+ * of {@code u} will be initialized to reflect the initial guards of the conjunction of SDTs
+ * along its path.
+ *
+ * @param u the prefix to elevate
+ * @param oracle tree oracle to use for updating the branching
+ * @param inputs all input symbols
+ * @return {@code u} as a {@code ShortPrefix}
+ * @see Branching
+ * @see SDT
+ * @see CTPath
+ */
+ protected ShortPrefix elevatePrefix(Word u, TreeOracle oracle, ParameterizedSymbol ... inputs) {
+ Prefix prefix = getPrefix(u);
+ assert !(prefix instanceof ShortPrefix) : "Prefix is already short: " + prefix;
+ prefixes.remove(prefix);
+ ShortPrefix sp = new ShortPrefix(prefix, oracle, inputs);
+ shortPrefixes.add(sp);
+ prefixes.add(sp);
+
+ if (prefix == rp) {
+ rp = sp;
+ }
+ return sp;
+ }
+
+ @Override
+ public String toString() {
+ String str = "{RP:[" + rp.toString() + "]";
+ for (Prefix u : prefixes) {
+ if (u != rp) {
+ str = str + ", " + (u instanceof ShortPrefix ? "SP:[" : "[") + u.toString() + "]";
+ }
+ }
+ return str + "}";
+ }
+
+ /**
+ * @return this leaf's representative prefix
+ */
+ @Override
+ public Word getAccessSequence() {
+ return getRepresentativePrefix();
+ }
+
+ @Override
+ public Bijection getRemapping(PrefixContainer r) {
+ assert r instanceof Prefix;
+ return ((Prefix) r).getRpBijection();
+ }
+
+ /**
+ * Get the branching for symbol {@code action} of the representative prefix.
+ * Requires that the representative prefix is a {@link ShortPrefix}.
+ *
+ * @param action
+ * @return {@code Branching} of {@code action} for the representative prefix of this leaf
+ */
+ @Override
+ public Branching getBranching(ParameterizedSymbol action) {
+ assert rp instanceof ShortPrefix : "Representative prefix is not a short prefix";
+ return ((ShortPrefix) rp).getBranching(action);
+ }
+
+ /**
+ * @return the representative prefix of this leaf
+ */
+ @Override
+ public PrefixContainer getPrimePrefix() {
+ return getRepresentativePrefix();
+ }
+
+ /**
+ * @return {@code Collection} of all prefixes in this leaf other than the representative prefix
+ */
+ @Override
+ public Collection getOtherPrefixes() {
+ Collection prefs = new LinkedList<>();
+ prefs.addAll(prefixes);
+ prefs.remove(rp);
+ return prefs;
+ }
+}
diff --git a/src/main/java/de/learnlib/ralib/ct/CTNode.java b/src/main/java/de/learnlib/ralib/ct/CTNode.java
new file mode 100644
index 00000000..ec9c9a39
--- /dev/null
+++ b/src/main/java/de/learnlib/ralib/ct/CTNode.java
@@ -0,0 +1,56 @@
+package de.learnlib.ralib.ct;
+
+import java.util.List;
+
+import de.learnlib.ralib.learning.SymbolicSuffix;
+import de.learnlib.ralib.oracles.TreeOracle;
+import de.learnlib.ralib.smt.ConstraintSolver;
+
+/**
+ * Node of a {@link ClassificationTree}.
+ *
+ * @author fredrik
+ */
+public abstract class CTNode {
+ private final CTNode parent;
+
+ public CTNode(CTNode parent) {
+ this.parent = parent;
+ }
+
+ /**
+ *
+ * @return immediate ancestor of this node
+ */
+ public CTNode getParent() {
+ return parent;
+ }
+
+ /**
+ * @return all symbolic suffixes from all the ancestor nodes.
+ */
+ public abstract List getSuffixes();
+
+ /**
+ *
+ * @return {@code true} if this node is a {@link CTLeaf}
+ */
+ public abstract boolean isLeaf();
+
+ /**
+ * Sifts {@code prefix} into the node. If this is a {@link CTLeaf}, adds {@code prefix} to it.
+ * If this is a {@link CTInnerNode}, a tree query is made for {@code prefix} with the node's
+ * {@link SymbolicSuffix}, after which {@code prefix} is sifted to the child whose path
+ * matches the tree query, determined with a call to {@link CTBranch#matches(CTPath, ConstraintSolver)}.
+ * If no such child exists, this method creates a new {@code CTLeaf} and adds {@code prefix}
+ * to it as its representative prefix.
+ *
+ * @param prefix the prefix to sift through this node
+ * @param oracle the {@link TreeOracle} to use when making tree queries
+ * @param solver the {@link ConstraintSolver} to use for comparing paths for equivalence
+ * @param ioMode {@code true} if using IO mode
+ * @return the {@code CTLeaf} node to which {@code prefix} is sifted
+ * @see CTPath
+ */
+ protected abstract CTLeaf sift(Prefix prefix, TreeOracle oracle, ConstraintSolver solver, boolean ioMode);
+}
diff --git a/src/main/java/de/learnlib/ralib/ct/CTPath.java b/src/main/java/de/learnlib/ralib/ct/CTPath.java
new file mode 100644
index 00000000..3cdf406c
--- /dev/null
+++ b/src/main/java/de/learnlib/ralib/ct/CTPath.java
@@ -0,0 +1,164 @@
+package de.learnlib.ralib.ct;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import de.learnlib.ralib.data.Bijection;
+import de.learnlib.ralib.data.DataValue;
+import de.learnlib.ralib.data.util.DataUtils;
+import de.learnlib.ralib.learning.SymbolicSuffix;
+import de.learnlib.ralib.learning.rastar.RaStar;
+import de.learnlib.ralib.oracles.TreeOracle;
+import de.learnlib.ralib.smt.ConstraintSolver;
+import de.learnlib.ralib.theory.SDT;
+
+/**
+ * This data structure stores the SDTs from tree queries for a prefix along a path
+ * in a {@link ClassificationTree}. It contains much of the same functionality as
+ * {@link Row}, but adapted for use with classification trees.
+ *
+ * @author fredrik
+ * @author falk
+ * @see Row
+ */
+public class CTPath {
+ private final Map sdts;
+ private final MemorableSet memorable;
+
+ private boolean ioMode;
+
+ public CTPath(boolean ioMode) {
+ this.sdts = new LinkedHashMap<>();
+ this.memorable = new MemorableSet();
+ this.ioMode = ioMode;
+ }
+
+ public void putSDT(SymbolicSuffix suffix, SDT sdt) {
+ assert !sdts.containsKey(suffix);
+ sdts.put(suffix, sdt);
+ memorable.addAll(sdt.getDataValues());
+ }
+
+ public MemorableSet getMemorable() {
+ return memorable;
+ }
+
+ public SDT getSDT(SymbolicSuffix suffix) {
+ return sdts.get(suffix);
+ }
+
+ public Map getSDTs() {
+ return sdts;
+ }
+
+ public boolean isAccepting() {
+ SDT s = sdts.get(RaStar.EMPTY_SUFFIX);
+ return s.isAccepting();
+ }
+
+ /**
+ * Checks whether two paths are equivalent under {@code renaming}.
+ *
+ * @param other
+ * @param renaming
+ * @param solver
+ * @return {@code true} if the SDTs of {@code this} are equivalent to those of {@code other} under {@code renaming}
+ */
+ public boolean isEquivalent(CTPath other, Bijection renaming, ConstraintSolver solver) {
+ if (!typeSizesMatch(other)) {
+ return false;
+ }
+
+ if (!memorable.equals(other.memorable.relabel(renaming))) {
+ return false;
+ }
+
+ for (Map.Entry e : sdts.entrySet()) {
+ SDT sdt1 = e.getValue();
+ SDT sdt2 = other.sdts.get(e.getKey());
+
+ if (!sdt1.isEquivalent(sdt2, renaming)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Checks whether the SDTs of {@code this} and {@code other} have the same number of
+ * data value types.
+ *
+ * @param other
+ * @return {@code true} if the number of types match for the SDTs of {@code this} and {@code other}
+ */
+ protected boolean typeSizesMatch(CTPath other) {
+ if (!DataUtils.typedSize(memorable).equals(DataUtils.typedSize(other.memorable))) {
+ return false;
+ }
+
+ for (Map.Entry e : sdts.entrySet()) {
+ SDT sdt1 = e.getValue();
+ SDT sdt2 = other.sdts.get(e.getKey());
+
+ if (ioMode) {
+ if (sdt1 == null && sdt2 == null) {
+ continue;
+ }
+
+ if (sdt1 == null || sdt2 == null) {
+ return false;
+ }
+ }
+
+ if (!equalTypeSizes(sdt1.getDataValues(), sdt2.getDataValues())) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static boolean equalTypeSizes(Set s1, Set s2) {
+ return DataUtils.typedSize(s1).equals(DataUtils.typedSize(s2));
+ }
+
+ /**
+ * Computes the path for {@code prefix} by computing SDTs for each suffix in {@code suffixes}.
+ * The SDTs already contained within the path {@code prefix} will be copied to the new path.
+ * Remaining SDTs are computed via tree queries.
+ *
+ * @param oracle the oracle to use for tree queries
+ * @param prefix the prefix for which the new path is to be computed
+ * @param suffixes the suffixes along the path
+ * @param ioMode {@code true} if the language being learned is an IO language
+ * @return a {@code CTPath} containing SDTs for each suffix in {@code suffixes}
+ */
+ public static CTPath computePath(TreeOracle oracle, Prefix prefix, List suffixes, boolean ioMode) {
+ CTPath r = new CTPath(ioMode);
+ SDT sdt = prefix.getSDT(RaStar.EMPTY_SUFFIX);
+ sdt = sdt == null ? oracle.treeQuery(prefix, RaStar.EMPTY_SUFFIX) : sdt;
+ r.putSDT(RaStar.EMPTY_SUFFIX, sdt);
+ for (SymbolicSuffix s : suffixes) {
+ sdt = prefix.getSDT(s);
+ if (sdt == null) {
+ sdt = oracle.treeQuery(prefix, s);
+ }
+
+ if (r.getSDT(s) == null) {
+ r.putSDT(s, sdt);
+ }
+ }
+
+ return r;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ for (Map.Entry e : this.sdts.entrySet()) {
+ sb.append("[").append(e.getKey()).append("->").append(e.getValue().toString().replaceAll("\\s+", " ")).append("] ");
+ }
+ return sb.toString();
+ }
+}
diff --git a/src/main/java/de/learnlib/ralib/ct/ClassificationTree.java b/src/main/java/de/learnlib/ralib/ct/ClassificationTree.java
new file mode 100644
index 00000000..d72c7ab8
--- /dev/null
+++ b/src/main/java/de/learnlib/ralib/ct/ClassificationTree.java
@@ -0,0 +1,811 @@
+package de.learnlib.ralib.ct;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import de.learnlib.ralib.data.Bijection;
+import de.learnlib.ralib.data.Constants;
+import de.learnlib.ralib.data.DataValue;
+import de.learnlib.ralib.data.Mapping;
+import de.learnlib.ralib.data.ParameterValuation;
+import de.learnlib.ralib.data.SymbolicDataValue;
+import de.learnlib.ralib.data.SymbolicDataValue.Parameter;
+import de.learnlib.ralib.data.SymbolicDataValue.Register;
+import de.learnlib.ralib.data.util.RemappingIterator;
+import de.learnlib.ralib.data.util.SymbolicDataValueGenerator.ParameterGenerator;
+import de.learnlib.ralib.learning.SymbolicSuffix;
+import de.learnlib.ralib.learning.rastar.RaStar;
+import de.learnlib.ralib.oracles.Branching;
+import de.learnlib.ralib.oracles.TreeOracle;
+import de.learnlib.ralib.oracles.mto.OptimizedSymbolicSuffixBuilder;
+import de.learnlib.ralib.oracles.mto.SymbolicSuffixRestrictionBuilder;
+import de.learnlib.ralib.smt.ConstraintSolver;
+import de.learnlib.ralib.smt.ReplacingValuesVisitor;
+import de.learnlib.ralib.theory.SDT;
+import de.learnlib.ralib.words.DataWords;
+import de.learnlib.ralib.words.OutputSymbol;
+import de.learnlib.ralib.words.PSymbolInstance;
+import de.learnlib.ralib.words.ParameterizedSymbol;
+import gov.nasa.jpf.constraints.api.Expression;
+import net.automatalib.word.Word;
+
+/**
+ * Data structure for a classification tree. Implements methods for sifting new prefixes into the tree
+ * and refining the tree with additional symbolic suffixes, as well as closedness and consistency checks.
+ *
+ * @author fredrik
+ */
+public class ClassificationTree {
+ private final CTInnerNode root;
+
+ private final Map, CTLeaf> prefixes;
+ private final Set> shortPrefixes;
+
+ private final ConstraintSolver solver;
+ private final TreeOracle oracle;
+ private final SymbolicSuffixRestrictionBuilder restrBuilder;
+ private final OptimizedSymbolicSuffixBuilder suffixBuilder;
+
+ private final ParameterizedSymbol[] inputs;
+ private final List outputs;
+
+ private final Constants consts;
+
+ private boolean ioMode;
+
+ public ClassificationTree(TreeOracle oracle,
+ ConstraintSolver solver,
+ SymbolicSuffixRestrictionBuilder restrBuilder,
+ OptimizedSymbolicSuffixBuilder suffixBuilder,
+ Constants consts,
+ boolean ioMode,
+ ParameterizedSymbol ... inputs) {
+ this.oracle = oracle;
+ this.solver = solver;
+ this.ioMode = ioMode;
+ this.inputs = inputs;
+ this.restrBuilder = restrBuilder;
+ this.suffixBuilder = suffixBuilder;
+ this.consts = consts;
+
+ prefixes = new LinkedHashMap<>();
+ shortPrefixes = new LinkedHashSet<>();
+ outputs = outputSuffixes(inputs);
+
+ root = new CTInnerNode(null, RaStar.EMPTY_SUFFIX);
+ }
+
+ public Set getLeaves() {
+ return new LinkedHashSet<>(prefixes.values());
+ }
+
+ public CTLeaf getLeaf(Word u) {
+ return prefixes.get(u);
+ }
+
+ public Set> getPrefixes() {
+ return new LinkedHashSet<>(prefixes.keySet());
+ }
+
+ public Set getShortPrefixes() {
+ Set sp = new LinkedHashSet<>();
+ for (CTLeaf leaf : getLeaves()) {
+ sp.addAll(leaf.getShortPrefixes());
+ }
+ assert sp.size() == shortPrefixes.size();
+ assert sp.containsAll(shortPrefixes);
+ return sp;
+ }
+
+ /**
+ * Get all one-symbol extensions of prefix {@code u}.
+ *
+ * @param u
+ * @return set of prefixes which are one-symbol extensions of {@code u}
+ */
+ public Set getExtensions(Word u) {
+ Set extensions = new LinkedHashSet<>();
+ for (ParameterizedSymbol action : inputs) {
+ extensions.addAll(getExtensions(u, action)
+ .stream()
+ .map(w -> getLeaf(w).getPrefix(w))
+ .collect(Collectors.toList()));
+ }
+ return extensions;
+ }
+
+ /**
+ * Get the sink node of the classification tree (IO mode only). The sink node of an IO classification
+ * tree is the leaf node in the rejecting branch of the tree (i.e., the branch for the empty symbolic suffix
+ * which is rejecting. Note that in IO mode, there should only be one rejecting leaf.
+ *
+ * @return an {@code Optional} containing the sink node if one exists, or an empty {@code Optional} otherwise
+ */
+ public Optional getSink() {
+ if (!ioMode) {
+ return Optional.empty();
+ }
+
+ for (CTBranch branch : root.getBranches()) {
+ if (!branch.getRepresentativePath().isAccepting()) {
+ CTNode node = branch.getChild();
+ while (!node.isLeaf()) {
+ CTInnerNode inner = (CTInnerNode) node;
+ List children = inner.getBranches();
+ if (children.isEmpty()) {
+ return Optional.empty();
+ }
+ node = children.stream().findFirst().get().getChild();
+ }
+ return Optional.of((CTLeaf) node);
+ }
+ }
+ return Optional.empty();
+ }
+
+ /**
+ * Get all one-symbol {@code action}-extensions of prefix {@code u}.
+ *
+ * @param u
+ * @param action
+ * @return set of prefixes which are one-symbol extensions of {@code u} with symbol {@code action}
+ */
+ public Set> getExtensions(Word u, ParameterizedSymbol action) {
+ return prefixes.keySet()
+ .stream()
+ .filter(w -> w.length() == u.length() + 1)
+ .filter(w -> w.prefix(w.length() - 1).equals(u) && w.lastSymbol().getBaseSymbol().equals(action))
+ .collect(Collectors.toSet());
+ }
+
+ /**
+ * Initialize the classification tree by sifting the empty prefix.
+ */
+ public void initialize() {
+ sift(RaStar.EMPTY_PREFIX);
+ }
+
+ ///////////////////////////////////////////
+ // OPERATIONS ON THE CLASSIFICATION TREE //
+ ///////////////////////////////////////////
+
+ /**
+ * Sift prefix into the tree. If prefix sifts to a new leaf, it becomes the representative prefix
+ * for that leaf.
+ *
+ * @param u prefix to sift
+ * @return the leaf into which {@code u} has been sifted
+ */
+ public CTLeaf sift(Word u) {
+ Prefix prefix = new Prefix(u, new CTPath(ioMode));
+ CTLeaf leaf = root.sift(prefix, oracle, solver, ioMode);
+ prefixes.put(u, leaf);
+ return leaf;
+ }
+
+ /**
+ * Expands a prefix by turning it into a short prefix. The new short prefix will have
+ * branching information initialized from the initial guards of the conjunction of all its SDTs.
+ *
+ * @param u the prefix to be expanded
+ */
+ public void expand(Word u) {
+ CTLeaf leaf = prefixes.get(u);
+ // sift u into leaf, if not already present
+ if (leaf == null) {
+ leaf = sift(u);
+ }
+ ShortPrefix prefix = leaf.elevatePrefix(u, oracle, inputs);
+ shortPrefixes.add(u);
+
+ // sift one-symbol extensions of u
+ for (ParameterizedSymbol ps : inputs) {
+ Branching b = prefix.getBranching(ps);
+ for (Word ua : b.getBranches().keySet()) {
+ CTLeaf l = sift(ua);
+ prefixes.put(ua, l);
+ }
+ }
+ }
+
+ /**
+ * Refine a {@code leaf} by adding a new inner node with above it. After the new inner node is added,
+ * all prefixes of {@code leaf} will be sifted into the classification tree with a call to
+ * {@link ClassificationTree#sift(Word)}. Any short prefix in {@code leaf} will have its branching
+ * updated.
+ *
+ * @param leaf the leaf to refine
+ * @param suffix the symbolic suffix to be contained within the new inner node
+ */
+ public void refine(CTLeaf leaf, SymbolicSuffix suffix) {
+ CTInnerNode parent = (CTInnerNode) leaf.getParent();
+ assert parent != null;
+ Map, CTLeaf> leaves = parent.refine(leaf, suffix, oracle, solver, ioMode, inputs);
+ prefixes.putAll(leaves);
+
+ // make sure all short prefixes are still short (this may not be the case if a new leaf was created)
+ for (Word sp : shortPrefixes) {
+ CTLeaf l = prefixes.get(sp);
+ Prefix p = l.getPrefix(sp);
+ if (!(p instanceof ShortPrefix)) {
+ l.elevatePrefix(sp, oracle, inputs);
+ }
+ }
+ }
+
+ ///////////////////////
+ // CLOSEDNESS CHECKS //
+ ///////////////////////
+
+ /**
+ * Checks for output closedness, i.e., whether a symbolic suffix for each output symbol is
+ * present for each leaf. If not output closed, add one missing output suffix as a new inner
+ * node with a call to {@link ClassificationTree#refine(CTLeaf, SymbolicSuffix)}.
+ *
+ * @return {@code true} if output closed
+ */
+ public boolean checkOutputClosed() {
+ if (!ioMode) {
+ return true;
+ }
+ return checkOutputClosed(root);
+ }
+
+ private boolean checkOutputClosed(CTNode node) {
+ if (node.isLeaf()) {
+ CTLeaf leaf = (CTLeaf) node;
+ for (SymbolicSuffix v : outputs) {
+ if (!leaf.getSuffixes().contains(v)) {
+ refine(leaf, v);
+ return false;
+ }
+ }
+ return true;
+ } else {
+ CTInnerNode n = (CTInnerNode) node;
+ for (CTBranch b : n.getBranches()) {
+ if (!checkOutputClosed(b.getChild())) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Checks for location closedness, i.e., whether each leaf has at least one short prefix. If
+ * not location closed, this method expands, with a call to {@link ClassificationTree#expand(Word)},
+ * the representative prefix of one leaf which does not have a short prefix.
+ *
+ * @return {@code true} if location closed
+ */
+ public boolean checkLocationClosedness() {
+ for (CTLeaf leaf : getLeaves()) {
+ if (leaf.getShortPrefixes().isEmpty()) {
+ expand(leaf.getRepresentativePrefix());
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Checks for transition closedness, i.e., whether each short prefix has a one-symbol extension
+ * for each guard. If not transition closed, one new one-symbol prefix will be sifted for one
+ * short prefix missing an extension.
+ *
+ * @return {@code true} if transition closed
+ */
+ public boolean checkTransitionClosedness() {
+ for (CTLeaf leaf : getLeaves()) {
+ for (ShortPrefix u : leaf.getShortPrefixes()) {
+ for (ParameterizedSymbol a : inputs) {
+ for (Word ua : u.getBranching(a).getBranches().keySet()) {
+ if (!prefixes.containsKey(ua)) {
+ sift(ua);
+ return false;
+ }
+ }
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Checks for register closedness, i.e., whether for each short prefix {@code u}, the memorable parameters
+ * of {@code u} contain all the memorable parameters of its one-symbol extensions {@code ua(d)}. If not register
+ * closed, a new symbolic suffix {@code av}, formed by prepending the symbol {@code a} of {@code ua(d)} with a
+ * symbolic suffix {@code v} which reveals a missing memorable parameter of {@code ua(d)}, will be added to
+ * the leaf of {@code u}. Note that only one new symbolic suffix will be added, even if there are multiple
+ * short prefixes for which a memorable parameter is missing.
+ *
+ * @return {@code true} if register closed
+ */
+ public boolean checkRegisterClosedness() {
+ for (Map.Entry, CTLeaf> e : prefixes.entrySet()) {
+ Word ua = e.getKey();
+ CTLeaf leaf = e.getValue();
+ if (ua.length() < 1) {
+ continue;
+ }
+ Word u = ua.prefix(ua.size() - 1);
+ Prefix ua_pref = leaf.getPrefix(ua);
+ CTLeaf u_leaf = prefixes.get(u);
+
+ Set ua_mem = leaf.getPrefix(ua).getRegisters();
+ Set u_mem = prefixes.get(u).getPrefix(u).getRegisters();
+ Set a_mem = actionRegisters(ua);
+
+ if (!consistentMemorable(ua_mem, u_mem, a_mem)) {
+ // memorables are missing, find suffix which reveals missing memorables
+ for (SymbolicSuffix v : leaf.getSuffixes()) {
+ Set s_mem = ua_pref.getSDT(v).getDataValues();
+ if (!consistentMemorable(s_mem, u_mem, a_mem)) {
+ DataValue[] missingRegs = missingRegisters(s_mem, u_mem, a_mem); // registers to not optimize away
+ SymbolicSuffix av = extendSuffix(ua, v, missingRegs);
+ refine(u_leaf, av);
+ break;
+ }
+ }
+ return false;
+ }
+ }
+ return true;
+ }
+
+ ////////////////////////
+ // CONSISTENCY CHECKS //
+ ////////////////////////
+
+ /**
+ * Checks for location consistency. This property states that, for each pair of short prefixes {@code u} and {@code v},
+ * each one-symbol extension {@code ua(d)} and {@code va(d)} of {@code u} and {@code v}, corresponding to the same
+ * guard, is in the same leaf. If the classification tree is not location consistent, this method refines the tree with a
+ * new symbolic suffix formed by prepending the symbolic suffix of the lowest common ancestor of {@code u} and {@code v}
+ * by the symbol {@code a} of the one-symbol extensions revealing the inconsistency. Note that only one location inconsistency
+ * will be resolved by this method. To resolve multiple inconsistencies, call the method multiple times.
+ *
+ * @return {@code true} if location consistent
+ */
+ public boolean checkLocationConsistency() {
+ for (CTLeaf l : getLeaves()) {
+ Iterator sp = l.getShortPrefixes().iterator();
+ if (!sp.hasNext()) {
+ continue;
+ }
+ ShortPrefix u = sp.next();
+ while (sp.hasNext()) {
+ ShortPrefix uOther = sp.next();
+ for (ParameterizedSymbol action : inputs) {
+ // loop over one-symbol extensions
+ for (Map.Entry, Expression> uEntry : u.getBranching(action).getBranches().entrySet()) {
+ // rename guard to make parameters consistent with RP
+ Word uExtension = uEntry.getKey();
+ Expression uGuard = uEntry.getValue();
+ Bijection uRenaming = u.getRpBijection().compose(uOther.getRpBijection().inverse());
+
+ ReplacingValuesVisitor rvv = new ReplacingValuesVisitor();
+ Expression uGuardRenamed = rvv.apply(uGuard, uRenaming.toVarMapping());
+
+ // find equivalent one-symbol extension for RP
+ Optional> uOtherExtension = uOther.getBranching(action).getPrefix(uGuardRenamed, solver);
+ assert uOtherExtension.isPresent();
+
+ CTLeaf uExtLeaf = getLeaf(uExtension);
+ CTLeaf uOtherExtLeaf = getLeaf(uOtherExtension.get());
+ if (uExtLeaf != uOtherExtLeaf) {
+ // inconsistent, refine leaf with extended suffix
+ SymbolicSuffix v = lca(uExtLeaf, uOtherExtLeaf).getSuffix();
+ SymbolicSuffix av = extendSuffix(uExtension, uOtherExtension.get(), v);
+ refine(l, av);
+ return false;
+ }
+ }
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Checks for transition consistency. There are two types of transition consistency, (a) and (b).
+ * This method checks for both. The transition consistency (a) property states that all one-symbol
+ * extensions {@code ua(d)} of a prefix {@code u} which satisfy the same guard are in the same leaf.
+ * Transition consistency (b) states that all one-symbol extensions {@code ua(d)} of a prefix {@code u}
+ * that satisfy the same guard and are in the same leaf should have equivalent SDTs under identity renaming.
+ * If either property is not satisfied, the classification tree will be refined by adding a new inner
+ * node to the leaf of {@code u} with a symbolic suffix formed by prepending the symbolic suffix
+ * revealing the inconsistency by the symbol {@code a} of the one-symbol extension.
+ * Note that only one inconsistency will be resolved. Multiple inconsistencies can be resolved through
+ * multiple calls to this method.
+ *
+ * @return {@code true} if transition consistent
+ */
+ public boolean checkTransitionConsistency() {
+ for (ShortPrefix u : getShortPrefixes()) {
+ for (ParameterizedSymbol action : inputs) {
+ Set> extensions = getExtensions(u, action);
+ for (Map.Entry, Expression> e : u.getBranching(action).getBranches().entrySet()) {
+ Word uA = e.getKey();
+ Expression g = e.getValue();
+ for (Word uB : extensions) {
+ if (uB.equals(uA)) {
+ continue;
+ }
+
+ // check if guard for uA is satisfiable under mapping of uB
+ Mapping mapping = new Mapping<>();
+ mapping.putAll(actionValuation(uB));
+ mapping.putAll(consts);
+ if (solver.isSatisfiable(g, mapping)) {
+ // check transition consistency A
+ Optional av = transitionConsistentA(uA, uB);
+ if (av.isEmpty()) {
+ // check transition consistency B
+ av = transitionConsistentB(uA, uB);
+ }
+ if (av.isPresent()) {
+ refine(getLeaf(u), av.get());
+ return false;
+ }
+ }
+ }
+ }
+ }
+ }
+ return true;
+ }
+
+ private Optional transitionConsistentA(Word uA, Word uB) {
+ Word u = uA.prefix(uA.length() - 1);
+ CTLeaf uALeaf = getLeaf(uA);
+ CTLeaf uBLeaf = getLeaf(uB);
+ if (uALeaf != uBLeaf) {
+ CTLeaf uLeaf = getLeaf(u);
+ assert uLeaf != null : "Prefix is not short: " + u;
+ SymbolicSuffix v = lca(uALeaf, uBLeaf).getSuffix();
+ SymbolicSuffix av = extendSuffix(uA, uB, v);
+ return Optional.of(av);
+ }
+ return Optional.empty();
+ }
+
+ private Optional transitionConsistentB(Word uA, Word uB) {
+ Prefix pA = getLeaf(uA).getPrefix(uA);
+ Prefix pB = getLeaf(uB).getPrefix(uB);
+ for (SymbolicSuffix v : getLeaf(uB).getSuffixes()) {
+ SDT sdtA = pA.getSDT(v).toRegisterSDT(uA, consts);
+ SDT sdtB = pB.getSDT(v).toRegisterSDT(uB, consts);
+ if (!SDT.equivalentUnderId(sdtA, sdtB)) {
+ CTLeaf uLeaf = getLeaf(uA.prefix(uA.length() - 1));
+ assert uLeaf != null;
+
+ // find registers that should not be removed through optimization
+ Register[] regs = inequivalentMapping(rpRegBijection(pA.getRpBijection(), pA), rpRegBijection(pB.getRpBijection(), pB));
+ DataValue[] regVals = regsToDvs(regs, uA);
+
+ SymbolicSuffix av = extendSuffix(uA, v, regVals);
+ if (suffixRevealsNewGuard(av, getLeaf(uA.prefix(uA.length() - 1)))) {
+ return Optional.of(av);
+ }
+ }
+ }
+ return Optional.empty();
+ }
+
+ /**
+ * Checks for register consistency. This property states that if there are any symmetries between
+ * memorable parameters of any prefix, then these symmetries must be present also in its
+ * one-symbol extensions. If this property does not hold for some prefix {@code u}, a new inner
+ * node will be added to break the symmetry of {@code u}. This node will contain a symbolic suffix
+ * formed by prepending the symbolic suffix breaking the symmetry in the one-symbol extension
+ * {@code ua(d)} by the symbol {@code a}. Not that only one inconsistency will be resolved in this
+ * manner. Multiple inconsistencies should be resolved with multiple calls to this method.
+ *
+ * @return {@code true} if register consistent
+ */
+ public boolean checkRegisterConsistency() {
+ for (Prefix u : getShortPrefixes()) {
+ if (u.length() < 2) {
+ continue;
+ }
+ for (ParameterizedSymbol action : inputs) {
+ Iterator extensions = getExtensions(u, action)
+ .stream()
+ .map(w -> getLeaf(w).getPrefix(w))
+ .iterator();
+ while (extensions.hasNext()) {
+ Prefix uExtended = extensions.next();
+
+ // loop over all bijections exhibiting symmetry
+ RemappingIterator rit = new RemappingIterator<>(u.getRegisters(), u.getRegisters());
+ for (Bijection gamma : rit) {
+ CTPath uPath = u.getPath();
+ if (uPath.isEquivalent(uPath, gamma, solver)) {
+ // u exhibits symmetry under gamma
+ for (Map.Entry e : uExtended.getPath().getSDTs().entrySet()) {
+ SymbolicSuffix v = e.getKey();
+ SDT uaSDT = e.getValue();
+ if (SDT.equivalentUnderBijection(uaSDT, uaSDT, gamma) == null) {
+ // one-symbol extension uExtended does not exhibit symmetry under gamma
+ DataValue[] regs = gamma.keySet().toArray(new DataValue[gamma.size()]);
+ SymbolicSuffix av = extendSuffix(uExtended, v, regs);
+ refine(getLeaf(u), av);
+ return false;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return true;
+ }
+
+ ////////////////////
+ // HELPER METHODS //
+ ////////////////////
+
+ /**
+ *
+ * @param n1
+ * @param n2
+ * @return lowest common ancestor of {@code n1} and {@code n2}
+ */
+ private CTInnerNode lca(CTNode n1, CTNode n2) {
+ if (n1 == n2) {
+ if (n1.isLeaf()) {
+ return (CTInnerNode) n1.getParent();
+ }
+ return (CTInnerNode) n1;
+ }
+ int h1 = height(n1);
+ int h2 = height(n2);
+
+ return lca(n1, h1, n2, h2);
+ }
+
+ private CTInnerNode lca(CTNode n1, int h1, CTNode n2, int h2) {
+ if (n1 == n2) {
+ assert n1 instanceof CTInnerNode;
+ return (CTInnerNode) n1;
+ }
+ if (h1 < h2) {
+ return lca(n1, h1, n2.getParent(), h2 - 1);
+ }
+ if (h1 > h2) {
+ return lca(n1.getParent(), h1 - 1, n2, h2);
+ }
+ return lca(n1.getParent(), h1 - 1, n2.getParent(), h2 - 1);
+ }
+
+ private int height(CTNode n) {
+ int h = 0;
+ while (n.getParent() != null) {
+ h++;
+ n = n.getParent();
+ }
+ return h;
+ }
+
+ /**
+ *
+ * @param ua_mem
+ * @param u_mem
+ * @param a_mem
+ * @return {@code true} if {@code ua_mem} contains all of {@code u_mem} and {@code a_mem}
+ */
+ private boolean consistentMemorable(Set ua_mem, Set u_mem, Set a_mem) {
+ Set union = new LinkedHashSet<>();
+ union.addAll(u_mem);
+ union.addAll(a_mem);
+ return union.containsAll(ua_mem);
+ }
+
+ /**
+ * @param ua
+ * @return the set of data values in the last symbol instance of {@code ua}
+ */
+ private Set actionRegisters(Word ua) {
+ int ua_arity = DataWords.paramLength(DataWords.actsOf(ua));
+ int u_arity = ua_arity - ua.lastSymbol().getBaseSymbol().getArity();
+ DataValue[] vals = DataWords.valsOf(ua);
+
+ Set regs = new LinkedHashSet<>();
+ for (int i = u_arity; i < ua_arity; i++) {
+ regs.add(vals[i]);
+ }
+ return regs;
+ }
+
+ /**
+ *
+ * @param s_mem
+ * @param u_mem
+ * @param a_mem
+ * @return an array containing the data values of {@code s_mem} not contained in either {@code u_mem} or {@code a_mem}
+ */
+ private DataValue[] missingRegisters(Set s_mem, Set u_mem, Set a_mem) {
+ Set union = new LinkedHashSet<>(u_mem);
+ union.addAll(a_mem);
+ Set difference = new LinkedHashSet<>(s_mem);
+ difference.removeAll(union);
+ return difference.toArray(new DataValue[difference.size()]);
+ }
+
+ /**
+ * Form a new symbolic suffix by prepending {@code v} by the last symbol of {@code ua},
+ * using suffix optimization.
+ *
+ * @param ua
+ * @param v
+ * @param missingRegs the register which should not be removed through suffix optimizations
+ * @return the last symbol of {@code ua} concatenated with {@code v}
+ */
+ private SymbolicSuffix extendSuffix(Word ua, SymbolicSuffix v, DataValue[] missingRegs) {
+ if (suffixBuilder == null) {
+ PSymbolInstance a = ua.lastSymbol();
+ Word u = ua.prefix(ua.length() - 1);
+ SymbolicSuffix alpha = new SymbolicSuffix(u, Word.fromSymbols(a), restrBuilder);
+ return alpha.concat(v);
+ }
+
+ SDT u_sdt = prefixes.get(ua).getPrefix(ua).getSDT(v);
+ assert u_sdt != null : "SDT for symbolic suffix " + v + " does not exist for prefix " + ua;
+
+ return suffixBuilder.extendSuffix(ua, u_sdt, v, missingRegs);
+ }
+
+ /**
+ * Perform a tree query for the representative prefix of {@code leaf} with {@code av} to check
+ * whether {@code av} reveals additional guards.
+ *
+ * @param av
+ * @param leaf
+ * @return {@code true} if {@code av} reveals additional guards
+ */
+ private boolean suffixRevealsNewGuard(SymbolicSuffix av, CTLeaf leaf) {
+ assert !leaf.getShortPrefixes().isEmpty() : "No short prefix in leaf " + leaf;
+ Word u = leaf.getShortPrefixes().iterator().next();
+ SDT sdt = oracle.treeQuery(u, av);
+ ParameterizedSymbol a = av.getActions().firstSymbol();
+ Branching branching = leaf.getBranching(a);
+ Branching newBranching = oracle.updateBranching(u, a, branching, sdt);
+ for (Expression guard : newBranching.getBranches().values()) {
+ if (!branching.getBranches().values().contains(guard)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Convert {@code Bijection} to {@code Bijection} using the
+ * data values of {@code prefix} to determine register ids.
+ *
+ * @param bijection
+ * @param prefx
+ * @return
+ */
+ private Bijection rpRegBijection(Bijection