package context.arch.intelligibility;
import java.util.Map;
import context.arch.enactor.Enactor;
import context.arch.intelligibility.expression.DNF;
import context.arch.intelligibility.expression.Expression;
import context.arch.intelligibility.expression.Negated;
import context.arch.intelligibility.expression.Parameter;
import context.arch.intelligibility.expression.Reason;
import context.arch.intelligibility.hmm.HmmExplainer;
import context.arch.intelligibility.rules.RulesExplainer;
import context.arch.intelligibility.weka.bayes.NaiveBayesExplainer;
import context.arch.intelligibility.weka.j48.J48Explainer;
import context.arch.storage.Attributes;
/**
* <p>
* Delegate class for Explainers useful for Explainers that generate explanations that are fixed
* once the model is non-changing. I.e., all explanations can be pre-generated once the model is
* final. If the model is updated (e.g. for dynamic training), then the explanations can be
* "refreshed". Explanations are be pre-generated by parsing the original model, and building a
* Disjunctive Normal Form expression (as a Disjunction). The model-dependent explanations can
* be obtained by reading this data structure.
* </p>
* <p>
* Contrast this with Explainers that can only generate explanations depending on the input states
* (e.g. {@link NaiveBayesExplainer}, {@link HmmExplainer}).
* </p>
* <p>
* This is an abstract class that needs to be subclassed to implement a way to build the DNF reasons
* for each outcome value.
* </p>
*
* @author Brian Y. Lim
* @see RulesExplainer
* @see J48Explainer
*/
public abstract class StaticExplainerDelegate {
/** Map of Disjunctions of traces for each class value */
protected Map<String, DNF> reasonsDNF; // made private so that it is not carelessly used prematurely by subclass
protected Enactor enactor;
protected Explainer explainer;
public StaticExplainerDelegate(Enactor enactor) {
this.enactor = enactor;
// don't set this up yet, as it would lead to a cyclic dependency with constructing the Explainer
//this.explainer = enactor.getExplainer();
}
/**
* Creates the reasons for all outcome values. This would be model-dependent,
* and parsed from the underlying model.
* Note that this may be called many times, so keep it light-weight.
* @param init if true, then it refreshes, and may use heavy-weight parsing to create the reasons;
* if false, then it reuses the previously stored reasons
* @return map where each key is an outcome value, and each value is the DNF Expression for that outcome value.
*/
protected Map<String, DNF> getReasonsDNF() {
if (reasonsDNF == null) {
this.explainer = enactor.getExplainer();
reasonsDNF = initReasonsDNF();
}
return reasonsDNF;
}
/**
* Used to init or refresh the reasonsDNF that store explanations in a DNF structure.
* @return
*/
protected abstract Map<String, DNF> initReasonsDNF();
public DNF getWhyExplanation() {
String classValue = enactor.getOutcomeValue(); // current class value
DNF traces = getReasonsDNF().get(classValue); // get traces applicable to value
DNF whyTraces = new DNF();
// get Conjunction of input values (Parameters), i.e. input explanation
Reason inputs = explainer.getInputsExplanation();
/*
* Iterate through Disjunction (DNF form) for the current class value,
* and select Conjunction trace that is satisfied.
* There should only be one.
*/
for (Reason trace : traces) {
if (trace.isSatisfiedBy(inputs)) {
whyTraces.add(trace);
}
}
return whyTraces;
}
/**
* Scans all traces for those that result in whyNotClassValue.
* @return null when whyNotClassValue = actual classValue. Empty collection when there is no solution.
*/
public DNF getWhyNotExplanations(String altOutcomeValue) {
// get traces applicable to alternative value
DNF traces = getReasonsDNF().get(altOutcomeValue);
DNF whyNotTraces = new DNF();
// get Conjunction of input values (Parameters), i.e. input explanation
Reason inputs = explainer.getInputsExplanation();
/*
* Iterate through Disjunction (DNF form)
* and adding only literal terminals of Conjunctions that are not satisfied,
* remembering to negate them to be true
*/
for (Reason trace : traces) {
Reason whyNotTrace = new Reason();
for (Expression child : trace) {
// test against each input separately
for (Parameter<?> input : inputs) {
// if not satisfied, add negated to whyNotTrace
if (!child.isSatisfiedBy(input)) {
Parameter<?> negated = Negated.negate((Parameter<?>) child);
whyNotTrace.add(negated);
break;
}
}
}
// don't bother adding if empty; but should never have been empty anyway
if (!whyNotTrace.isEmpty()) {
whyNotTraces.add(whyNotTrace);
}
}
return whyNotTraces;
}
public DNF getHowToExplanations(String classValue) {
DNF traces = getReasonsDNF().get(classValue); // get traces applicable to value
return traces;
}
/**
* Constrains How To explanations by pre-conditioning some attribute values.
* @param attributes to pre-set as the If condition; it should not have extra Attributes not in the inputs,
* but can have "missing" attributes that are to be found by this search
* @param classValue
* @return DNF with Conjunctions that include all (pre-set and un-set) attributes; Disjunction would be empty if no solution found
*/
public DNF getHowToIfExplanation(Attributes attributes, String classValue) {
Reason whatIfInputs = Reason.fromAttributes(attributes);
DNF traces = getReasonsDNF().get(classValue); // get traces applicable to value
DNF howToIfTraces = new DNF();
/*
* Iterate through Disjunction (DNF form) for the class value,
* and add Conjunction traces that are satisfied.
*/
for (Reason trace : traces) {
if (trace.isSatisfiedBy(whatIfInputs)) { // return the first (which should also be the only)
howToIfTraces.add(trace);
}
}
return howToIfTraces;
}
}