package context.arch.intelligibility; import java.util.ArrayList; import java.util.Date; import java.util.List; import context.arch.discoverer.ComponentDescription; import context.arch.discoverer.query.AbstractQueryItem; import context.arch.discoverer.query.HmmQueryItem; import context.arch.enactor.Enactor; import context.arch.enactor.EnactorReference; import context.arch.enactor.HmmEnactorReference; import context.arch.intelligibility.expression.DNF; import context.arch.intelligibility.expression.Parameter; import context.arch.intelligibility.expression.Reason; import context.arch.intelligibility.query.AltQuery; import context.arch.intelligibility.query.Query; import context.arch.intelligibility.query.WhatIfQuery; import context.arch.intelligibility.reducers.Reducer; import context.arch.storage.Attributes; import context.arch.widget.Widget; /** * The Explainer class generates explanations of various question types from a Query. * This provides default implementation for model-independent explanations (e.g. What, When, Inputs, Outputs), * and descriptive explanations (through DescriptiveExplanationDelegate). * This needs to be subclassed to implement model-dependent explanations (e.g. Why, Why Not, How To, Certainty). * @author Brian Y. Lim * @see Query * @see DescriptiveExplainerDelegate */ public abstract class Explainer { /** * The enactor that this explainer is explaining. */ protected Enactor enactor; /** * Set to static so that it is a global variable. * Defaults as DescriptiveExplainerDelegate, but can be replaced. */ protected DescriptiveExplainerDelegate descExplainer; /** * * @param enactor that this explainer is explaining */ public Explainer(Enactor enactor) { this.enactor = enactor; } public DescriptiveExplainerDelegate getDescriptionExplainer() { // lazy loading if (descExplainer == null) { descExplainer = new DescriptiveExplainerDelegate(); } return descExplainer; } public void setDescriptionExplainer(DescriptiveExplainerDelegate descExplainer) { this.descExplainer = descExplainer; } /** * Convenience method to apply a reduction to the explanation while getting it. * @param query * @param widgetState * @param reducer * @return */ public Explanation getExplanation(Query query, Reducer reducer) { Explanation explanation = getExplanation(query); explanation = reducer.apply(explanation); return explanation; } /** * This is the main method to be used to get Explanations from explainers * by supplying a Query. * It should be extended by subclasses to support more types of question. * @param query containing the question about a context regarding a certain time * @return the generated Explanation corresponding to the Query */ public Explanation getExplanation(Query query) { if (query == null) { return new Explanation(query, DNF.UNKNOWN); } String question = query.getQuestion(); String context = query.getContext(); // System.out.println("Explainer.getExplanation question = " + question); /* * Model-independent explanations */ if (question == null) { return new Explanation(query, DNF.UNKNOWN); } if (question.equals(Query.QUESTION_WHAT)) { // output value return new Explanation(query, new DNF(getWhatExplanation(context))); } else if (question.equals(Query.QUESTION_WHEN)) { return new Explanation(query, new DNF(getWhenExplanation())); } else if (question.equals(WhatIfQuery.QUESTION_WHAT_IF)) { return new Explanation(query, new DNF(getWhatIfExplanation(((WhatIfQuery)query).getInputs()))); } else if (question.equals(Query.QUESTION_INPUTS)) { // for convenience, return names and values of inputs return new Explanation(query, new DNF(getInputsExplanation())); } else if (question.equals(Query.QUESTION_OUTPUTS)) { return new Explanation(query, getOutputsExplanation()); } else if (question.equals(Query.QUESTION_CERTAINTY)) { return new Explanation(query, getCertaintyExplanation()); } /* * Descriptive explanations */ else if (question.equals(Query.QUESTION_DEFINITION)) { return new Explanation(query, new DNF(getDefinitionExplanation(context)) ); } else if (question.equals(Query.QUESTION_RATIONALE)) { return new Explanation(query, new DNF(descExplainer.getRationaleExplanation(context))); } else if (question.equals(Query.QUESTION_PRETTY_NAME)) { return new Explanation(query, new DNF(descExplainer.getPrettyNameExplanation(context))); } else if (question.equals(Query.QUESTION_UNIT)) { return new Explanation(query, new DNF(descExplainer.getUnitExplanation(context))); } /* * Model-dependent explanations */ if (question.equals(Query.QUESTION_WHY)) { return new Explanation(query, getWhyExplanation()); } else if (question.equals(AltQuery.QUESTION_WHY_NOT)) { String altOutcomeValue = ((AltQuery)query).getAltOutcomeValue(); return new Explanation(query, getWhyNotExplanation(altOutcomeValue)); } else if (question.equals(AltQuery.QUESTION_HOW_TO)) { String altOutcomeValue = ((AltQuery)query).getAltOutcomeValue(); return new Explanation(query, getHowToExplanation(altOutcomeValue)); } // else if (question.equals(Query.QUESTION_CONTROL)) { // // TODO maybe this should be implemented at the subclass level, which is application domain dependent? // // or can just use the EnactorParameters framework // } /* * Explanation unknown */ return new Explanation(query, DNF.UNKNOWN); } /* ---------------------------------------------------------------------------------------- * Model-Independent explanations * ---------------------------------------------------------------------------------------- */ /** * Get the current value of an attribute. This is normally the outcome value (i.e. of * the output attribute), but may also refer to an input attribute, depending on the name * of the context. * @param <T> the type of the value of the context * @param context to retrieve the value of, normally the outcome name (i.e. {@link Enactor#getOutcomeName()}, * but may also be an input attribute name. * @return Parameter<T> where name is context, and value is of the context */ @SuppressWarnings("unchecked") public <T extends Comparable<? super T>> Parameter<T> getWhatExplanation(String context) { T value; // System.out.println("context = " + context); // System.out.println("enactor.containsOutAttribute(context) = " + enactor.containsOutAttribute(context)); // if (enactor.containsOutAttribute(context)) { // asking What about output if (enactor.getOutcomeName().equals(context)) { value = (T) enactor.getOutcomeValue(); } else { // asking What about input value = enactor.getInWidgetState().getAttributeValue(context); } Parameter<T> p = Parameter.instance(context, value); return p; } /** * Get the timestamp of when the outcome value of the enactor was changed. * @return Parameter<Date> where name is {@link Widget.TIMESTAMP}, and value is a java.util.Date */ protected Parameter<Date> getWhenExplanation() { long timestamp = enactor.getInWidgetState().getAttributeValue(Widget.TIMESTAMP); Date date = new Date(timestamp); Parameter<Date> exp = Parameter.instance(Widget.TIMESTAMP, date); return exp; } /** * Get the outcome value if an alternative set of input values were provided instead of the * current input values. * @param <T> the type of the outcome value * @param altInputs conjunction of alternative input values to ask about * @return Parameter<T> where name is the outcome name, and value is the resulting outcome value. */ @SuppressWarnings("unchecked") public <T extends Comparable<? super T>> Parameter<T> getWhatIfExplanation(Reason altInputs) { // check which enactorRef has its query satisfied by the widgetState String outcomeName = enactor.getOutcomeName(); String outcomeValue = null; // TODO replace by directly crunching through enactor? But need to be non-mutative // create modified widget stub from altInputs ComponentDescription altWidgetState = enactor.getInWidgetState().clone(); altWidgetState.addNonConstantAttributes(altInputs.toAttributes()); // to replace some attributes for (EnactorReference enactorRef : enactor.getReferences()) { AbstractQueryItem<?,?> query = enactorRef.getConditionQuery(); Boolean queryResult = query.match(altWidgetState); // System.out.println("getWhatIfExplanation queryResult = " + queryResult); // System.out.println("getWhatIfExplanation enactorRef.getOutcomeValue() = " + enactorRef.getOutcomeValue()); // System.out.println("getWhatIfExplanation query = " + query); if (queryResult != null && queryResult) { // TODO: this is a non-scalable way of handling the special case for sequences if (enactorRef instanceof HmmEnactorReference) { outcomeValue = ((HmmQueryItem)query).getLastOutcomeValueSequence().toString(); } else { // Rules or Classifier outcomeValue = enactorRef.getOutcomeValue(); // this only works for Rule ERs, not classifier, which need to set value } break; } } if (outcomeValue == null) { return null; } // no rule satisfied by state, so no valid reaction // return output of selected enactorRef Parameter<T> p = Parameter.instance(outcomeName, (T) outcomeValue); return p; } /** * Get a conjunction of input values. * @return conjunction of {@link Parameter}s */ public Reason getInputsExplanation() { ComponentDescription widgetState = enactor.getInWidgetState(); if (widgetState == null) { return new Reason(); } // empty /* * TODO: reconsider risk of this is that attributes not used in rules also gets returned * Alternative of reading off from rules is that it is not generalizable to the whole application * i.e. different rules may consider a subset of all inputs */ // not matching constant attributes, as they should only be used for subscription Attributes attributes = widgetState.getNonConstantAttributes(); // System.out.println("getInputsExplanation.attributes = " + attributes); return Reason.fromAttributes(attributes); } /** * Get a {@link DNF} of all the possible output values. * @return Disjunction of {@link Reasons}, where each Reason is of size one containing a {@link Parameter} * for an outcome value, and all their names are the outcome name. */ public DNF getOutputsExplanation() { DNF outputs = new DNF(); String outcomeName = enactor.getOutcomeName(); for (String outcomeValue : enactor.getOutcomeValues()) { outputs.add(new Reason(Parameter.instance(outcomeName, outcomeValue))); } return outputs; } /** * Returns the descriptive definition of the context with name = attributeName. * By default, this uses a DescriptionExplainerDelegate, but may be overridden. * @param attributeName of the context to get its definition * @return Parameter<String> where name is attributeName and value is the definition. */ public Parameter<String> getDefinitionExplanation(String attributeName) { return descExplainer.getDefinitionExplanation(attributeName); } /* ---------------------------------------------------------------------------------------- * Model-dependent explanations * ---------------------------------------------------------------------------------------- */ /** * Get why the enactor's model decided on its outcome value. This may be a reason trace, a probabilistic explanation, etc. * @return DNF with one or more explanations. */ public abstract DNF getWhyExplanation(); /** * Get why the enactor's model did <b>not</b> decide on an alternative outcome value. This may be a reason trace, a probabilistic explanation, etc. * @param altOutcomeValue the alternative outcome value to ask about * @return DNF with one or more explanations. */ public abstract DNF getWhyNotExplanation(String altOutcomeValue); /** * Get how the enactor's model would decide on a candidate outcome value. This may be a reason trace, a probabilistic explanation, etc. * @param altOutcomeValue the candidate outcome value to ask about * @return DNF with one or more explanations. */ public abstract DNF getHowToExplanation(String altOutcomeValue); /** * Get an explanation about how certain the enactor's model is of its decision. * @return DNF with one or more explanations. The explanation may also be very simple with just one literal wrapped in a DNF. */ public abstract DNF getCertaintyExplanation(); /* ---------------------------------------------------------------------------------------- * Convenience methods * ---------------------------------------------------------------------------------------- */ /** * Convenience method to extract the names of inputs from an inputs explanation. * @param * @return */ public static List<String> inputsToLabels(Reason inputs) { // doesn't matter if Conjunction or Disjunction, both are List<Expression> List<String> list = new ArrayList<String>(); for (Parameter<?> literal : inputs) { String label = literal.getName(); list.add(label); } return list; } /** * Convenience method to unwrap the output explanation in DNF form into a list of string values. * @param outputs output values explanation * @return */ public static List<String> outputsToLabels(DNF outputs) { // doesn't matter if Conjunction or Disjunction, both are List<Expression> List<String> list = new ArrayList<String>(); for (Reason reason : outputs) { String label = reason.get(0).getValue().toString(); list.add(label); } return list; } }