package org.geogebra.common.util; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.TreeSet; import org.geogebra.common.kernel.Construction; import org.geogebra.common.kernel.StringTemplate; import org.geogebra.common.kernel.algos.AlgoDependentBoolean; import org.geogebra.common.kernel.algos.AlgoElement; import org.geogebra.common.kernel.algos.AlgoJoinPoints; import org.geogebra.common.kernel.algos.AlgoJoinPointsSegment; import org.geogebra.common.kernel.geos.GProperty; import org.geogebra.common.kernel.geos.GeoElement; import org.geogebra.common.kernel.geos.GeoLine; import org.geogebra.common.kernel.geos.GeoNumeric; import org.geogebra.common.kernel.geos.GeoPoint; import org.geogebra.common.kernel.geos.GeoSegment; import org.geogebra.common.kernel.prover.AbstractProverReciosMethod; import org.geogebra.common.kernel.prover.ProverBotanasMethod; import org.geogebra.common.kernel.prover.ProverPureSymbolicMethod; import org.geogebra.common.main.Localization; import org.geogebra.common.plugin.EuclidianStyleConstants; import org.geogebra.common.util.debug.Log; /** * Prover package for GeoGebra. Allows using multiple backends for theorem * proving. */ public abstract class Prover { /** * Enum list of supported prover backends for GeoGebra * * @author Zoltan Kovacs <zoltan@geogebra.org> * */ public enum ProverEngine { /** * Tomas Recio's method */ RECIOS_PROVER, /** * Francisco Botana's method */ BOTANAS_PROVER, /** * OpenGeoProver (http://code.google.com/p/open-geo-prover/), Wu's * method */ OPENGEOPROVER_WU, /** * OpenGeoProver, Area method */ OPENGEOPROVER_AREA, /** * pure symbolic prover (every object is calculated symbolically, also * the statements) */ PURE_SYMBOLIC_PROVER, /** * default prover (GeoGebra decides internally) */ AUTO, /** * * not a theorem prover, but an implicit locus calculator */ LOCUS_IMPLICIT, /** * not a theorem prover, but an explicit locus calculator */ LOCUS_EXPLICIT } /** * Possible results of an attempted proof * * @author Zoltan Kovacs <zoltan@geogebra.org> * */ public enum ProofResult { /** * The proof is completed, the statement is generally true (with some * NDG conditions) */ TRUE, /** * The statement is generally true (with some NDG conditions) but no * readable NDGs were found */ TRUE_NDG_UNREADABLE, /** * The proof is completed. The statement is neither generally true, nor * generally false. That is, it is true on a component, but not true on * all components in the algebraic geometry context. */ TRUE_COMPONENT, /** * The proof is completed, the statement is generally false */ FALSE, /** * The statement cannot be proved by using the current backend within * the given timeout */ UNKNOWN, /** * "?", usually from giac.js --- processing in progress */ PROCESSING } /** * Maximal time to be spent in the prover subsystem */ /* input */ private int timeout = 5; private ProverEngine engine = ProverEngine.AUTO; private Construction construction; /** * The statement to be prove */ protected GeoElement statement; /** * Recio's prover. */ protected AbstractProverReciosMethod reciosProver; /** * Gives the current statement to prove * * @return the statement (usually a GeoBoolean) */ public GeoElement getStatement() { return statement; } /* output */ private HashSet<NDGCondition> ndgConditions = new HashSet<NDGCondition>(); /** * The result of the proof */ protected ProofResult result; /** * Should the prover return extra NDG conditions? If not, some computation * time may be saved. */ private boolean returnExtraNDGs; /** * @author Zoltan Kovacs <zoltan@geogebra.org> An object which contains a * condition description (e.g. "AreCollinear") and an ordered list * of GeoElement's (e.g. A, B, C) */ public static class NDGCondition { /** * The condition String */ String condition; /** * How human readable is this condition? The lower the better. This * number is always >= 0; */ double readability = 1.0; /** * Gets readability score for this NDG condition. * * @return score */ public double getReadability() { return readability; } /** * Sets readability score for this NDG condition. * * @param readability * score */ public void setReadability(double readability) { this.readability = readability; } /** * Array of GeoElements (parameters of the condition) */ GeoElement[] geos; /** * A short textual description of the condition * * @return the condition */ public String getCondition() { return condition; } /** * Sets a condition text * * @param condition * the text, e.g. "AreCollinear" */ public void setCondition(String condition) { this.condition = condition; } /** * Returns the GeoElements for a given condition * * @return the array of GeoElements */ public GeoElement[] getGeos() { return geos; } /** * Sets the GeoElements for a given condition * * @param object * the array of GeoElements */ public void setGeos(GeoElement[] object) { this.geos = object; } @Override public int hashCode() { int result = condition.hashCode(); if (geos != null) { for (GeoElement geo : geos) { if (geo != null) { result += geo.hashCode(); } } } return result; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (obj == this) { return true; } if (obj.getClass() != getClass()) { return false; } return this.hashCode() == obj.hashCode(); } private static GeoLine line(GeoPoint P1, GeoPoint P2, Construction cons) { TreeSet<GeoElement> ges = cons.getGeoSetConstructionOrder(); Iterator<GeoElement> it = ges.iterator(); // TODO: Maybe there is a better way here to lookup the appropriate // line // if it already exists (by using kernel). while (it.hasNext()) { GeoElement ge = it.next(); if (ge instanceof GeoLine) { GeoPoint Q1 = ((GeoLine) ge).getStartPoint(); GeoPoint Q2 = ((GeoLine) ge).getEndPoint(); if ((Q1 != null && Q2 != null) && ((Q1.equals(P1) && Q2.equals(P2)) || (Q1.equals(P2) && Q2.equals(P1)))) { return (GeoLine) ge; } } } // If there is no such line, we simply create one. boolean oldMacroMode = cons.isSuppressLabelsActive(); cons.setSuppressLabelCreation(false); AlgoJoinPoints ajp = new AlgoJoinPoints(cons, null, P1, P2); GeoLine line = ajp.getLine(); line.setEuclidianVisible(true); line.setLineType(EuclidianStyleConstants.LINE_TYPE_DASHED_LONG); line.setLabelVisible(true); line.updateVisualStyle(GProperty.COMBINED);// visibility and style cons.setSuppressLabelCreation(oldMacroMode); return line; } /* TODO: Unify this code with line(). */ private static GeoSegment segment(GeoPoint P1, GeoPoint P2, Construction cons) { TreeSet<GeoElement> ges = cons.getGeoSetConstructionOrder(); Iterator<GeoElement> it = ges.iterator(); // TODO: Maybe there is a better way here to lookup the appropriate // line // if it already exists (by using kernel). while (it.hasNext()) { GeoElement ge = it.next(); if (ge instanceof GeoSegment) { GeoPoint Q1 = ((GeoSegment) ge).getStartPoint(); GeoPoint Q2 = ((GeoSegment) ge).getEndPoint(); if ((Q1 != null && Q2 != null) && ((Q1.equals(P1) && Q2.equals(P2)) || (Q1.equals(P2) && Q2.equals(P1)))) { return (GeoSegment) ge; } } } // If there is no such line, we simply create one. boolean oldMacroMode = cons.isSuppressLabelsActive(); cons.setSuppressLabelCreation(false); AlgoJoinPointsSegment ajp = new AlgoJoinPointsSegment(cons, null, P1, P2); GeoSegment segment = ajp.getSegment(); segment.setEuclidianVisible(true); segment.setLineType(EuclidianStyleConstants.LINE_TYPE_DASHED_LONG); segment.setLabelVisible(true); segment.updateVisualStyle(GProperty.COMBINED); cons.setSuppressLabelCreation(oldMacroMode); return segment; } private void sortGeos() { // We need this because geos are sorted in the order of creation. Arrays.sort(geos, new Comparator<GeoElement>() { @Override public int compare(GeoElement g1, GeoElement g2) { return g1.getLabelSimple().compareTo(g2.getLabelSimple()); } }); } /** * Rewrites the NDG to a simpler form. * * @param cons * the current construction */ public void rewrite(Construction cons) { String cond = this.getCondition(); if ("AreCollinear".equals(cond)) { sortGeos(); } else if ("ArePerpendicular".equals(cond) && this.geos.length == 3) { // ArePerpendicular[Line[P1,P3],Line[P3,P2]]. GeoPoint P1 = (GeoPoint) this.geos[0]; GeoPoint P2 = (GeoPoint) this.geos[1]; GeoPoint P3 = (GeoPoint) this.geos[2]; GeoLine l1 = line(P1, P3, cons); GeoLine l2 = line(P3, P2, cons); if (l1 != null && l2 != null) { geos = new GeoElement[2]; geos[0] = l1; geos[1] = l2; sortGeos(); } } else if (("AreEqual".equals(cond) || "ArePerpendicular".equals(cond) || "AreParallel".equals(cond))) { if (this.geos.length == 4) { // This is an AreEqual[P1,P2,P3,P4]-like condition. // We should try to rewrite it to // AreEqual[Line[P1,P2],Line[P3,P4]]. GeoPoint P1 = (GeoPoint) this.geos[0]; GeoPoint P2 = (GeoPoint) this.geos[1]; GeoLine l1 = line(P1, P2, cons); GeoPoint P3 = (GeoPoint) this.geos[2]; GeoPoint P4 = (GeoPoint) this.geos[3]; GeoLine l2 = line(P3, P4, cons); if (l1 != null && l2 != null) { geos = new GeoElement[2]; geos[0] = l1; geos[1] = l2; sortGeos(); } } else if (this.geos.length == 2) { // This is an AreEqual[l1,l2]-like condition. // We should sort l1 and l2. sortGeos(); // Unsure if this is called at all. } } else if ("IsIsoscelesTriangle".equals(cond)) { GeoPoint P1 = (GeoPoint) this.geos[0]; GeoPoint P2 = (GeoPoint) this.geos[1]; GeoPoint P3 = (GeoPoint) this.geos[2]; GeoSegment l1 = segment(P1, P2, cons); GeoSegment l2 = segment(P2, P3, cons); if (l1 != null && l2 != null) { geos = new GeoElement[2]; geos[0] = l1; geos[1] = l2; sortGeos(); this.setCondition("AreCongruent"); } } } } /** * Constructor for the package. */ public Prover() { proveAutoOrder = new ArrayList<ProverEngine>(); // Order of Prove[] for the AUTO prover: // Recio is the fastest. proveAutoOrder.add(ProverEngine.RECIOS_PROVER); // Botana's prover is also fast for general problems. proveAutoOrder.add(ProverEngine.BOTANAS_PROVER); // Wu may be a bit slower. proveAutoOrder.add(ProverEngine.OPENGEOPROVER_WU); // Area method is not polished yet, thus it's disabled: // proveAutoOrder.add(ProverEngine.OPENGEOPROVER_AREA); // Order of ProveDetails[] for the AUTO prover: proveDetailsAutoOrder = new ArrayList<ProverEngine>(); // Botana's prover based on elimination (with no presumed NDGs) gives // the shortest conditions, best for educational use. proveDetailsAutoOrder.add(ProverEngine.BOTANAS_PROVER); // Wu's method does the most general good job. proveDetailsAutoOrder.add(ProverEngine.OPENGEOPROVER_WU); // Recio does not give NDGs: // proveDetailsAutoOrder.add(ProverEngine.RECIOS_PROVER); // Area method is buggy at the moment, needs Damien's fixes. // It returns {true} always at the moment, not useful. // proveDetailsAutoOrder.add(ProverEngine.OPENGEOPROVER_AREA); } /** * Sets the maximal time spent in the Prover for the given proof. * * @param timeout * The timeout in seconds */ public void setTimeout(int timeout) { this.timeout = timeout; } /** * Sets the maximal time spent in the Prover for the given proof. * * @return The timeout in seconds */ public int getTimeout() { return timeout; } /** * Sets the prover engine. * * @param engine * The engine subsystem */ public void setProverEngine(ProverEngine engine) { this.engine = engine; } /** * Gets the prover engine. * * @return the engine subsystem */ public ProverEngine getProverEngine() { return this.engine; } /** * Sets the GeoGebra construction as the set of the used objects in the * proof. * * @param construction * The GeoGebra construction */ public void setConstruction(Construction construction) { this.construction = construction; } /** * Sets the statement to be proven. * * @param root * The statement to be proven */ public void setStatement(GeoElement root) { this.statement = root; } /** * Adds a non-degeneracy condition to the prover object * * @param ndgc * the condition itself */ public void addNDGcondition(NDGCondition ndgc) { ndgConditions.add(ndgc); } private List<ProverEngine> proveAutoOrder; private List<ProverEngine> proveDetailsAutoOrder; /** * The real computation of decision of a statement. The statement is * forwarded to an engine (or more engines). */ public void decideStatement() { // Step 1: Checking if the statement is null. if (statement == null) { Log.error("No statement to prove"); result = ProofResult.UNKNOWN; return; } // Step 2: // Maybe an already computed value is asked to be proven, e.g. // Prove[1==1], i.e. Prove[true] AlgoElement algoParent = statement.getParentAlgorithm(); if (algoParent == null) { if (statement.getValueForInputBar().equals("true")) { result = ProofResult.TRUE; // Trust in kernel's wisdom } else if (statement.getValueForInputBar().equals("false")) { result = ProofResult.FALSE; // Trust in kernel's wisdom } else { result = ProofResult.UNKNOWN; // Not sure if this is executed at } // all, but for sure. return; } StatementFeatures.init(statement); // Step 3: Non-AUTO provers if (engine != ProverEngine.AUTO) { callEngine(engine); return; } // Step 4: AUTO prover Log.debug("Using " + engine); Iterator<ProverEngine> it; if (isReturnExtraNDGs()) { it = proveDetailsAutoOrder.iterator(); } else { it = proveAutoOrder.iterator(); } result = ProofResult.UNKNOWN; while ((result == ProofResult.UNKNOWN || result == ProofResult.TRUE_NDG_UNREADABLE) && it.hasNext()) { ProverEngine pe = it.next(); if (pe == ProverEngine.OPENGEOPROVER_WU || pe == ProverEngine.OPENGEOPROVER_AREA) { /* * Checking if OGP is capable of working on this statement * properly or not. */ AlgoElement ae = statement.getParentAlgorithm(); if (ae instanceof AlgoDependentBoolean) { /* see triangle-midsegment6 */ Log.debug( "OGP cannot safely check expressions, OGP will be ignored"); continue; /* try the next prover */ } } callEngine(pe); } } /** * A helper method to override the last found proof result with the new one, * if the new one is not unknown, or if the result is null yet, then we * prefer the unknown result. * * @param pr * the new result * @return decision which result is better */ private ProofResult override(ProofResult pr) { if (result == null || pr != ProofResult.UNKNOWN) { return pr; } return result; } private void callEngine(ProverEngine currentEngine) { Log.debug("Using " + currentEngine); ndgConditions = new HashSet<NDGCondition>(); // reset if (currentEngine == ProverEngine.BOTANAS_PROVER) { ProverBotanasMethod pbm = new ProverBotanasMethod(); result = override(pbm.prove(this)); return; } else if (currentEngine == ProverEngine.RECIOS_PROVER) { result = override(getReciosProver().prove(this)); return; } else if (currentEngine == ProverEngine.PURE_SYMBOLIC_PROVER) { result = override(ProverPureSymbolicMethod.prove(this)); return; } else if (currentEngine == ProverEngine.OPENGEOPROVER_WU || currentEngine == ProverEngine.OPENGEOPROVER_AREA) { result = override(openGeoProver(currentEngine)); return; } } /** * Gets non-degeneracy conditions of the current proof. * * @return The XML output string of the NDG condition */ public HashSet<NDGCondition> getNDGConditions() { return ndgConditions; } /** * Gets the proof result * * @return The result (TRUE, FALSE or UNKNOWN) */ public ProofResult getProofResult() { return result; } /** * If the result of the proof can be expressed by a boolean value, then it * returns that value. * * @return The result of the proof (true, false or null) */ public ExtendedBoolean getYesNoAnswer() { if (result != null) { if (result == Prover.ProofResult.TRUE || result == Prover.ProofResult.TRUE_NDG_UNREADABLE || result == Prover.ProofResult.TRUE_COMPONENT) { return ExtendedBoolean.TRUE; } if (result == Prover.ProofResult.FALSE) { return ExtendedBoolean.FALSE; } } return ExtendedBoolean.UNKNOWN; } /** * A minimal version of the construction XML. Only elements/commands are * preserved, the rest is deleted. * * @param cons * The construction * @param statement * The statement to prove * @return The simplified XML */ // TODO: Cut even more unneeded parts to reduce unneeded traffic between OGP // and GeoGebra. protected static String simplifiedXML(Construction cons, GeoElement statement) { StringBuilder sb = new StringBuilder(); cons.getConstructionElementsXML_OGP(sb, statement); // /* FIXME: EXTREMELY DIRTY HACK. This should be handled in OGP instead // here. // * In GeoGebra3D some objects get a 3D parameter, e.g. Circle. OGP is // not // * yet prepared for handling this, so we simply remove the // a2="xOyPlane" texts // * from the XML. Hopefully this works for most cases... // */ // return "<construction>\n" + sb.toString().replace(" a2=\"xOyPlane\"", // "") + "</construction>"; return "<construction>\n" + sb.toString() + "</construction>"; } /** * Does the real computation for the proof */ public void compute() { // Will be overridden by web and desktop } /** * Calls OpenGeoProver * * @param pe * Prover Engine * @return the proof result */ protected abstract ProofResult openGeoProver(ProverEngine pe); /** * Will the prover return extra NDGs? * * @return yes or no */ public boolean isReturnExtraNDGs() { return returnExtraNDGs; } /** * The prover may return extra NDGs * * @param returnExtraNDGs * setting for the prover */ public void setReturnExtraNDGs(boolean returnExtraNDGs) { this.returnExtraNDGs = returnExtraNDGs; } /** * Formulate figure in readable format: create a mathematically readable * statement. TODO: create translation keys. * * @param statement * the input statement * @return a localized statement in readable format */ public static String getTextFormat(GeoElement statement) { Localization loc = statement.getKernel().getLocalization(); ArrayList<String> freePoints = new ArrayList<String>(); Iterator<GeoElement> it = statement.getAllPredecessors().iterator(); StringBuilder hypotheses = new StringBuilder(); while (it.hasNext()) { GeoElement geo = it.next(); if (geo.isGeoPoint() && geo.getParentAlgorithm() == null) { freePoints.add(geo.getLabelSimple()); } else if (!(geo instanceof GeoNumeric)) { String definition = geo.getDefinitionDescription( StringTemplate.noLocalDefault); String textLocalized = loc.getPlain("LetABeB", geo.getLabelSimple(), definition); hypotheses.append(textLocalized).append(".\n"); } } StringBuilder theoremText = new StringBuilder(); StringBuilder freePointsText = new StringBuilder(); for (String str : freePoints) { freePointsText.append(str); freePointsText.append(","); } int l = freePointsText.length(); if (l > 0) { freePointsText.deleteCharAt(l - 1); theoremText.append(loc.getPlain("LetABeArbitraryPoints", freePointsText.toString())).append(".\n"); } theoremText.append(hypotheses); String toProveStr = String.valueOf(statement.getParentAlgorithm()); theoremText.append(loc.getPlain("ProveThat", toProveStr)).append("."); return theoremText.toString(); } /** * @return Recio's prover */ AbstractProverReciosMethod getReciosProver() { if (reciosProver == null) { reciosProver = getNewReciosProver(); } return reciosProver; } /** * @return new Recio's prover */ protected abstract AbstractProverReciosMethod getNewReciosProver(); /** * @return The full GeoGebra construction, containing all geos and algos. */ public Construction getConstruction() { return construction; } }