package org.geogebra.common.cas.singularws;
import java.util.Date;
import org.geogebra.common.cas.error.ComputationException;
import org.geogebra.common.factories.UtilFactory;
import org.geogebra.common.main.SingularWSSettings;
import org.geogebra.common.util.HttpRequest;
import org.geogebra.common.util.URLEncoder;
import org.geogebra.common.util.debug.Log;
/**
* Maintains a Singular WebService. For the SingularWS API please see the
* documentation of SingularWS
*
* @see "https://github.com/kovzol/singularws"
*
* @author Zoltan Kovacs <zoltan@geogebra.org>
*/
public class SingularWebService {
private final static int GET_REQUEST_MAX_SIZE = 2000;
private int timeout = SingularWSSettings.getTimeout();
private final static String testConnectionCommand = "t";
private final static String singularDirectCommand = "s";
private String wsHost = SingularWSSettings.getSingularWebServiceRemoteURL();
private Boolean available;
private String locusLib = "";
private boolean fastConn;
private final static String[] SINGULAR_LIB_GROBCOVs = { "grobcov",
"grobcovG",
"grobcovF2m", "grobcovC1", "grobcovC0" };
private final static int CONNECTION_SPEED_NO_TESTS = 3;
private final static int CONNECTION_SPEED_THRESHOLD = 100;
private String swsCommandResult(String command) throws Throwable {
return swsCommandResult(command, "");
}
private String swsCommandResult(String command, String parameters)
throws Throwable {
String url1 = wsHost + "/";
String encodedParameters = "";
String caching = cachingString();
if (parameters != null) {
URLEncoder urle = UtilFactory.getPrototype().newURLEncoder();
encodedParameters = urle.encode(parameters);
}
HttpRequest httpr = UtilFactory.getPrototype().newHttpRequest();
httpr.setTimeout(timeout);
// Varnish currently cannot do caching for POST requests,
// so we prefer GET for the shorter Singular programs:
if (encodedParameters.length() + url1.length() + command.length()
+ 6 <= GET_REQUEST_MAX_SIZE) {
httpr.sendRequest(url1 + "?c=" + command + "&p=" + encodedParameters
+ caching);
} else {
httpr.sendRequestPost(url1,
"c=" + command + "&p=" + encodedParameters + caching,
null);
}
// In fact we will not use Varnish after changing SingularWS to version
// >= 3 (2014-01-03).
String response = httpr.getResponse(); // will not work in web, TODO:
// callback!
if (response == null)
{
return null; // avoiding NPE in web
}
// Trimming:
if (response.endsWith("> ")) {
response = response.substring(0, response.length() - 2);
}
if (response.endsWith("\n")) {
response = response.substring(0, response.length() - 1);
}
if (response.contains("error")) {
// Intuitive detection of error in computation. TODO: be more
// strict.
Log.error("Computation error in SingularWS: " + response);
throw new ComputationException("Computation error in SingularWS");
}
return response;
}
private static String cachingString() {
final String prefix = "&l=";
if (SingularWSSettings.getUseCaching()) {
return prefix + "1";
}
return prefix + "0";
}
/**
* Reports if SingularWS is available. (It must be initialized by enable()
* first.)
*
* @return true if SingularWS is available
*/
public boolean isAvailable() {
if (available == null) {
return false;
}
if (available) {
return true;
}
return false;
}
/**
* Reports if SingularWS has a fast connection available.
*
* @return true if SingularWS connection is fast enough
*/
public boolean isFast() {
return fastConn;
}
private String speed() {
if (isFast()) {
return "fast";
}
return "slow";
}
/**
* Create a connection to the SingularWS server for testing. Also sets up
* variables depending on the installed features of Singular.
*
* @return true if the connection works properly
*/
public boolean testConnection() {
// Log.debug("TEST: " +
// convertFloatsToRationals("((6.56*(x-x1))-(-0.2197*(y-x2)))"));
String result = null;
try {
result = swsCommandResult(testConnectionCommand);
} catch (Throwable e) {
Log.error("Failure while testing SingularWS connection");
}
if (result == null) {
return false;
}
if ("ok".equals(result)) {
// Testing connection speed.
fastConn = true; // be optimistic
for (int i = 0; i < CONNECTION_SPEED_NO_TESTS && fastConn; ++i) {
Date date = new Date();
long startTime = date.getTime();
try {
swsCommandResult(testConnectionCommand);
} catch (Throwable e) {
Log.error("Failure while testing SingularWS connection");
}
date = new Date();
long elapsedTime = date.getTime() - startTime;
Log.debug("Measuring speed to SWS #" + i + ": " + elapsedTime
+ " ms");
if (elapsedTime > CONNECTION_SPEED_THRESHOLD) {
fastConn = false;
}
}
// Testing extra features.
for (String l : SINGULAR_LIB_GROBCOVs) {
if (testLib(l)) {
locusLib = l;
break;
}
}
return true;
}
return false;
}
private boolean testLib(String name) {
String result;
try {
result = directCommand("LIB \"" + name + ".lib\";");
if (result.length() == 0) {
Log.debug("SingularWS supports library " + name);
return true;
}
Log.debug("SingularWS doesn't support library " + name + " ("
+ result + ")");
} catch (Throwable e) {
Log.error("Failure connecting to SingularWS");
}
return false;
}
/**
* Sends a Singular program to the SingularWS server and returns the answer.
*
* @param singularProgram
* The program code to be sent directly to Singular
* @return the answer
* @throws Throwable
* ComputationException when problem occurs
*/
public String directCommand(String singularProgram) throws Throwable {
return swsCommandResult(singularDirectCommand,
singularProgram);
}
/**
* Sets the remote server being used for SingularWS.
*
* @param site
* The remote http URL for the remote server
*/
public void setConnectionSite(String site) {
this.wsHost = site;
}
/**
* Reports what remote server is used for SingularWS.
*
* @return the URL of the remote server
*/
public String getConnectionSite() {
return this.wsHost;
}
/**
* If the test connection is working, then set the webservice "available",
* unless it is disabled by a command line option.
*/
public void enable() {
if (!SingularWSSettings.useSingularWebService()) {
Log.debug("SingularWS connection disabled by command line option");
this.available = false;
return;
}
Log.debug("Trying to enable SingularWS connection");
boolean tc = testConnection();
if (tc) {
this.available = true;
} else {
this.available = false;
}
}
/**
* Set the SingularWS connection handler to off
*/
public void disable() {
this.available = false;
}
/**
* Sets the maximal time spent in SingularWS for a program (not yet
* implemented).
*
* @param timeout
* the timeout in seconds
*/
public void setTimeout(int timeout) {
this.timeout = timeout;
}
/**
* Gets Singular version.
*
* @return version number (int), e.g. 3150
*/
public String getVersion() {
if (isAvailable()) {
try {
return directCommand("system(\"version\");");
} catch (Throwable e) {
Log.error("Failure while getting SingularWS version");
}
}
return null;
}
/**
* Gets full Singular version string.
*
* @return version string
*/
public String getSingularVersionString() {
if (this.available) {
return "SingularWS " + getVersion() + " " + speed() + " at "
+ getConnectionSite();
}
return null;
}
/**
* If non-empty, it contains the name of the auxiliary Singular library
* "grobcovCx" to compute loci in such a form which does not contain the
* degenerate parts of the algebraic curve. See
* http://www-ma2.upc.edu/montes/ for more details. Thanks to Antonio Montes
* and Francisco Botana for providing this extra library.
*
* @return the name of the auxiliary Groebner cover library
*/
public String getLocusLib() {
return locusLib;
}
/**
* Decides if we are using a new or an old version of the grobcov library.
* In old versions the "locusdg" command was "locus2d". In the newest
* version it is "locus".
*
* @return locus command
*/
public String getLocusCommand() {
if ("grobcov".equals(locusLib)) {
return "locus";
}
if (locusLib.endsWith("F2m") || locusLib.endsWith("G")) {
return "locusdg";
}
return "locus2d";
}
/**
* Helper computations from Singular. Note that the returned string will be
* post-processed by GeoGebra and GeoGebraCAS. This is important since we
* assume that the factors computed by Singular in CIFactor.1 will be
* further simplified by Giac.
*
* @param command
* the GeoGebra command pattern
* @return its translation to Singular commands
*/
public String getTranslatedCASCommand(String command) {
if ("CIFactor.1".equals(command)) {
StringBuilder sb = new StringBuilder();
sb.append("LIB \"absfact.lib\";").
// FIXME: This covers the one-letter variables only, but
// does nothing for the others
// (infinitely many).
append("ring R=0, (x,y,z,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w), ds; short=0;")
.append("poly q=%0;").
// Warning: the absfact.lib package prints an unwanted line
// containing "absolute_factors".
// It must be filtered somehow. Currently it is done in
// SingularWS (server side)
// for SingularWS version < 3. TODO: Do it for version >= 3
// too, or hack the LIB.
append("def S=absFactorize(q); setring(S); list af=absolute_factors;")
.append("string Z=\"\";")
.append("int i; int p=size(af[1]);").
// quadpolyroot returns the Ith root of poly P
append("proc quadpolyroot(poly P, int I) {")
.append("string PS=string(P);")
.append("string RS=string(\"poly PP=\",PS[2,size(PS)-2]);").
// @c is the variable name for the rootof-like polynomial.
append("def RR=basering; ring NR=0,(@c),ds; execute(RS);")
.append("matrix L=coeffs(PP,@c); bigint A=bigint(L[3,1]); bigint B=bigint(L[2,1]); bigint C=bigint(L[1,1]);")
.append("string SC; if (I==1) { SC=\"+\"; } if (I==2) { SC=\"-\"; }")
.append("string RV=string(\"((\",(-B),SC,\"sqrt(\",(B*B)-(4*A*C),\"))/(\",(2*A),\"))\");")
.append("setring(RR); return(RV); }").
// polydeg returns the degree of poly P
append("proc polydeg(poly P) { string PS=string(P); string RS=string(\"poly PP=\",PS[2,size(PS)-2]);")
.append("def RR=basering; ring NR=0,(@c),ds; execute(RS); int L=size(coeffs(PP,@c))-1; setring(RR); return(L); }")
.
// replace is a standard search-replace string function
append("proc replace(string HS, string N, string TO) { int found=1; while (found>0) { found=find(HS,N);")
.append("if (found>0) { string BEF=HS[1,found-1]; string AFT; if (found+size(N)<=size(HS)) {")
.append("AFT=HS[found+size(N),size(HS)]; } HS=string(BEF,TO,AFT); } } return(HS); }")
.
// the main computation: we return the product of the
// factors as a string in Z
append("for (i=1; i<=p; i++) { poly s=af[3][i]; if (polydeg(s)>2) { print(\"error\"); }")
.append("string f=string(\"(\",af[1][i],\")\");")
.append("if (polydeg(s)==2) { string f1=replace(f,\"@c\",quadpolyroot(s,1));")
.append("string f2=replace(f,\"@c\",quadpolyroot(s,2)); f=string(\"simplify(\",f1,\")*simplify(\",f2,\")\"); }")
.append("if (af[2][i]!=1) { f=string(f,\"^\",af[2][i]); }")
.append("Z=string(Z,f); if (i<p) { Z=string(Z,\"*\"); } } Z;");
return sb.toString();
}
return null;
}
}