From 408eac1222e87f8a9dcdfa31b9cac7e06aed0f14 Mon Sep 17 00:00:00 2001 From: Max Horn Date: Sun, 16 Aug 2026 19:46:02 +0200 Subject: [PATCH 1/6] Use polymake's JSON format, require polymake 4 polymaking spoke to polymake through the pre-4 plain file format: it wrote files in it, and lib/pm_script_arg.pl stringified results so that GAP could scrape them back out of standard output. Reconstructing typed values from that text needed a table mapping each of 58 keywords to a hand written parser, one of which had to guess whether a block was a matrix or a list of sets by looking for a brace. polymake 4 has its own JSON format, and the json package can read it, so do that instead. lib/pm.pl evaluates each requested property, serializes it with polymake's own serializer and writes one JSON file, which GAP reads back. Going through a file rather than standard output means results can never be confused with anything polymake prints. Values are now decoded by polymake's type rather than by keyword, so properties polymaking has never heard of decode correctly too. Only the conventions that genuinely depend on the keyword are left: stripping the homogenizing 1 from points, and the two node indices that the type system cannot flag as indices. Sets, incidence matrices and adjacency lists are recognised as index containers by their type, so the 0-based to 1-based shift no longer needs a table either. Consequences: - Files are written as JSON, so polymake never converts them and never warns that it did. This is the rest of issue #23. - Nested properties can be named directly: Polymake(poly, "HASSE_DIAGRAM.FACES") rather than the keyword rewriting in workaround_maps.gi. This restores full access to the Hasse diagram. DIMS is not among them; polymake 4 cannot compute it from a polytope's Hasse diagram and says so. - Polymake(poly, "GRAPH") works again. It used to fail with POSITION_SUBSTRING: must be a string. - Keywords are evaluated independently, so one failing no longer discards the results of the others. - Floating point properties such as MINIMAL_VERTEX_ANGLE return a GAP float rather than a rational approximation of one. polymake before 4.0 cannot read the files polymaking now writes, so it is rejected with a clear error on the first call rather than left to fail obscurely. The version is detected lazily; loading the package still spawns no processes. AppendToPolymakeObject now takes a GAP value rather than a string to append verbatim, since JSON is not append friendly: the file is rewritten from the properties set so far. As a result polymaking refuses to write to a file it did not create, rather than corrupting it. Fixes #22. Co-Authored-By: Claude Opus 5 --- CHANGES.md | 20 ++ PackageInfo.g | 2 +- README.md | 10 +- doc/environment.xml | 23 ++- doc/input.xml | 88 ++++----- doc/internals.xml | 216 +++++---------------- doc/output.xml | 19 +- init.g | 4 +- lib/ObjectConverters.gd | 27 --- lib/ObjectConverters.gi | 85 -------- lib/Objects.gd | 4 +- lib/Objects.gi | 25 +-- lib/construct.gd | 5 +- lib/construct.gi | 349 ++++++++++++++------------------- lib/convert.gd | 53 ----- lib/convert.gi | 420 ---------------------------------------- lib/environment.gd | 3 + lib/environment.gi | 84 ++++++++ lib/json.gd | 12 ++ lib/json.gi | 228 ++++++++++++++++++++++ lib/pm.pl | 67 +++++++ lib/pm_script_arg.pl | 66 ------- lib/userpref.gi | 4 +- lib/workaround_maps.gd | 26 --- lib/workaround_maps.gi | 68 ------- read.g | 4 +- tst/example.tst | 2 +- tst/json.tst | 67 +++++++ tst/polymaking.tst | 30 ++- 29 files changed, 787 insertions(+), 1224 deletions(-) delete mode 100644 lib/ObjectConverters.gd delete mode 100644 lib/ObjectConverters.gi delete mode 100644 lib/convert.gd delete mode 100644 lib/convert.gi create mode 100644 lib/json.gd create mode 100644 lib/json.gi create mode 100644 lib/pm.pl delete mode 100644 lib/pm_script_arg.pl delete mode 100644 lib/workaround_maps.gd delete mode 100644 lib/workaround_maps.gi create mode 100644 tst/json.tst diff --git a/CHANGES.md b/CHANGES.md index 2e7b335..a2332a2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,25 @@ 0.9.0 (unreleased) +- polymake 4.0 or newer is now required, and the GAP package json is a new + dependency. polymaking now writes and reads polymake's own JSON data format + instead of the pre-4 plain format, which means polymake no longer converts + the files and no longer says so (issue #22) +- nested polymake properties can be named directly, e.g. + `Polymake(poly, "HASSE_DIAGRAM.FACES")` +- `Polymake(poly, "GRAPH")` works again; it used to fail with + `POSITION_SUBSTRING: must be a string` +- when several keywords are given, they are now evaluated independently, so one + failing no longer discards the others +- `MINIMAL_VERTEX_ANGLE` and other floating point properties now return a + proper GAP float instead of a rational approximation +- values decoded from polymake are chosen by polymake's own type rather than by + a per-keyword table, so properties polymaking has never heard of are decoded + correctly too. `ObjectConverters` and the `ConvertPolymake...` functions are + gone, as is `ConvertMatrixToPolymakeString` +- `AppendToPolymakeObject(poly, name, value)` now takes a GAP value; it used to + take a string to append to the file verbatim +- polymaking will not write to a file it did not create + - polymaking is now configured via the GAP user preferences `PolymakeCommand` and `PolymakeDataDirectory` (issues #16, #19) - the data directory is determined lazily and re-created when it has vanished, diff --git a/PackageInfo.g b/PackageInfo.g index ee5f0b3..1fc6f42 100644 --- a/PackageInfo.g +++ b/PackageInfo.g @@ -51,7 +51,7 @@ PackageDoc := rec( Dependencies := rec( GAP := ">=4.8", - NeededOtherPackages := [], + NeededOtherPackages := [ [ "json", ">= 2.0.0" ] ], SuggestedOtherPackages := [], NeededSystemPackages := rec( Ubuntu := [["polymake"]] ), ExternalConditions := [["polymake must be installed", "https://www.polymake.org"]] diff --git a/README.md b/README.md index ec65f65..060bb29 100644 --- a/README.md +++ b/README.md @@ -21,12 +21,10 @@ National University of Ireland, Galway Requirements ------------ -polymaking requires GAP version 4.8. It also needs polymake to be -installed. The GAPDoc package is needed to display the documentation. -polymaking was written for the "first generation" polymake which was -called as a command-line tool. Using it with a current ("next generation") -version of polymake will result in longer runtimes and fewer supported -keywords/features. +polymaking requires GAP version 4.8, the GAP package json, and polymake 4.0 +or newer. The GAPDoc package is needed to display the documentation. + +Use polymaking 0.8.9 if you need to work with polymake 3 or older. Installation diff --git a/doc/environment.xml b/doc/environment.xml index 3622650..5e3ee1e 100644 --- a/doc/environment.xml +++ b/doc/environment.xml @@ -9,26 +9,25 @@ But it should be as platform independent as ⪆ and polymake.

The interaction with polymake is restricted to writing files and - carrying out simple operations. These looked like + carrying out simple operations. polymaking writes a data file in polymake's + own JSON format and then asks polymake to evaluate properties of it, using + the script lib/pm.pl:
-polymake file KEYWORD1 KEYWORD2 KEYWORD3 -
- - on the command line for polymake versions before 4. The keywords are polymake methods without arguments. - Since polymake no longer supports this interface the polymaking package - provides the script lib/pm_script_arg.pl to emulate this. - -
- polymake ––script lib/pm_script_arg.pl KEYWORD1 KEYWORD2 KEYWORD3 + polymake ––script lib/pm.pl RESULTFILE OBJECTFILE KEYWORD1 KEYWORD2
+ The keywords are polymake properties or methods without arguments. The script + writes the results as JSON to RESULTFILE, which ⪆ reads back with + the json package; going through a file rather than through + standard output keeps the results clear of anything polymake prints. + Using custom scripts is not supported.
Every call to polymake will re-start the program anew. This causes considerable overhead. The number of calls to polymake is reduced by caching the results in the so-called PolymakeObject in GAP. -As of polymaking version 0.8.0, old versions of polymake (i.e. versions before 2.7.9) are not -supported anymore. +As of polymaking version 0.9.0, polymake 4.0 or newer is required. Use +polymaking 0.8.9 with older versions of polymake.

diff --git a/doc/input.xml b/doc/input.xml index 51b7faa..3b48d54 100644 --- a/doc/input.xml +++ b/doc/input.xml @@ -3,14 +3,14 @@ The interaction with the polymake program is done via files. A PolymakeObject is mainly a pointer to a file and a list of known properties of the object. These properties need not be stored - in the file. Whenever polymake is called, the returned value is - read from standard output and stored in the PolymakeObject - corresponding to the file for which polymake is called. + in the file. Whenever polymake is called, the returned values are + read back and stored in the PolymakeObject corresponding to the + file for which polymake was called. - The files for polymake are written in the old (non-xml) format. - The first run of polymake converts them into the new (xml) format. This - means that changes to the file by means of the methods outlined below - after the first run of polymake will probably lead to corrupted files. + The files are written in polymake's own JSON format, so polymake never has + to convert them. Writing to a PolymakeObject rewrites the whole file + from the properties given so far, which means a file may be extended at any + point, also after polymake has been called for it. @@ -39,11 +39,12 @@ "poly1340.1"). If prefix is given, the filename starts with this prefix. - Optionally, the file can be generated with a header specifying - application, version and type of the object. This is done by passing the - triple of strings appvertyp to - . A valid triple is - ["polytope","2.3","RationalPolytope"]. Validity is checked by + Objects are of polymake type polytope::Polytope<Rational> + unless another type is given. A type may be passed either as a qualified + polymake 4 type name, or as the triple of strings appvertyp the + pre-0.9 interface used, for example + ["polytope","2.3","RationalPolytope"]; in the latter case the + version is ignored and the application and type are joined, see . @@ -70,15 +71,17 @@ This method generates a PolymakeObject corresponding to the file filename in the directory dir. If - dir is not given, the POLYMAKE&uscore;DATA&uscore;DIR is - used.If no file with name filename exists in dir (or - POLYMAKE&uscore;DATA&uscore;DIR, respectively), an empty file is created. + dir is not given, is used. If + no file of that name exists there, a new one is created. - Note that the contents of the file do not matter for the + Note that the contents of an existing file do not matter for the generation of the object. In particular, the object does not know any of the properties that might be encoded in the file. The only way to transfer information from files to PolymakeObjects - is via . + is via . polymaking will not write to a file it + did not create, so the methods of Section + raise an error for such an object; polymake + may read files in any format it understands. @@ -211,15 +214,15 @@ rec( resulting polymake file is still consistent. - + nothing - This appends the string string to the file associated to - the PolymakeObject - poly. It is not tested if the string is syntactically - correct as a part of a polymake file. It is also not tested if - the string is compatible with the data already contained in the - file. + Sets the polymake property name of poly to value and + rewrites the file. Rational numbers are written the way polymake spells + them; no check is made that value is meaningful for name, or + consistent with the properties set so far. +

+ Before version 0.9 this took a string to append verbatim to the file. @@ -230,12 +233,9 @@ rec( nothing - Takes a list pointlist of vectors and converts it into a - string which represents a polymake block labeled "POINTS". This - string is then added to the file associated with poly. - The "POINTS" block of the file associated with poly then - contains points with leading ones, as polymake uses affine - notation. + Sets the "POINTS" property of poly to the list + pointlist of vectors. The points are written with leading ones, as + polymake uses affine notation. @@ -252,38 +252,22 @@ rec( nothing - Just appends the inequalities given in ineqlist to the polymake - object poly (with caption "INEQUALITIES"). Note that this does - not check if an "INEQUALITIES" section does already exist in the file - associated with poly. + Sets the "INEQUALITIES" property of the polymake object poly to + ineqlist. Unlike points and vertices, inequalities are written as + given. - - - String - - This function takes a matrix matrix and converts it to a - string. This string can then be appended to a polymake file via - to form a block of data labeled - name. - - This may be used to write blocks like INEQUALITIES or FACETS. - - - nothing Deletes all known properties of the PolymakeObject - poly and replaces its file with an empty one. + poly and replaces its file with an empty object of the same type.
- If the triple of strings appvertyp specifying application, version - and type (see ) is given, the file is - replaced with a file that contains only a header specifying application, - version and type of the polymake object. + If appvertyp is given, the object is given that polymake type + instead, see .
diff --git a/doc/internals.xml b/doc/internals.xml index e60c578..d740470 100644 --- a/doc/internals.xml +++ b/doc/internals.xml @@ -44,205 +44,87 @@ output

The General Method -When polymake is called, its output is read as a string and then -processed as follows: - - - the lines containing upper case letters are found. These - are treated as lines containing the keywords. Each of those lines - marks the beginning of a block of data. - The string is then cut into a list of blocks (also strings). Each - block starts with a line containing the keyword and continues with some lines of data. - -for each of the blocks, the appropriate function of - ObjectConverters is called. Here "appropriate" just means, - that the keyword of the block coincides with the name of the - function. -The output of the conversion function is then added to - the known properties of the PolymakeObject for which - Polymake was called. - - -Converter- Philosopy - -The converter functions should take meaningful polymake data into meaningful +polymaking hands its requests to the polymake script lib/pm.pl, which +evaluates each of them and serializes the results into a single JSON file using +polymake's own serializer. &GAP; reads that file with the +json package and converts each value. + +

+ +The conversion is driven by the polymake type of the value, which is part of +what polymake writes, so there is no table mapping keywords to parsers -- a +Matrix<Rational> is decoded as a matrix of rationals whether or not +polymaking has ever heard of the keyword. Note that polymake writes its own +number types as strings, so that no precision is lost. + +Converter Philosophy + +The converters take meaningful polymake data into meaningful &GAP; data. This sometimes means that the (mathematical) representation is changed. Here is an example: polymake writes vectors as augmented affine vectors of the form 1 a1 a2 a3... which does not go very well with the usual &GAP; conventions of column vectors and multiplying matrices from the right. So polymaking converts such a vector to [a1,a2,a3,...] and the user is left with the problem of -augmentation and left or right multiplication. +augmentation and left or right multiplication. This applies to +POINTS, VERTICES, REL&uscore;INT&uscore;POINT, +VALID&uscore;POINT and VERTEX&uscore;BARYCENTER; it is a property +of the keyword, not of the type, so it cannot be derived.

Another area where the &GAP; object isn't a literal translation from the -polymake world is combinatorics. In Polymake, list elements are enumerated +polymake world is combinatorics. In polymake, list elements are enumerated starting from 0. &GAP; enumerates lists starting at 1. So the conversion -process adds 1 to the numbers corresponding to vertices in facet lists, for -example. +process adds 1 to everything that is an index: sets of integers, incidence +matrices and graph adjacency lists. Lists of plain integers, such as +FACET&uscore;DEGREES, are values rather than indices and are left alone.

-The conversion process is done by the following methods: - +The conversion is done by the following functions. You are unlikely to need +them directly. - - Record having polymake keywords as entry names and - the respective converted polymake output as entries. - + + Record with components name and params - - Given a the output of the polymake program as a string - string, this method first calls . For each of the returned - blocks, the name (=first line) of the block is read and the record - is looked up for an entry with that - name. If such an entry exists, it (being a function!) is called - and passed the block. The returned value is then given the name of - the block and added to the record returned by - ConvertPolymakeOutputToGapNotation. - + Parses a polymake type such as "common::Array<Set<Int>>" + into a record; the application prefix is dropped and type parameters are + parsed recursively. Passing fail, which is how the + json package reports a JSON null, gives the empty + name; polymake uses null for values that are plain Perl scalars. - - - - List of strings -- "blocks"-- - - The string string is cut at the lines starting with an upper case - character and consisting only of upper case - letters, numbers and underscore (&uscore;) characters. - The parts are returned as a list of - strings. The initial string string remains unchanged. - - - - - - The entries of this - record are labeled by polymake keywords. Each of the entries is a - function which converts a string returned by polymake to &GAP; - format. So far, only a few converters are implemented. To see - which, try - RecNames(ObjectConverters); - - You can define new converters using the basic functions - described in section . - - - - -

- - -
Conversion Functions - - The following functions are used for the functions in - . - - + - The string string is converted to a rational number. Unlike - Rat, it tests, if the number represented by string - is a floating point number an converts it correctly. If this is - the case, a warning is issued. + Converts the JSON value data into a &GAP; object, guided by the + parsed type. Understands scalars, dense and sparse vectors and + matrices, sets, incidence matrices, graph adjacency lists, node maps and + arrays. Unknown types are returned unchanged, with a warning at + level 1. - + - If list contains a single string, this string is converted - into a number using . + Calls for the polymake result + entry and then applies the conventions that depend on + keyword rather than on the type, as described above. - - - - Tries to decide if the list list of strings represents a matrix or - a list of sets by testing if they start with "{". It then calls either - or . - The "PlusOne" version calls if list represents a list -of sets. - - - - - - + + String - The list list of strings is interpreted as a list of row - vectors and converted into a matrix. - - The "KillOnes" version removes the leading ones. + The other direction: renders the record properties as a polymake + data file of polymake type type. Rational numbers are written as + strings, which is how polymake spells them, so nothing is rounded. - - - - - - - As the corresponding "Matrix" version. Just for vectors. - ConvertPolymakeIntVectorToGAPPlusOne requires the vector to - contain integers. It also adds 1 to every entry. - - - - - - - - If list contains a single string, which is either - 0,false,1, or true this function returns false or - true, respectively. - - - - - - - - - Let list be a list containing a single string, which is a - list of numbers separated by whitespaces and enclosed by - &obrace; and &cbrace; . The returned value is then a set of - rational numbers (in the GAP sense). - - - - - - - - - - Let list be a list containing several strings representing sets. - Then each of these strings is converted to a set of rational numbers and - the returned value is the list of all those sets. - The "PlusOne" version adds 1 to every entry. - - - - - - - - - Let list be a list of strings representing sets (that is, - a list of integers enclosed by &obrace; and &cbrace;). Then a record is returned - containing two sets named .vertices and .edges. - - - - -
+
diff --git a/doc/output.xml b/doc/output.xml index e3d8a8c..399cccd 100644 --- a/doc/output.xml +++ b/doc/output.xml @@ -7,18 +7,15 @@ This method calls the polymake program (see ) with the option option. You may use several keywords such as - "FACETS VERTICES" as an option. The returned value is cut into blocks - starting with keywords (which are taken from output and not - looked up in option). Each block is then - interpreted and translated into &GAP; readable form. This - translation is done using the functions given in . + "FACETS VERTICES" as an option. Each keyword names a polymake + property; nested properties are reached with a dot, as in + "HASSE&uscore;DIAGRAM.FACES". polymake computes each of them and + returns its value, which is then translated into &GAP; readable form as + described in Chapter . - The first line of each block of polymake output is taken as a - keyword and the according entry in - is called to convert the block into &GAP; readable form. If no - conversion function is known, an info string is printed and fail is - returned. + Keywords are evaluated independently, so one of them failing does not stop + the others; whatever polymake said about the failures can be found in + . If only one keyword has been given as option, Polymake returns the result of the conversion operation. diff --git a/init.g b/init.g index ef51f4e..502806b 100644 --- a/init.g +++ b/init.g @@ -1,7 +1,5 @@ ReadPackage("polymaking","lib/environment.gd" ); ReadPackage("polymaking","lib/Objects.gd"); ReadPackage("polymaking","lib/construct.gd"); -ReadPackage("polymaking","lib/workaround_maps.gd"); -ReadPackage("polymaking","lib/convert.gd"); -ReadPackage("polymaking","lib/ObjectConverters.gd"); +ReadPackage("polymaking","lib/json.gd"); ReadPackage("polymaking","lib/application_version_type.gd"); diff --git a/lib/ObjectConverters.gd b/lib/ObjectConverters.gd deleted file mode 100644 index 86e5ff3..0000000 --- a/lib/ObjectConverters.gd +++ /dev/null @@ -1,27 +0,0 @@ -############################################################################# -## -#W ObjectConverters.gd polymaking Package Marc Roeder -## -## - -## -## -#Y Copyright (C) 2006 Marc Roeder -#Y -#Y This program is free software; you can redistribute it and/or -#Y modify it under the terms of the GNU General Public License -#Y as published by the Free Software Foundation; either version 2 -#Y of the License, or (at your option) any later version. -#Y -#Y This program is distributed in the hope that it will be useful, -#Y but WITHOUT ANY WARRANTY; without even the implied warranty of -#Y MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -#Y GNU General Public License for more details. -#Y -#Y You should have received a copy of the GNU General Public License -#Y along with this program; if not, write to the Free Software -#Y Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA -## -DeclareGlobalVariable("ObjectConverters","Functions to convert polymake output to gap"); - - \ No newline at end of file diff --git a/lib/ObjectConverters.gi b/lib/ObjectConverters.gi deleted file mode 100644 index bcc4c0f..0000000 --- a/lib/ObjectConverters.gi +++ /dev/null @@ -1,85 +0,0 @@ -############################################################################# -## -#W ObjectConverters.gi polymaking Package Marc Roeder -## -## - -## -## -#Y Copyright (C) 2006 Marc Roeder -#Y -#Y This program is free software; you can redistribute it and/or -#Y modify it under the terms of the GNU General Public License -#Y as published by the Free Software Foundation; either version 2 -#Y of the License, or (at your option) any later version. -#Y -#Y This program is distributed in the hope that it will be useful, -#Y but WITHOUT ANY WARRANTY; without even the implied warranty of -#Y MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -#Y GNU General Public License for more details. -#Y -#Y You should have received a copy of the GNU General Public License -#Y along with this program; if not, write to the Free Software -#Y Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA -## -InstallValue(ObjectConverters, rec( - ALTSHULER_DET:=ConvertPolymakeScalarToGAP, - AMBIENT_DIM:=ConvertPolymakeScalarToGAP, - BALANCED:=ConvertPolymakeBoolToGAP, - BALANCE:=ConvertPolymakeScalarToGAP, - BOUNDED:=ConvertPolymakeBoolToGAP, - CENTERED:=ConvertPolymakeBoolToGAP, - COCUBICAL:=ConvertPolymakeBoolToGAP, - COMPLEXITY:=ConvertPolymakeScalarToGAP, - CUBICAL:=ConvertPolymakeBoolToGAP, - COCUBICALITY:=ConvertPolymakeScalarToGAP, - COMPLEXITY:=ConvertPolymakeScalarToGAP, - DIMS:=ConvertPolymakeVectorToGAP, ## done using mapping-hack - DIM:=ConvertPolymakeScalarToGAP, - DUAL_CONNECTIVITY:=ConvertPolymakeScalarToGAP, - DUAL_DIAMETER:=ConvertPolymakeScalarToGAP, - DUAL_EVEN:=ConvertPolymakeBoolToGAP, - DUAL_GRAPH_SIGNATURE:=ConvertPolymakeScalarToGAP, - DUAL_TRIANGLE_FREE:=ConvertPolymakeBoolToGAP, - EQUATIONS:=ConvertPolymakeMatrixToGAP, - ESSENTIALLY_GENERIC:=ConvertPolymakeBoolToGAP, - EVEN:=ConvertPolymakeBoolToGAP, - F_VECTOR:=ConvertPolymakeVectorToGAP, - F2_VECTOR:=ConvertPolymakeMatrixToGAP, - FACES:=ConvertPolymakeListOfSetsToGAPPlusOne, - ADJACENCY:=ConvertPolymakeListOfSetsToGAPPlusOne, - FACET_DEGREES:=ConvertPolymakeVectorToGAP, - FACETS:=ConvertPolymakeMatrixOrListOfSetsToGAPPlusOne, - FAR_HYPERPLANE:=ConvertPolymakeVectorToGAP, - FATNESS:=ConvertPolymakeScalarToGAP, - FEASIBLE:=ConvertPolymakeBoolToGAP, - GRAPH:=ConvertPolymakeGraphToGAP, - GRAPH_SIGNATURE:=ConvertPolymakeScalarToGAP, - INEQUALITIES:=ConvertPolymakeMatrixToGAP, - LATTICE:=ConvertPolymakeBoolToGAP, - N_01POINTS:=ConvertPolymakeScalarToGAP, - N_BOUNDED_VERTICES:=ConvertPolymakeScalarToGAP, - N_FACETS:=ConvertPolymakeScalarToGAP, - N_FLAGS:=ConvertPolymakeScalarToGAP, - N_INEQUALITIES:=ConvertPolymakeScalarToGAP, - N_POINTS:=ConvertPolymakeScalarToGAP, - N_RIDGES:=ConvertPolymakeScalarToGAP, - N_VERTEX_FACET_INC:=ConvertPolymakeScalarToGAP, - N_VERTICES:=ConvertPolymakeScalarToGAP, - NEIGHBORLINESS:=ConvertPolymakeScalarToGAP, - NEIGHBORLY:=ConvertPolymakeBoolToGAP, - MINIMAL_VERTEX_ANGLE:=ConvertPolymakeScalarToGAP, - POINTED:=ConvertPolymakeBoolToGAP, - POINTS:=ConvertPolymakeMatrixToGAPKillOnes, - POSITIVE:=ConvertPolymakeBoolToGAP, - REL_INT_POINT:=ConvertPolymakeVectorToGAPKillOne, - SELF_DUAL:=ConvertPolymakeBoolToGAP, - SIMPLE:=ConvertPolymakeBoolToGAP, - SIMPLICIAL:=ConvertPolymakeBoolToGAP, - VALID_POINT:=ConvertPolymakeVectorToGAPKillOne, - VERTEX_BARYCENTER:=ConvertPolymakeVectorToGAPKillOne, - VERTEX_DEGREES:=ConvertPolymakeVectorToGAP, - VERTICES:=ConvertPolymakeMatrixToGAPKillOnes, - VERTICES_IN_FACETS:=ConvertPolymakeListOfSetsToGAPPlusOne, - VOLUME:=ConvertPolymakeScalarToGAP, -)); diff --git a/lib/Objects.gd b/lib/Objects.gd index 07711cf..1e3cefc 100644 --- a/lib/Objects.gd +++ b/lib/Objects.gd @@ -28,7 +28,9 @@ DeclareRepresentation("IsPolymakeObjectRep", IsComponentObjectRep, ["dir", "filename", - "knownProperties"] + "knownProperties", + "input", + "type"] ); # diff --git a/lib/Objects.gi b/lib/Objects.gi index 652abb7..ee68f53 100644 --- a/lib/Objects.gi +++ b/lib/Objects.gi @@ -165,27 +165,28 @@ end); InstallMethod(ClearPolymakeObject, [IsPolymakeObject], function(poly) - CreateEmptyFile(FullFilenameOfPolymakeObject(poly)); Unbind(poly!.knownProperties); + InitPolymakeObject(poly); end); -# clear known data. Clear file and then set application, version,type -# information +# clear known data, then set the polymake type. The three element form takes the +# [application, version, type] list the pre-0.9 interface used; polymake 4 has +# no use for the version, and wants the type qualified by the application. InstallMethod(ClearPolymakeObject, [IsPolymakeObject,IsDenseList], function(poly,appvertyp) - local appendstring; - if not CheckAppVerTypList(appvertyp) - then + local type; + if IsString(appvertyp) then + type:=appvertyp; + elif CheckAppVerTypList(appvertyp) then + type:=Concatenation(appvertyp[1],"::",appvertyp[3]); + else Error("application, version, type not well-formed"); fi; - CreateEmptyFile(FullFilenameOfPolymakeObject(poly)); Unbind(poly!.knownProperties); - appendstring:=Concatenation(["_application ",appvertyp[1],"\n", - "_version ",appvertyp[2],"\n", - "_type ",appvertyp[3],"\n"] - ); - AppendToPolymakeObject(poly,appendstring); + poly!.input:=rec(); + poly!.type:=type; + POLYMAKING_WriteObject(poly); end); # Deleting a something known: diff --git a/lib/construct.gd b/lib/construct.gd index b97490b..e5e83fe 100644 --- a/lib/construct.gd +++ b/lib/construct.gd @@ -36,10 +36,9 @@ DeclareOperation("CreatePolymakeObject",[IsDirectory,IsDenseList]); DeclareOperation("CreatePolymakeObject",[IsString,IsDirectory]); DeclareOperation("CreatePolymakeObject",[IsString,IsDirectory,IsDenseList]); -DeclareOperation("ConvertMatrixToPolymakeString",[IsString,IsDenseList]); -DeclareOperation("AppendToPolymakeObject",[IsPolymakeObject,IsString]); -DeclareOperation("AppendToPolymakeObject",[IsPolymakeObject,IsString,IsString]); +DeclareOperation("AppendToPolymakeObject",[IsPolymakeObject,IsString,IsObject]); +DeclareGlobalFunction("POLYMAKING_WriteObject"); DeclareOperation("AppendPointlistToPolymakeObject",[IsPolymakeObject,IsDenseList]); DeclareOperation("AppendVertexlistToPolymakeObject",[IsPolymakeObject,IsDenseList]); DeclareOperation("AppendInequalitiesToPolymakeObject",[IsPolymakeObject,IsDenseList]); diff --git a/lib/construct.gi b/lib/construct.gi index 30b43ce..2a687c5 100644 --- a/lib/construct.gi +++ b/lib/construct.gi @@ -28,15 +28,24 @@ InstallMethod(CreateEmptyFile,[IsString], PrintTo(name,""); end); +# polymaking writes polymake's own JSON format, so polymake never has to convert +# the file and never says so on stderr. +BindGlobal("POLYMAKING_DEFAULT_TYPE", "polytope::Polytope"); + +InstallGlobalFunction(POLYMAKING_WriteObject, function(poly) + if not "input" in NamesOfComponents(poly) then + ErrorNoReturn("this polymake object was read from an existing file, ", + "so polymaking cannot add properties to it"); + fi; + FileString(FullFilenameOfPolymakeObject(poly), + PolymakeEncodeObject(poly!.type, poly!.input)); +end); + InstallMethod(InitPolymakeObject,[IsPolymakeObject], function(poly) - local appstring; - # In polymake 4.1 this is required. - appstring:=Concatenation( - "_application polytope\n", - "_type Polytope\n\n" - ); - AppendToPolymakeObject(poly, appstring); + poly!.input:=rec(); + poly!.type:=POLYMAKING_DEFAULT_TYPE; + POLYMAKING_WriteObject(poly); return poly; end); @@ -125,100 +134,56 @@ InstallMethod(CreatePolymakeObject,[IsDenseList], end); -InstallMethod(AppendToPolymakeObject,[IsPolymakeObject,IsString,IsString], +InstallMethod(AppendToPolymakeObject,[IsPolymakeObject,IsString,IsObject], function(poly,keyword,data) - local string; - string:=ShallowCopy(keyword); - while string[Size(string)] in ['\n','\r','\c'] - do - Unbind(string[Size(string)]); - od; - string:=Concatenation(keyword,"\n",data); - while string[Size(string)] in ['\n','\r','\c'] - do - Unbind(string[Size(string)]); - od; - Add(string,'\n'); - AppendToPolymakeObject(poly,string); -end); - - -InstallMethod(AppendToPolymakeObject,[IsPolymakeObject,IsString], - function(poly,string) - local file, retval; - - #file:=IO_File(record.filename,"w"); - #retval:=IO_WriteFlush(file,string); - file:=OutputTextFile(FullFilenameOfPolymakeObject(poly),true); - #SetPrintFormattingStatus(file,false); - retval:=WriteAll(file,string); - if not retval - then - Error("Error writing file"); + if not "input" in NamesOfComponents(poly) then + ErrorNoReturn("this polymake object was read from an existing file, ", + "so polymaking cannot add properties to it"); fi; - CloseStream(file); + poly!.input.(keyword):=data; + POLYMAKING_WriteObject(poly); end); -InstallMethod(ConvertMatrixToPolymakeString,[IsString,IsDenseList], - function(name,matrix) - local dim, string, point, stringpoint; +BindGlobal("POLYMAKING_CheckMatrix", function(matrix) + local dim; + if IsEmpty(matrix) then + return; + fi; dim:=Size(matrix[1]); - if not ForAll(matrix,point->Size(point)=dim) - then + if not ForAll(matrix,point->Size(point)=dim) then Error("not all rows have the same dimension"); - elif not ForAll(Concatenation(matrix),IsRat) - then + elif not ForAll(Concatenation(matrix),IsRat) then Error("matrix contains non-rational entries."); fi; - string:=ShallowCopy(name); - Append(string,"\n"); - for point in matrix - do - stringpoint:=JoinStringsWithSeparator(List(point,String)," "); - Append(string,stringpoint); - Append(string,"\n"); - od; - Append(string,"\n"); - return string; end); -# Now a few functions for convenience: +# polymake wants points in homogeneous coordinates +BindGlobal("POLYMAKING_Homogenize", + matrix -> List(matrix, p -> Concatenation([1],p))); + InstallMethod(AppendPointlistToPolymakeObject,[IsPolymakeObject,IsDenseList], function(polygon,pointlist) - local list, point,string; - list:=[]; - for point in pointlist - do - Add(list,Concatenation([1],point)); - od; - string:=ConvertMatrixToPolymakeString("POINTS",list); - AppendToPolymakeObject(polygon,string); + POLYMAKING_CheckMatrix(pointlist); + AppendToPolymakeObject(polygon,"POINTS",POLYMAKING_Homogenize(pointlist)); end); InstallMethod(AppendVertexlistToPolymakeObject,[IsPolymakeObject,IsDenseList], function(polygon,pointlist) - local list, point,string; - list:=[]; - for point in pointlist - do - Add(list,Concatenation([1],point)); - od; - string:= ConvertMatrixToPolymakeString("VERTICES",list); - AppendToPolymakeObject(polygon,string); + POLYMAKING_CheckMatrix(pointlist); + AppendToPolymakeObject(polygon,"VERTICES",POLYMAKING_Homogenize(pointlist)); end); InstallMethod(AppendInequalitiesToPolymakeObject,[IsPolymakeObject,IsDenseList], function(polygon,ineqlist) - - AppendToPolymakeObject(polygon,ConvertMatrixToPolymakeString("INEQUALITIES",ineqlist)); + POLYMAKING_CheckMatrix(ineqlist); + AppendToPolymakeObject(polygon,"INEQUALITIES",ineqlist); end); - ############################## # Call polymake. If the option "PolymakeNolookup" is true, # it is not checked, whether Polymake has already been called @@ -226,151 +191,129 @@ end); # this by looking at the file associated to ). # +# polymake 4 spells nested properties with a dot. Keep the short names the +# pre-0.9 interface used; note DIMS is gone, polymake 4 cannot compute it from a +# polytope's Hasse diagram. +BindGlobal("POLYMAKING_ALIASES", MakeImmutable(rec( + FACES := "HASSE_DIAGRAM.FACES", + ADJACENCY := "HASSE_DIAGRAM.ADJACENCY", + GRAPH := "GRAPH.ADJACENCY" +))); + +BindGlobal("POLYMAKING_Keyword", function(kw) + if IsBound(POLYMAKING_ALIASES.(kw)) then + return POLYMAKING_ALIASES.(kw); + fi; + return kw; +end); + +# GRAPH is documented to come back as a record of vertices and edges, but +# polymake gives us adjacency lists. +BindGlobal("POLYMAKING_POSTPROCESS", MakeImmutable(rec( + GRAPH := function(adj) + local i, j, edges; + edges := []; + for i in [1..Length(adj)] do + for j in adj[i] do + AddSet(edges, Set([i,j])); + od; + od; + return rec(vertices := [1..Length(adj)], edges := edges); + end +))); + + InstallMethod(Polymake,"for PolymakeObject",[IsPolymakeObject,IsString], function(polygon,option) - local callPolymake, gapobject, splitoption, knownProperties, - returnval, returnedstring, block; - - callPolymake:=function(object,splitoption) - local returnedstring, scriptarg, errfile, p, stdout, stdin, - dir, cmd, exitstatus; - - returnedstring:=[]; - errfile:=POLYMAKING_ScratchFile("stderr.txt"); - RemoveFile(errfile); - scriptarg:=["--config-path", - UserPreference("polymaking","PolymakeConfigPath"), - "--script", - Filename(DirectoriesPackageLibrary("polymaking"), "pm_script_arg.pl"), - "--stderr", errfile]; - if UserPreference("polymaking","PolymakeQuiet")=true - then - Add(scriptarg,"--quiet"); - fi; - for p in UserPreference("polymaking","PolymakePreferences") - do - Append(scriptarg,["--prefer",p]); - od; - Add(scriptarg,"--"); - stdout:=OutputTextString(returnedstring,false); - stdin:=InputTextNone();; - dir:=DirectoryOfPolymakeObject(object); - if dir=fail + local keywords, known, lookup, wanted, dir, r, kw, ask, val, + returnval, failed; + + POLYMAKING_CheckVersion(); + + keywords:=Filtered(SplitString(NormalizedWhitespace(option)," "), x->x<>""); + if IsEmpty(keywords) + then + Error("you must pass an option to polymake"); + fi; + + known:=NamesKnownPropertiesOfPolymakeObject(polygon); + lookup:=ValueOption("PolymakeNolookup") in [fail,false] and known<>fail; + + wanted:=keywords; + if lookup + then + wanted:=Filtered(keywords, kw -> not kw in known); + fi; + + if not IsEmpty(wanted) + then + dir:=DirectoryOfPolymakeObject(polygon); + if dir=fail then dir:=DirectoryCurrent(); fi; - cmd:=PolymakeCommand(); - if cmd=fail - then - UpdatePolymakeFailReason("no usable polymake executable configured"); - ErrorNoReturn("polymake not found; set it via SetUserPreference(", - "\"polymaking\", \"PolymakeCommand\", )"); - fi; - exitstatus:=Process( dir, cmd, stdin, stdout, - Concatenation(scriptarg, [FullFilenameOfPolymakeObject(object)], - splitoption) - );; - CloseStream(stdout); - CloseStream(stdin); - errfile:=StringFile(errfile); - if errfile=fail + ask:=List(wanted, kw -> POLYMAKING_Keyword(kw)); + r:=POLYMAKING_Run(dir, Concatenation([FullFilenameOfPolymakeObject(polygon)], ask)); + + if r.result=fail then - errfile:=""; + UpdatePolymakeFailReason(Concatenation( + "polymake terminated with exit status ",String(r.status), + "\n",r.stderr)); + Error("polymake returned an error (error code ", r.status, ")\n", r.stderr); fi; - if errfile<>"" and exitstatus=0 + if IsBound(r.result.fatal) then - Info(InfoPolymaking,2,Chomp(errfile)); + UpdatePolymakeFailReason(r.result.fatal); + Error("polymake could not read ", + FullFilenameOfPolymakeObject(polygon),":\n",r.result.fatal); fi; - return rec(status:=exitstatus,string:=returnedstring,stderr:=errfile); - end; - - gapobject:=[]; - option:=NormalizedWhitespace(option); - splitoption:=SplitString(option," "); - knownProperties:=NamesKnownPropertiesOfPolymakeObject(polygon); - returnval:=[]; - - Info(InfoPolymaking,2,"option=",option); - Info(InfoPolymaking,2,"Size(splitoption)=",Size(splitoption)); - if Size(splitoption)=0 - then - Error("you must pass an option to polymake"); - - elif Size(splitoption)=1 - then - if ValueOption("PolymakeNolookup") in [fail,false] - and knownProperties<>fail - and splitoption[1] in knownProperties - then - returnval:=PropertyOfPolymakeObject(polygon,splitoption[1]); - else - Apply(splitoption, MapKeyWordToPolymakeFormat); - returnedstring:=callPolymake(polygon,splitoption); - Info(InfoPolymaking,2,String(returnedstring)); - if returnedstring.status <>0 - then - UpdatePolymakeFailReason(Concatenation("polymake terminated with exit status ", - String(returnedstring.status),"\n",returnedstring.stderr)); - returnval:=fail; - Error("polymake returned an error (error code ", returnedstring.status, - ")\n", returnedstring.stderr); - elif returnedstring.string<>[] + + failed:=[]; + for kw in wanted + do + if IsBound(r.result.values.(POLYMAKING_Keyword(kw))) then - Info(InfoPolymaking,2,returnedstring.string); - gapobject:=ConvertPolymakeOutputToGapNotation(returnedstring.string); - if gapobject[1].object<>fail + val:=PolymakeDecodeProperty(kw, + r.result.values.(POLYMAKING_Keyword(kw))); + if IsBound(POLYMAKING_POSTPROCESS.(kw)) then - WriteKnownPropertyToPolymakeObject(polygon,gapobject[1].name,gapobject[1].object); - returnval:=gapobject[1].object; - else - returnval:=fail; + val:=POLYMAKING_POSTPROCESS.(kw)(val); fi; + WriteKnownPropertyToPolymakeObject(polygon,kw,val); else - UpdatePolymakeFailReason("polymake did not return anything"); - returnval:=fail; + Add(failed,kw); fi; - fi; - - else - # we return fail, whatever happens. - ## only the reason may change... - ### - returnval:=fail; - UpdatePolymakeFailReason("polymake called with multiple keywords"); - if ValueOption("PolymakeNolookup") in [fail,false] - and knownProperties<>fail + od; + if not IsEmpty(failed) then - splitoption:=Filtered(splitoption,i->not i in knownProperties); + UpdatePolymakeFailReason(Concatenation( + "polymake could not compute ", + JoinStringsWithSeparator(failed,", "),":\n", + JoinStringsWithSeparator( + List(failed, kw -> Concatenation(kw,": ", + r.result.errors.(POLYMAKING_Keyword(kw)))), + ""))); fi; - if Size(splitoption)>0 + fi; + + # as before: a single keyword returns its value, several always return fail + if Size(keywords)>1 + then + if IsEmpty(wanted) then - Apply(splitoption, MapKeyWordToPolymakeFormat); - returnedstring:=callPolymake(polygon,splitoption); - if returnedstring.status <>0 - then - UpdatePolymakeFailReason(Concatenation("polymake terminated with exit status ", - String(returnedstring.status),"\n",returnedstring.stderr)); - Error("polymake returned an error (error code ", returnedstring.status, - ")\n", returnedstring.stderr); - elif returnedstring.string<>[] - then - Info(InfoPolymaking,2,returnedstring.string); - gapobject:=ConvertPolymakeOutputToGapNotation(returnedstring.string); - - for block in Filtered(gapobject,i->i.object<>fail) - do - WriteKnownPropertyToPolymakeObject(polygon,block.name,block.object); - od; - else - UpdatePolymakeFailReason("polymake didn't return anything. All keywords that would have triggered output were looked up."); - fi; - + UpdatePolymakeFailReason( + "polymake called with multiple keywords"); fi; - fi; - return returnval; -end); - - - + return fail; + fi; - + known:=NamesKnownPropertiesOfPolymakeObject(polygon); + if known<>fail and keywords[1] in known + then + returnval:=PropertyOfPolymakeObject(polygon,keywords[1]); + else + returnval:=fail; + fi; + return returnval; +end); diff --git a/lib/convert.gd b/lib/convert.gd deleted file mode 100644 index 6b4277a..0000000 --- a/lib/convert.gd +++ /dev/null @@ -1,53 +0,0 @@ -############################################################################# -## -#W convert.gd polymaking Package Marc Roeder -## -## - -## -## -#Y Copyright (C) 2006 Marc Roeder -#Y -#Y This program is free software; you can redistribute it and/or -#Y modify it under the terms of the GNU General Public License -#Y as published by the Free Software Foundation; either version 2 -#Y of the License, or (at your option) any later version. -#Y -#Y This program is distributed in the hope that it will be useful, -#Y but WITHOUT ANY WARRANTY; without even the implied warranty of -#Y MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -#Y GNU General Public License for more details. -#Y -#Y You should have received a copy of the GNU General Public License -#Y along with this program; if not, write to the Free Software -#Y Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA -## -DeclareOperation("ConvertPolymakeOutputToGapNotation",[IsString]); -DeclareOperation("SplitPolymakeOutputStringIntoBlocks",[IsString]); -DeclareOperation("ConvertedObject",[IsString,IsDenseList]); - -DeclareOperation("ConverterSyntaxError",[IsString]); - -DeclareOperation("ConvertPolymakeNumber",[IsString]); - -DeclareOperation("ConvertPolymakeScalarToGAP",[IsDenseList]); -DeclareOperation("ConvertPolymakeBoolToGAP",[IsDenseList]); -DeclareOperation("ConvertPolymakeDescriptionToGAP",[IsDenseList]); - -DeclareOperation("ConvertPolymakeMatrixOrListOfSetsToGAP",[IsDenseList]); -DeclareOperation("ConvertPolymakeMatrixOrListOfSetsToGAPPlusOne",[IsDenseList]); - -DeclareOperation("ConvertPolymakeMatrixToGAP",[IsDenseList]); -DeclareOperation("ConvertPolymakeMatrixToGAPKillOnes",[IsDenseList]); -DeclareOperation("ConvertPolymakeVectorToGAP",[IsDenseList]); -DeclareOperation("ConvertPolymakeVectorToGAPKillOne",[IsDenseList]); -DeclareOperation("ConvertPolymakeIntVectorToGAPPlusOne",[IsDenseList]); - - -DeclareOperation("ConvertPolymakeSetToGAP",[IsDenseList]); -DeclareOperation("ConvertPolymakeListOfSetsToGAP",[IsDenseList]); -DeclareOperation("ConvertPolymakeListOfSetsToGAPPlusOne",[IsDenseList]); - -DeclareOperation("ConvertPolymakeSetOfSetsToGAP",[IsDenseList]); -DeclareOperation("ConvertPolymakeGraphToGAP",[IsDenseList]); -DeclareOperation("ConvertPolymakeFaceLatticeToGAP",[IsDenseList]); diff --git a/lib/convert.gi b/lib/convert.gi deleted file mode 100644 index ee6f2e6..0000000 --- a/lib/convert.gi +++ /dev/null @@ -1,420 +0,0 @@ -############################################################################# -## -#W convert.gi polymaking Package Marc Roeder -## -## - -## -## -#Y Copyright (C) 2006 Marc Roeder -#Y -#Y This program is free software; you can redistribute it and/or -#Y modify it under the terms of the GNU General Public License -#Y as published by the Free Software Foundation; either version 2 -#Y of the License, or (at your option) any later version. -#Y -#Y This program is distributed in the hope that it will be useful, -#Y but WITHOUT ANY WARRANTY; without even the implied warranty of -#Y MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -#Y GNU General Public License for more details. -#Y -#Y You should have received a copy of the GNU General Public License -#Y along with this program; if not, write to the Free Software -#Y Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA -## -InstallMethod(ConvertPolymakeOutputToGapNotation,[IsString], - function(string) - local split, blockpositions, splitblocks, returnlist, - splitblock, name, rest, object; - - - splitblocks:=SplitPolymakeOutputStringIntoBlocks(string); - returnlist:=[]; - for splitblock in splitblocks - do - name:=NormalizedWhitespace(splitblock[1]); - if Size(splitblock) < 2 - then - Info(InfoPolymaking,2,"No data for ", name,". Record not updated for this block"); - UpdatePolymakeFailReason(Concatenation("polymake returned ",name," empty")); - Add(returnlist, rec(name:=name,object:=fail)); - continue; - fi; - rest:=splitblock{[2..Maximum(Size(splitblock),2)]}; - if rest[Size(rest)]="" - then - Unbind(rest[Size(rest)]); - fi; -# Apply(rest,NormalizedWhitespace); - - if rest=["==UNDEF=="] - then - Info(InfoPolymaking,1,Concatenation("polymake declares object ",name," undefined")); - UpdatePolymakeFailReason(Concatenation("polymake declares ",name," undefined")); - Add(returnlist,rec(name:=name,object:=fail)); - elif IsBound(ObjectConverters.(name)) - then - object:=ObjectConverters.(name)(rest); - if object<>fail - then - Add(returnlist,rec(name:=name,object:=object)); - else - Info(InfoPolymaking,1,"Conversion failed. Maybe invalid output from polymake. Record not updated for this block"); - Add(returnlist, rec(name:=name,object:=fail)); - fi; - else - Info(InfoPolymaking,1,"No Method to convert ", name,". Record not updated for this block"); - UpdatePolymakeFailReason(Concatenation("polymaking doesn't know how to convert ",name," to GAP")); - Add(returnlist, rec(name:=name,object:=fail)); - fi; - od; - return returnlist; -end); - - -# -#First the method to split polymake's output into different blocks by -# recognizing lines which may be keyword-lines -############### - -InstallMethod(SplitPolymakeOutputStringIntoBlocks,[IsString], - function(string) - local blocks, newblock, lines, line; - - blocks:=[]; - newblock:=[]; - lines:=SplitString(string,"\n"); - while lines<>[] - do - line:=Remove(lines,1); - if not IsEmptyString(line) - then - if IsUpperAlphaChar(line[1]) - and ForAll(line,c->IsUpperAlphaChar(c) or c in ['_','-','>'] or IsDigitChar(c)) - then - if blocks=[] and newblock=[] - then - newblock:=[MapKeyWordFromPolymakeFormat(line)]; - else - Add(blocks,ShallowCopy(newblock)); - newblock:=[MapKeyWordFromPolymakeFormat(line)]; - fi; - else - Add(newblock,line); - fi; - fi; - od; - if newblock<>[] - then - Add(blocks,newblock); - fi; - return blocks; -end); - - -############################################################################# -## -## update the fail reason in case of syntax errors: -## -InstallMethod(ConverterSyntaxError,[IsString], - function(methname) - UpdatePolymakeFailReason( - Concatenation("input for converter method ",methname, " syntactically incorrect (cast an error and went into break loop)") - ); - Error("incorrect input"); -end); - - -############################################################################# -# Now the different conversion methods: -############################################################################# -## -## Basics, numbers and bools: -## -InstallMethod(ConvertPolymakeNumber,[IsString], - function(string) - local sstring, denom, enum; - if '.' in string - then - Info(InfoPolymaking,1,"Warning!converting a floating point number"); - sstring:=SplitString(string,"."); - denom:=10^(Length(sstring[2])); - enum:=Int(Concatenation(sstring)); - return enum/denom; - elif string="" - then - ConverterSyntaxError("ConvertPolymakeNumber"); - return fail; - else - return Rat(string); - fi; -end); - - -InstallMethod(ConvertPolymakeScalarToGAP,[IsDenseList], - function(stringlist) - if Size(stringlist)<>1 - then - ConverterSyntaxError("ConvertPolymakeScalarToGAP"); - return fail; - else - return ConvertPolymakeNumber(stringlist[1]); - fi; -end); - - -InstallMethod(ConvertPolymakeBoolToGAP,[IsDenseList], - function(stringlist) - if Size(stringlist)<>1 - then - ConverterSyntaxError("ConvertPolymakeBoolToGAP"); - return fail; - elif stringlist[1]="1" or stringlist[1]="true" - then - return true; - elif stringlist[1]="0" or stringlist[1]="false" - then - return false; - else - UpdatePolymakeFailReason("Boolean conversion failed, the returned value wasn't 0, 1, true, or false. Error cast"); - Error("Conversion failed"); - fi; -end); - -InstallMethod(ConvertPolymakeDescriptionToGAP,[IsDenseList], - function(stringlist) - return JoinStringsWithSeparator(stringlist," "); -end); - -############################################################################# -## -## MATRICES AND LISTS OF SETS: -## -## Unfortunately some of the keywords that return matrices -## might also return lists of sets (this is a polytope/topaz problem). -## So we try to guess the type if necessary. -## -InstallMethod(ConvertPolymakeMatrixOrListOfSetsToGAP,[IsDenseList], - function(stringlist) - - if ForAll(stringlist,i->i[1]='{') - then - return ConvertPolymakeListOfSetsToGAP(stringlist); - elif ForAll(stringlist,i->not ('{' in i or '}' in i)) - then - return ConvertPolymakeMatrixToGAP(stringlist); - else - UpdatePolymakeFailReason("ConvertPolymakeMatrixOrListOfSetsToGAP couldn't decide what to do. Error cast"); - Error("don't know if this is a matrix or a list of sets "); - return fail; - fi; -end); - - -InstallMethod(ConvertPolymakeMatrixOrListOfSetsToGAPPlusOne,[IsDenseList], - function(stringlist) - - if ForAll(stringlist,i->i[1]='{') - then - return ConvertPolymakeListOfSetsToGAPPlusOne(stringlist); - elif ForAll(stringlist,i->not ('{' in i or '}' in i)) - then - return ConvertPolymakeMatrixToGAP(stringlist); - else - UpdatePolymakeFailReason("ConvertPolymakeMatrixOrListOfSetsToGAP couldn't decide what to do. Error cast"); - Error("don't know if this is a matrix or a list of sets "); - return fail; - fi; -end); - - -InstallMethod(ConvertPolymakeMatrixToGAP,[IsDenseList], - function(stringlist) - local returnmatrix, string, vector; - returnmatrix:=[]; - for string in stringlist - do - vector:=SplitString(string," "); - Apply(vector,ConvertPolymakeNumber); - Add(returnmatrix,vector); - od; - return returnmatrix; -end); - - -InstallMethod(ConvertPolymakeMatrixToGAPKillOnes,[IsDenseList], - function(stringlist) - local returnmatrix; - returnmatrix:=ConvertPolymakeMatrixToGAP(stringlist); - if not IsMatrix(returnmatrix) - or Size(returnmatrix[1])<2 - or not ForAll(returnmatrix,i->i[1]=1) - then - Error("returnmatrix is not a matrix or too small"); - UpdatePolymakeFailReason("Error in ConvertPolymakeMatrixToGAPKillOnes. The returned object is either not a matrix, has fewer than 2 columns or doesn't have all ones in the first column. (Error cast)"); - return fail; - fi; - if returnmatrix<>fail - then - return List(returnmatrix,i->i{[2..Size(i)]}); #kill leading ones - else - return fail; - fi; -end); - -############################################################################# -## -## Vectors -## -InstallMethod(ConvertPolymakeVectorToGAP,[IsDenseList], - function(stringlist) - local vector; - if Size(stringlist)<>1 - then - ConverterSyntaxError("ConvertPolymakeVectorToGAP"); - return fail; - else - vector:=SplitString(stringlist[1]," "); - Apply(vector,ConvertPolymakeNumber); - return vector; - fi; -end); - - -InstallMethod(ConvertPolymakeVectorToGAPKillOne,[IsDenseList], - function(stringlist) - local vector; - vector:=ConvertPolymakeVectorToGAP(stringlist); - if not IsVector(vector) or Size(vector)<2 - or vector[1]<>1 - then - Error("vector is not a vector or too small"); - UpdatePolymakeFailReason("Error in ConvertPolymakeVectorToGAPKillOne. The returned object is either not a vector or has fewer than 2 columns or doesn't start with 1. (Error cast)"); - return fail; - fi; - return vector{[2..Size(vector)]}; -end); - - -InstallMethod(ConvertPolymakeIntVectorToGAPPlusOne,[IsDenseList], - function(stringlist) - local vec; - - vec:=ConvertPolymakeVectorToGAP(stringlist); - if not ForAll(vec,IsInt) - then - Error("Vector is not a vector of integers as expected"); - UpdatePolymakeFailReason("Error in ConvertPolymakeIntVectorToGAPPlusOne. The returned vector did not consist of integers. Error cast"); - return fail; - else - return vec+1; - fi; -end); - - - - -############################################################################# -## -## Sets, Sets of Sets, Lists of Sets -## -InstallMethod(ConvertPolymakeSetToGAP,[IsDenseList], - function(stringlist) - local entries; - if not Size(stringlist)=1 and IsString(stringlist[1]) - then - ConverterSyntaxError("ConvertPolymakeSetToGAP"); - return fail; - else - entries:=ReplacedString(ReplacedString(stringlist[1],"{",""),"}",""); - entries:=Set(SplitString(entries," ")); - RemoveSet(entries,""); - return Set(entries,ConvertPolymakeNumber); - fi; -end); - - -InstallMethod(ConvertPolymakeSetOfSetsToGAP,[IsDenseList], - function(stringlist) - local returnlist, line, newlist, entry; - - returnlist:=[]; - if Size(stringlist)<> 1 or not IsString(stringlist[1]) - then - ConverterSyntaxError("ConvertPolymakeSetOfSetsToGAP"); - return fail; - fi; - line:=stringlist[1]; - newlist:=ReplacedString(line{[2..Size(line)-1]},"} {","},{"); - newlist:=SplitString(newlist,","); - for entry in newlist - do - Add(returnlist,ConvertPolymakeSetToGAP([entry])); - od; - return returnlist; -end); - - -InstallMethod(ConvertPolymakeListOfSetsToGAP,[IsDenseList], - function(stringlist) - return List(stringlist,s->ConvertPolymakeSetToGAP([s])); -end); - - -InstallMethod(ConvertPolymakeListOfSetsToGAPPlusOne,[IsDenseList], - function(stringlist) - return List(stringlist,s->ConvertPolymakeSetToGAP([s])+1); -end); - - -############################################################################# -## -## More complex stuff: -## - -############################################################################# -## -## Graphs: -## -InstallMethod(ConvertPolymakeGraphToGAP,[IsDenseList], - function(stringlist) - local edgelist, edge, vertices; - edgelist:=[]; - for edge in stringlist - do -# verts:=ReplacedString(ReplacedString(edge,"{",""),"}",""); -# verts:=SplitString(verts," "); - Add(edgelist,ConvertPolymakeSetToGAP(edge)); - od; - vertices:=Set(Flat(edgelist)); - return rec(vertices:=vertices,edges:=Set(edgelist)); -end); - - - -############################################################################# -## -## Face Lattices: -## -InstallMethod(ConvertPolymakeFaceLatticeToGAP,[IsDenseList], - function(stringlist) - local returnlist, line, faces; - returnlist:=[]; - for line in stringlist - do - if line<>"" and line[1]='{' - then - faces:=Set(ConvertPolymakeSetOfSetsToGAP([line])); - Apply(faces,i->i+1); - Add(returnlist,faces); - fi; - od; - return returnlist; -end); - - - -############################################################################# -## -#E END of file convert.gi -## diff --git a/lib/environment.gd b/lib/environment.gd index 1dd47de..f3aa10f 100644 --- a/lib/environment.gd +++ b/lib/environment.gd @@ -29,6 +29,9 @@ DeclareInfoClass("InfoPolymaking"); ## DeclareGlobalFunction("PolymakeCommand"); DeclareGlobalFunction("PolymakeDataDirectory"); +DeclareGlobalFunction("PolymakeVersion"); +DeclareGlobalFunction("POLYMAKING_Run"); +DeclareGlobalFunction("POLYMAKING_CheckVersion"); ## ## deprecated in favour of the user preferences PolymakeCommand and diff --git a/lib/environment.gi b/lib/environment.gi index 99ce489..2ae48b7 100644 --- a/lib/environment.gi +++ b/lib/environment.gi @@ -49,6 +49,90 @@ InstallMethod(SetPolymakeDataDirectory,[IsDirectory], end); +## +## Running polymake. Everything goes through lib/pm.pl, which writes its result +## to a file: polymake's own chatter goes to stderr, so a result on stdout could +## never be trusted. +## +InstallGlobalFunction(POLYMAKING_Run, function(dir, args) + local cmd, errfile, resfile, scriptarg, p, out, status, err, res; + + cmd := PolymakeCommand(); + if cmd = fail then + UpdatePolymakeFailReason("no usable polymake executable configured"); + ErrorNoReturn("polymake not found; set it via SetUserPreference(", + "\"polymaking\", \"PolymakeCommand\", )"); + fi; + + errfile := POLYMAKING_ScratchFile("stderr.txt"); + resfile := POLYMAKING_ScratchFile("result.json"); + RemoveFile(errfile); + RemoveFile(resfile); + + scriptarg := ["--config-path", UserPreference("polymaking","PolymakeConfigPath"), + "--script", Filename(DirectoriesPackageLibrary("polymaking"), "pm.pl"), + "--stderr", errfile]; + if UserPreference("polymaking","PolymakeQuiet") = true then + Add(scriptarg, "--quiet"); + fi; + for p in UserPreference("polymaking","PolymakePreferences") do + Append(scriptarg, ["--prefer", p]); + od; + Append(scriptarg, ["--", resfile]); + Append(scriptarg, args); + + out := OutputTextNone(); + status := Process(dir, cmd, InputTextNone(), out, scriptarg); + CloseStream(out); + + err := StringFile(errfile); + if err = fail then + err := ""; + fi; + if err <> "" then + Info(InfoPolymaking, 2, Chomp(err)); + fi; + + res := StringFile(resfile); + if res = fail then + return rec(status := status, stderr := err, result := fail); + fi; + return rec(status := status, stderr := err, result := JsonStringToGap(res)); +end); + + +InstallGlobalFunction(PolymakeVersion, function() + local r; + if POLYMAKING_STATE.version = fail then + r := POLYMAKING_Run(DirectoryCurrent(), ["--version"]); + if r.result = fail or not IsBound(r.result.version) then + return fail; + fi; + POLYMAKING_STATE.version := r.result.version; + fi; + return POLYMAKING_STATE.version; +end); + + +# polymaking 0.9 writes and reads polymake's JSON format, which polymake 3 and +# earlier cannot handle at all, so this is an error rather than a warning. +InstallGlobalFunction(POLYMAKING_CheckVersion, function() + local v; + if POLYMAKING_STATE.versionChecked then + return; + fi; + v := PolymakeVersion(); + if v = fail then + ErrorNoReturn("could not determine the polymake version; check that ", + PolymakeCommand(), " works"); + elif not CompareVersionNumbers(v, "4.0") then + ErrorNoReturn("polymaking requires polymake 4.0 or newer, but found ", + v, ". Use polymaking 0.8.9 with older versions of polymake."); + fi; + POLYMAKING_STATE.versionChecked := true; +end); + + if PolymakeCommand() = fail then Info(InfoWarning, 1, "polymake command not found; set it via ", "SetUserPreference(\"polymaking\", \"PolymakeCommand\", )"); diff --git a/lib/json.gd b/lib/json.gd new file mode 100644 index 0000000..0d17d50 --- /dev/null +++ b/lib/json.gd @@ -0,0 +1,12 @@ +############################################################################# +## +#W json.gd polymaking Package +## +## Decoding of the JSON that polymake 4 produces. See lib/pm.pl. +## + +DeclareGlobalFunction("PolymakeParseType"); +DeclareGlobalFunction("PolymakeDecodeValue"); +DeclareGlobalFunction("PolymakeDecodeProperty"); +DeclareGlobalFunction("PolymakeEncodeObject"); +DeclareGlobalFunction("POLYMAKING_EncodeValue"); diff --git a/lib/json.gi b/lib/json.gi new file mode 100644 index 0000000..7e86eed --- /dev/null +++ b/lib/json.gi @@ -0,0 +1,228 @@ +############################################################################# +## +#W json.gi polymaking Package +## +## polymake serializes a value as { "data": ..., "_type": ..., "_ns": ... }. +## The type tells us the shape, so unlike the pre-0.9 converters we do not +## need a table mapping every keyword to a parser; only the conventions that +## cannot be read off the type are keyword specific, see the two records at +## the bottom of this file. +## + +# "common::Array>" -> rec(name:="Array", params:=[rec(name:="Set", ...)]) +InstallGlobalFunction(PolymakeParseType, function(str) + local pos, parse, name, params, depth, start, i; + + if str = fail then + return rec(name := "", params := []); + fi; + + # strip the application prefix + pos := PositionSublist(str, "::"); + if pos <> fail then + str := str{[pos+2..Length(str)]}; + fi; + + pos := Position(str, '<'); + if pos = fail then + return rec(name := NormalizedWhitespace(str), params := []); + fi; + + name := NormalizedWhitespace(str{[1..pos-1]}); + params := []; + depth := 0; + start := pos+1; + for i in [pos+1..Length(str)] do + if str[i] = '<' then + depth := depth+1; + elif str[i] = '>' then + if depth = 0 then + Add(params, str{[start..i-1]}); + break; + fi; + depth := depth-1; + elif str[i] = ',' and depth = 0 then + Add(params, str{[start..i-1]}); + start := i+1; + fi; + od; + return rec(name := name, params := List(params, PolymakeParseType)); +end); + + +# polymake writes its own number types as strings, plain perl values as native +# JSON, so both spellings turn up. +BindGlobal("POLYMAKING_Scalar", function(x) + if IsString(x) then + return Rat(x); + fi; + return x; +end); + + +BindGlobal("POLYMAKING_Indices", l -> Set(l, i -> i+1)); + + +# a dense list, or a sparse record {"3": v, "_dim": n} +BindGlobal("POLYMAKING_Vector", function(v) + local r, k; + if not IsRecord(v) then + return List(v, POLYMAKING_Scalar); + fi; + r := ListWithIdenticalEntries(v._dim, 0); + for k in RecNames(v) do + if k <> "_dim" then + r[Int(k)+1] := POLYMAKING_Scalar(v.(k)); + fi; + od; + return r; +end); + + +# rows, with an optional trailing {"cols": n} giving the width +BindGlobal("POLYMAKING_Rows", function(data) + local rows, cols, last; + rows := ShallowCopy(data); + cols := fail; + if not IsEmpty(rows) then + last := rows[Length(rows)]; + if IsRecord(last) and IsBound(last.cols) then + cols := Remove(rows).cols; + fi; + fi; + return rec(rows := rows, cols := cols); +end); + + +BindGlobal("POLYMAKING_Matrix", function(data) + local r, m; + r := POLYMAKING_Rows(data); + m := List(r.rows, function(row) + local v; + if IsRecord(row) then + row := ShallowCopy(row); + row._dim := r.cols; + fi; + v := POLYMAKING_Vector(row); + if r.cols <> fail and Length(v) < r.cols then + Append(v, ListWithIdenticalEntries(r.cols - Length(v), 0)); + fi; + return v; + end); + return m; +end); + + +InstallGlobalFunction(PolymakeDecodeValue, function(type, data) + local name; + + if data = fail then + return fail; + fi; + name := type.name; + + # _type null: a plain perl value, already native JSON + if name = "" then + return data; + + elif name in ["Rational", "Integer", "Int", "Float", "Bool"] then + return POLYMAKING_Scalar(data); + + elif name in ["Vector", "SparseVector"] then + return POLYMAKING_Vector(data); + + elif name in ["Matrix", "SparseMatrix"] then + return POLYMAKING_Matrix(data); + + # rows are index sets, shifted to GAP's 1-based convention + elif name = "IncidenceMatrix" then + return List(POLYMAKING_Rows(data).rows, POLYMAKING_Indices); + + elif name = "GraphAdjacency" then + return List(data, POLYMAKING_Indices); + + elif name = "Set" then + if not IsEmpty(type.params) and type.params[1].name in ["Int"] then + return POLYMAKING_Indices(data); + fi; + return Set(data, POLYMAKING_Scalar); + + elif name = "Array" then + if IsEmpty(type.params) then + return data; + fi; + return List(data, x -> PolymakeDecodeValue(type.params[1], x)); + + elif name = "NodeMap" then + if Length(type.params) < 2 then + return data; + fi; + return List(data, x -> PolymakeDecodeValue(type.params[2], x)); + fi; + + Info(InfoPolymaking, 1, "polymaking does not know the polymake type ", + name, "; returning the raw JSON value"); + return data; +end); + + +## +## Conventions that the type cannot tell us about. +## + +# polymake writes points and vectors in homogeneous coordinates; GAP users want +# them without the leading 1. +BindGlobal("POLYMAKING_DEHOMOGENIZE_ROWS", + MakeImmutable(Set(["POINTS", "VERTICES"]))); + +BindGlobal("POLYMAKING_DEHOMOGENIZE", + MakeImmutable(Set(["REL_INT_POINT", "VALID_POINT", "VERTEX_BARYCENTER"]))); + +# node numbers that are plain integers, so not caught by the Set rule +BindGlobal("POLYMAKING_SHIFT_SCALAR", + MakeImmutable(Set(["TOP_NODE", "BOTTOM_NODE"]))); + + +InstallGlobalFunction(PolymakeDecodeProperty, function(keyword, entry) + local val; + + val := PolymakeDecodeValue(PolymakeParseType(entry._type), entry.data); + + if keyword in POLYMAKING_DEHOMOGENIZE_ROWS then + val := List(val, r -> r{[2..Length(r)]}); + elif keyword in POLYMAKING_DEHOMOGENIZE then + val := val{[2..Length(val)]}; + elif keyword in POLYMAKING_SHIFT_SCALAR then + val := val+1; + fi; + return val; +end); + + +## +## Writing. polymake accepts its own number types as strings, which is the only +## faithful way to hand it a GAP rational: the json package cannot serialize +## rationals, and floats would lose exactness. +## + +InstallGlobalFunction(POLYMAKING_EncodeValue, function(v) + if IsRat(v) then + return String(v); + elif IsBool(v) or IsString(v) then + return v; + elif IsList(v) then + return List(v, POLYMAKING_EncodeValue); + fi; + return v; +end); + + +InstallGlobalFunction(PolymakeEncodeObject, function(type, properties) + local r, name; + r := rec(_ns := rec(polymake := ["https://polymake.org", "4.0"]), + _type := type); + for name in RecNames(properties) do + r.(name) := POLYMAKING_EncodeValue(properties.(name)); + od; + return GapToJsonString(r); +end); diff --git a/lib/pm.pl b/lib/pm.pl new file mode 100644 index 0000000..48d74c2 --- /dev/null +++ b/lib/pm.pl @@ -0,0 +1,67 @@ +# Evaluate polymake properties and write them to a JSON file for polymaking. +# +# usage: pm.pl [OPTIONS] RESULTFILE OBJFILE KEYWORD... +# pm.pl [OPTIONS] RESULTFILE --version +# +# The result is a JSON object with the keys "version", "values" (keyword -> +# serialized value), "errors" (keyword -> message) and, if the object could not +# be loaded at all, "fatal". Writing it to a file rather than to stdout keeps it +# clear of anything polymake prints. + +my ($errfile, $quiet, @prefer); +while (@ARGV && $ARGV[0] =~ /^--/ && $ARGV[0] ne '--version') { + my $opt = shift(@ARGV); + last if $opt eq '--'; + if ($opt eq '--stderr') { $errfile = shift(@ARGV) } + elsif ($opt eq '--quiet') { $quiet = 1 } + elsif ($opt eq '--prefer') { push @prefer, shift(@ARGV) } + else { die "pm.pl: unknown option $opt\n" } +} + +# Reassociating the glob also catches err_print/warn_print, which write to +# $Polymake::console, and polymake's own fatal error handler. +if (defined $errfile) { + open(STDERR, '>', $errfile) or die "cannot redirect stderr to $errfile: $!\n"; + STDERR->autoflush; +} + +# Must happen before load(), which consults Verbose::files. +if ($quiet) { + $Polymake::User::Verbose::credits = 0; + $Polymake::User::Verbose::files = 0; +} + +my $out = shift(@ARGV); +my $file = shift(@ARGV); +my %r = (version => "$Polymake::Version", values => {}, errors => {}); + +if (defined($file) && $file ne '--version') { + my $obj = eval { load($file) }; + if ($@) { + $r{fatal} = "$@"; + } else { + # Not Polymake::User::prefer_now: under --script $Polymake::User::application + # is a stub whose preferences are unset. Mode::create rather than the usual + # Mode::strict, so polymake does not consider its settings changed and + # rewrite them when a config path is in use. + $obj->type->application->prefs + ->add_preference($_, Polymake::Core::Preference::Mode::create) + for @prefer; + + for my $kw (@ARGV) { + my $v = eval { my $x = $obj; $x = $x->$_ for split /\./, $kw; $x }; + if ($@) { + $r{errors}{$kw} = "$@"; + } elsif (!defined($v)) { + $r{errors}{$kw} = "undefined"; + } else { + my $s = eval { Polymake::Core::Serializer::serialize($v) }; + $@ ? ($r{errors}{$kw} = "$@") : ($r{values}{$kw} = $s); + } + } + } +} + +open(my $fh, '>', $out) or die "pm.pl: cannot write $out: $!\n"; +print $fh Polymake::encode_json(\%r); +close($fh); diff --git a/lib/pm_script_arg.pl b/lib/pm_script_arg.pl deleted file mode 100644 index 729e148..0000000 --- a/lib/pm_script_arg.pl +++ /dev/null @@ -1,66 +0,0 @@ -# This is a compatibility hack for polymake 4.1 -# See https://polymake.org/doku.php/user_guide/tutorials/release/4.1/legacy -# Copyright Joachim Zobel . -# Licensed under the same license as GAP polymaking. - -my ($errfile, $quiet, @prefer); -while (@ARGV && $ARGV[0] =~ /^--/) { - my $opt = shift(@ARGV); - last if $opt eq '--'; - if ($opt eq '--stderr') { $errfile = shift(@ARGV) } - elsif ($opt eq '--quiet') { $quiet = 1 } - elsif ($opt eq '--prefer') { push @prefer, shift(@ARGV) } - else { die "pm_script_arg.pl: unknown option $opt\n" } -} - -# Reassociating the glob also catches err_print/warn_print, which write to -# $Polymake::console, and polymake's own fatal error handler. -if (defined $errfile) { - open(STDERR, '>', $errfile) or die "cannot redirect stderr to $errfile: $!\n"; - STDERR->autoflush; -} - -# Must happen before load(), which consults Verbose::files. -if ($quiet) { - $Polymake::User::Verbose::credits = 0; - $Polymake::User::Verbose::files = 0; -} - -my $file = shift(@ARGV); - -my $rtn = 0; -$rtn = 1 if $#ARGV > 1; - -sub give_from { - my ($c, $arg) = @_; - no strict 'refs'; - return $c->$arg; -} - -my $c=load($file); - -# Not Polymake::User::prefer_now: under --script $Polymake::User::application is -# a stub whose preferences are unset, so go through the object's own application. -# Mode::create rather than the usual Mode::strict, so that polymake does not -# consider its settings changed and rewrite them when a config path is in use. -$c->type->application->prefs->add_preference($_, Polymake::Core::Preference::Mode::create) - for @prefer; -my @rtn = (); -foreach my $arg (@ARGV) { - my @sargs = split(/\b\s+\b/, $arg); - $rtn = 1 if $#sargs > 1; - foreach my $sarg (@sargs) { - my @ssargs = split('->', $sarg); - my $given = $c; - # We follow the arrows - foreach my $ssarg (@ssargs) { - $given = give_from($given, $ssarg); - } - # and return what the last one gave us - push(@rtn, "$sarg\n$given\n"); - } -} -print join("\n", @rtn); - -return $rtn; - diff --git a/lib/userpref.gi b/lib/userpref.gi index d4b0dac..bd5046c 100644 --- a/lib/userpref.gi +++ b/lib/userpref.gi @@ -10,7 +10,9 @@ # Temporary directories are created on demand and re-created whenever they have # vanished, e.g. after restoring a workspace saved in an earlier session. -BindGlobal("POLYMAKING_STATE", rec(tmpdir := fail, scratch := fail)); +BindGlobal("POLYMAKING_STATE", + rec(tmpdir := fail, scratch := fail, + version := fail, versionChecked := false)); BindGlobal("POLYMAKING_TempDirectory", function(key) diff --git a/lib/workaround_maps.gd b/lib/workaround_maps.gd deleted file mode 100644 index 5b69336..0000000 --- a/lib/workaround_maps.gd +++ /dev/null @@ -1,26 +0,0 @@ -############################################################################# -## -#W workaround_maps.gd polymaking Package Marc Roeder -## -## - -## -## -#Y Copyright (C) 2006 Marc Roeder -#Y -#Y This program is free software; you can redistribute it and/or -#Y modify it under the terms of the GNU General Public License -#Y as published by the Free Software Foundation; either version 2 -#Y of the License, or (at your option) any later version. -#Y -#Y This program is distributed in the hope that it will be useful, -#Y but WITHOUT ANY WARRANTY; without even the implied warranty of -#Y MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -#Y GNU General Public License for more details. -#Y -#Y You should have received a copy of the GNU General Public License -#Y along with this program; if not, write to the Free Software -#Y Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA -## -DeclareOperation("MapKeyWordToPolymakeFormat",[IsString]); -DeclareOperation("MapKeyWordFromPolymakeFormat",[IsString]); \ No newline at end of file diff --git a/lib/workaround_maps.gi b/lib/workaround_maps.gi deleted file mode 100644 index b898eca..0000000 --- a/lib/workaround_maps.gi +++ /dev/null @@ -1,68 +0,0 @@ -############################################################################# -## -#W workaround_maps.gi polymaking Package Marc Roeder -## -## - -## -## -#Y Copyright (C) 2006 Marc Roeder -#Y -#Y This program is free software; you can redistribute it and/or -#Y modify it under the terms of the GNU General Public License -#Y as published by the Free Software Foundation; either version 2 -#Y of the License, or (at your option) any later version. -#Y -#Y This program is distributed in the hope that it will be useful, -#Y but WITHOUT ANY WARRANTY; without even the implied warranty of -#Y MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -#Y GNU General Public License for more details. -#Y -#Y You should have received a copy of the GNU General Public License -#Y along with this program; if not, write to the Free Software -#Y Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA -## - -########### -# This is a workaround for the next-generation polymake -# there we need to call keywords of the form "HASSE_DIAGRAM->FACES" -# and that does not work as an entry of a record. So we have do do -# some extra mapping (yes, it is a hack). -## -# direction of the mapping: GAP keyword mapped to polymake keyword -####### -InstallMethod(MapKeyWordToPolymakeFormat,"for PolymakeObject",[IsString], - function(option) - if option = "FACES" then - return "HASSE_DIAGRAM->FACES"; - elif option = "DIMS" then - return "HASSE_DIAGRAM->DIMS"; - elif option = "ADJACENCY" then - return "HASSE_DIAGRAM->ADJACENCY"; - else - return option; - fi; -end); - -########## -# This is a workaround for the next-generation polymake -# there we need to call keywords of the form "HASSE_DIAGRAM->FACES" -# and that does not work as an entry of a record. So we have do do -# some extra mapping (yes, it is a hack). -## -# direction: Polymake output mapped to GAP keywords -####### - -InstallMethod(MapKeyWordFromPolymakeFormat,"for PolymakeObject",[IsString], - function(option) - - if option = "HASSE_DIAGRAM->FACES" then - return "FACES"; - elif option = "HASSE_DIAGRAM->DIMS" then - return "DIMS"; - elif option = "HASSE_DIAGRAM->ADJACENCY" then - return "ADJACENCY"; - else - return option; - fi; -end); diff --git a/read.g b/read.g index a6f1259..0ff44fb 100644 --- a/read.g +++ b/read.g @@ -2,8 +2,6 @@ ReadPackage("polymaking","lib/userpref.gi"); POLYMAKING_WarnAboutObsoleteGlobals(); ReadPackage("polymaking","lib/environment.gi"); ReadPackage("polymaking","lib/Objects.gi"); +ReadPackage("polymaking","lib/json.gi"); ReadPackage("polymaking","lib/construct.gi"); -ReadPackage("polymaking","lib/workaround_maps.gi"); -ReadPackage("polymaking","lib/convert.gi"); -ReadPackage("polymaking","lib/ObjectConverters.gi"); ReadPackage("polymaking","lib/application_version_type.gi"); diff --git a/tst/example.tst b/tst/example.tst index c2156da..ab11b54 100644 --- a/tst/example.tst +++ b/tst/example.tst @@ -14,7 +14,7 @@ gap> Polymake(permutahedron,"N_VERTICES"); 6 gap> PropertyOfPolymakeObject(permutahedron,"VERTICES"); fail -gap> NamesKnownPropertiesOfPolymakeObject(permutahedron); +gap> Set(NamesKnownPropertiesOfPolymakeObject(permutahedron)); [ "N_VERTICES", "VOLUME" ] gap> Polymake(permutahedron,"DIM"); 2 diff --git a/tst/json.tst b/tst/json.tst new file mode 100644 index 0000000..794a830 --- /dev/null +++ b/tst/json.tst @@ -0,0 +1,67 @@ +gap> START_TEST("json.tst"); + +# the type drives the decoding, so check the shapes polymake actually emits +gap> t := PolymakeParseType("common::Array>");; +gap> [t.name, t.params[1].name, t.params[1].params[1].name]; +[ "Array", "Set", "Int" ] +gap> PolymakeParseType("common::NodeMap>").params[2].name; +"Set" +gap> PolymakeParseType(fail).name; +"" + +# polymake writes its own number types as strings, plain perl values natively +gap> PolymakeDecodeProperty("VOLUME", rec(_type := "common::Rational", data := "1/4")); +1/4 +gap> PolymakeDecodeProperty("ALTSHULER_DET", rec(_type := "common::Integer", +> data := "123456789012345678901234567890")); +123456789012345678901234567890 +gap> PolymakeDecodeProperty("BOUNDED", rec(_type := fail, data := true)); +true +gap> PolymakeDecodeProperty("N_VERTICES", rec(_type := fail, data := 8)); +8 + +# indices are 0-based in polymake and 1-based in GAP +gap> PolymakeDecodeProperty("VERTICES_IN_FACETS", +> rec(_type := "common::IncidenceMatrix", +> data := [[0,2,4],[1,3,5],rec(cols := 6)])); +[ [ 1, 3, 5 ], [ 2, 4, 6 ] ] +gap> PolymakeDecodeProperty("HASSE_DIAGRAM.FACES", +> rec(_type := "common::NodeMap>", data := [[],[0],[0,1]])); +[ [ ], [ 1 ], [ 1, 2 ] ] +gap> PolymakeDecodeProperty("TOP_NODE", rec(_type := fail, data := 7)); +8 + +# ... but plain integer arrays are values, not indices +gap> PolymakeDecodeProperty("FACET_DEGREES", +> rec(_type := "common::Array", data := [3,3,4])); +[ 3, 3, 4 ] + +# sparse vectors and matrices +gap> PolymakeDecodeProperty("V", rec(_type := "common::SparseVector", +> data := rec(("3") := "1/2", _dim := 5))); +[ 0, 0, 0, 1/2, 0 ] +gap> PolymakeDecodeProperty("M", rec(_type := "common::SparseMatrix", +> data := [rec(("0") := "1", ("1") := "1"), rec(("3") := "-1"), rec(cols := 4)])); +[ [ 1, 1, 0, 0 ], [ 0, 0, 0, -1 ] ] + +# homogeneous coordinates are stripped +gap> PolymakeDecodeProperty("VERTICES", rec(_type := "common::Matrix", +> data := [["1","1/4","0"],["1","0","1/5"]])); +[ [ 1/4, 0 ], [ 0, 1/5 ] ] +gap> PolymakeDecodeProperty("REL_INT_POINT", rec(_type := "common::Vector", +> data := ["1","1/3","1/7"])); +[ 1/3, 1/7 ] + +# ... but FACETS are not points, so they keep every coordinate +gap> PolymakeDecodeProperty("FACETS", rec(_type := "common::Matrix", +> data := [["1","-1","0"]])); +[ [ 1, -1, 0 ] ] + +# writing: rationals become strings, which is how polymake spells them +gap> j := JsonStringToGap( +> PolymakeEncodeObject("polytope::Polytope", rec(POINTS := [[1,1/4]])));; +gap> [ j._type, j.POINTS, j._ns.polymake[2] ]; +[ "polytope::Polytope", [ [ "1", "1/4" ] ], "4.0" ] + +# +gap> STOP_TEST("json.tst", 1); diff --git a/tst/polymaking.tst b/tst/polymaking.tst index dafdbdd..4cff5f0 100644 --- a/tst/polymaking.tst +++ b/tst/polymaking.tst @@ -134,9 +134,9 @@ gap> Polymake(poly,"NEIGHBORLINESS"); 1 gap> Polymake(poly,"NEIGHBORLY"); true -gap> Polymake(poly,"MINIMAL_VERTEX_ANGLE"); -#I Warning!converting a floating point number -314159265358979/100000000000000 +gap> angle := Polymake(poly,"MINIMAL_VERTEX_ANGLE");; +gap> IsFloat(angle) and AbsoluteValue(angle - 3.14159265358979) < 1.e-10; +true gap> Polymake(poly,"POINTED"); true gap> Polymake(poly,"POSITIVE"); @@ -180,6 +180,30 @@ gap> Polymake(plane, "FACETS"); [ [ 1, 2, 5 ], [ 1, 2, 6 ], [ 1, 3, 4 ], [ 1, 3, 5 ], [ 1, 4, 6 ], [ 2, 3, 4 ], [ 2, 3, 6 ], [ 2, 4, 5 ], [ 3, 5, 6 ], [ 4, 5, 6 ] ] +## polymake 4 spells nested properties with a dot, and polymaking no longer has +## to rewrite the keyword to reach them +## +gap> Polymake(poly,"HASSE_DIAGRAM.FACES") = faces; +true + +## GRAPH comes back as documented; it used to raise an error +## +gap> g := Polymake(poly,"GRAPH");; +gap> Set(RecNames(g)); +[ "edges", "vertices" ] +gap> g.vertices = [1..9]; +true +gap> g.edges = Filtered(CanonicalFaceList(faces), f -> Size(f) = 2); +true + +## polymaking writes polymake's own format, so nothing needs converting +## +gap> j := JsonStringToGap(StringFile(FullFilenameOfPolymakeObject(poly)));; +gap> j._type; +"polytope::Polytope" +gap> j.POINTS[1]; +[ "1", "1/4", "1/75", "1/22" ] + # gap> SetUserPreference("polymaking", "PolymakeDataDirectory", olddatadir);; gap> STOP_TEST("polymaking.tst", 10000); From c879ecdb38b9deae7609a3d15f41c71c961885b4 Mon Sep 17 00:00:00 2001 From: Max Horn Date: Sun, 16 Aug 2026 22:41:04 +0200 Subject: [PATCH 2/6] Require GAP 4.12, as the json package does Also avoid the two argument form of Set, declared only in GAP 4.11, in the decoder; the version bump makes it available, but there is no reason to depend on it. Co-Authored-By: Claude Opus 5 --- CHANGES.md | 2 +- PackageInfo.g | 2 +- README.md | 2 +- lib/json.gi | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a2332a2..d80ae77 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,7 @@ 0.9.0 (unreleased) - polymake 4.0 or newer is now required, and the GAP package json is a new - dependency. polymaking now writes and reads polymake's own JSON data format + dependency; json needs GAP 4.12, so that is now polymaking's minimum too. polymaking now writes and reads polymake's own JSON data format instead of the pre-4 plain format, which means polymake no longer converts the files and no longer says so (issue #22) - nested polymake properties can be named directly, e.g. diff --git a/PackageInfo.g b/PackageInfo.g index 1fc6f42..3317660 100644 --- a/PackageInfo.g +++ b/PackageInfo.g @@ -50,7 +50,7 @@ PackageDoc := rec( ), Dependencies := rec( - GAP := ">=4.8", + GAP := ">= 4.12", # this is what the json package requires NeededOtherPackages := [ [ "json", ">= 2.0.0" ] ], SuggestedOtherPackages := [], NeededSystemPackages := rec( Ubuntu := [["polymake"]] ), diff --git a/README.md b/README.md index 060bb29..3aa5631 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ National University of Ireland, Galway Requirements ------------ -polymaking requires GAP version 4.8, the GAP package json, and polymake 4.0 +polymaking requires GAP version 4.12, the GAP package json, and polymake 4.0 or newer. The GAPDoc package is needed to display the documentation. Use polymaking 0.8.9 if you need to work with polymake 3 or older. diff --git a/lib/json.gi b/lib/json.gi index 7e86eed..8a976d1 100644 --- a/lib/json.gi +++ b/lib/json.gi @@ -60,7 +60,7 @@ BindGlobal("POLYMAKING_Scalar", function(x) end); -BindGlobal("POLYMAKING_Indices", l -> Set(l, i -> i+1)); +BindGlobal("POLYMAKING_Indices", l -> Set(List(l, i -> i+1))); # a dense list, or a sparse record {"3": v, "_dim": n} @@ -145,7 +145,7 @@ InstallGlobalFunction(PolymakeDecodeValue, function(type, data) if not IsEmpty(type.params) and type.params[1].name in ["Int"] then return POLYMAKING_Indices(data); fi; - return Set(data, POLYMAKING_Scalar); + return Set(List(data, POLYMAKING_Scalar)); elif name = "Array" then if IsEmpty(type.params) then From 3cbea083130a25e1fbc728cb289c6d3e4376a11e Mon Sep 17 00:00:00 2001 From: Max Horn Date: Sun, 16 Aug 2026 22:47:13 +0200 Subject: [PATCH 3/6] Test that polymake before 4.0 is refused The version gate is a hard error on a path users can hit, but nothing exercised it. Drive it through POLYMAKING_STATE rather than a stub binary, so the test needs no subprocess. Co-Authored-By: Claude Opus 5 --- tst/json.tst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tst/json.tst b/tst/json.tst index 794a830..547e257 100644 --- a/tst/json.tst +++ b/tst/json.tst @@ -63,5 +63,19 @@ gap> j := JsonStringToGap( gap> [ j._type, j.POINTS, j._ns.polymake[2] ]; [ "polytope::Polytope", [ [ "1", "1/4" ] ], "4.0" ] +# polymake before 4.0 cannot read the files polymaking writes, so it is refused +gap> oldver := POLYMAKING_STATE.version;; +gap> POLYMAKING_STATE.version := "3.6";; +gap> POLYMAKING_STATE.versionChecked := false;; +gap> CALL_WITH_CATCH(POLYMAKING_CheckVersion, [])[1]; +Error, polymaking requires polymake 4.0 or newer, but found 3.6. Use polymakin\ +g 0.8.9 with older versions of polymake. +false +gap> POLYMAKING_STATE.version := "4.0";; +gap> POLYMAKING_CheckVersion(); +gap> POLYMAKING_STATE.versionChecked; +true +gap> POLYMAKING_STATE.version := oldver;; + # gap> STOP_TEST("json.tst", 1); From 038f2595ab6526aa9a13a6f539c8caada867f1f4 Mon Sep 17 00:00:00 2001 From: Max Horn Date: Sun, 16 Aug 2026 22:52:37 +0200 Subject: [PATCH 4/6] Test the sparse matrix shapes polymake actually emits A SparseMatrix may mix sparse rows with dense ones, and an all-zero row serializes as an empty record; both are easy to get wrong. The shapes here were taken from polymake 4.15 output rather than invented. Co-Authored-By: Claude Opus 5 --- tst/json.tst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tst/json.tst b/tst/json.tst index 547e257..dda2bdb 100644 --- a/tst/json.tst +++ b/tst/json.tst @@ -44,6 +44,18 @@ gap> PolymakeDecodeProperty("M", rec(_type := "common::SparseMatrix data := [rec(("0") := "1", ("1") := "1"), rec(("3") := "-1"), rec(cols := 4)])); [ [ 1, 1, 0, 0 ], [ 0, 0, 0, -1 ] ] +# a SparseMatrix may mix sparse rows with dense ones, and an all-zero row +# serializes as an empty record +gap> PolymakeDecodeProperty("M", rec(_type := "common::SparseMatrix", +> data := [["5"], rec(cols := 1)])); +[ [ 5 ] ] +gap> PolymakeDecodeProperty("M", rec(_type := "common::SparseMatrix", +> data := [rec(), rec(), rec(cols := 2)])); +[ [ 0, 0 ], [ 0, 0 ] ] +gap> PolymakeDecodeProperty("M", rec(_type := "common::SparseMatrix", +> data := [rec(cols := 3)])); +[ ] + # homogeneous coordinates are stripped gap> PolymakeDecodeProperty("VERTICES", rec(_type := "common::Matrix", > data := [["1","1/4","0"],["1","0","1/5"]])); From 4338f18ddf0bfe118402cbd3692dba50886bf119 Mon Sep 17 00:00:00 2001 From: Max Horn Date: Mon, 17 Aug 2026 00:06:05 +0200 Subject: [PATCH 5/6] Do not refer to the old file format in the manual The remark only made sense to readers who knew what polymaking used to write. Co-Authored-By: Claude Opus 5 --- doc/input.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/input.xml b/doc/input.xml index 3b48d54..1c36997 100644 --- a/doc/input.xml +++ b/doc/input.xml @@ -7,10 +7,10 @@ read back and stored in the PolymakeObject corresponding to the file for which polymake was called. - The files are written in polymake's own JSON format, so polymake never has - to convert them. Writing to a PolymakeObject rewrites the whole file - from the properties given so far, which means a file may be extended at any - point, also after polymake has been called for it. + The files are written in polymake's own JSON format. Writing to a + PolymakeObject rewrites the whole file from the properties given so + far, which means a file may be extended at any point, also after polymake + has been called for it. From 702efbb34714d68232e6dba9256ee2358798c2af Mon Sep 17 00:00:00 2001 From: Max Horn Date: Mon, 17 Aug 2026 09:34:17 +0200 Subject: [PATCH 6/6] Keep the polymaking 0.8 interface working hap and hapcryst are needed-dependency consumers of polymaking, and neither pins an upper bound, so 0.9.0 will be paired with existing releases of both. Measured against them, the rewrite broke hapcryst's test suite entirely (9 failures, 0 before) and hap's IsAspherical. Three things they rely on come back: - POLYMAKE_COMMAND and POLYMAKE_DATA_DIR are set again, for code that reads them. Whether the *user* bound them before loading is recorded once, in POLYMAKING_LEGACY_SET, so our own values are not mistaken for theirs and the preferences stay the source of truth. A post restore hook refreshes them, so unlike in 0.8.9 they do not go stale with a saved workspace. - ConvertMatrixToPolymakeString and the two argument AppendToPolymakeObject work together as before. Nothing appends verbatim to a JSON file, so the former now passes the section along as a record rather than as a string; code composing the two, which is how both packages use them, is unaffected. Appending a bare string reports what to use instead. - The polymake 2.3 era type names that CheckAppVerTypList accepts are mapped to their polymake 4 spellings. hapcryst asks for RationalPolytope, which polymake 4 knows as Polytope and would otherwise reject. The deprecation notices for the second group are issued at InfoObsolete level 2 rather than 1: warning by default would change the output of the packages this is meant to keep working, and so break their tests. With this, hapcryst is back to 0 failures and hap's IsAspherical answers again, both unmodified. Co-Authored-By: Claude Opus 5 --- CHANGES.md | 10 +++++++--- doc/internals.xml | 7 ++++--- lib/Objects.gi | 20 ++++++++++++++++++-- lib/construct.gd | 7 +++++++ lib/construct.gi | 32 ++++++++++++++++++++++++++++++++ lib/environment.gd | 1 + lib/environment.gi | 39 +++++++++++++++++++++++++++++++++++++-- lib/userpref.gi | 27 +++++++++++++++++++++------ tst/userprefs.tst | 38 ++++++++++++++++++++++++++++---------- 9 files changed, 155 insertions(+), 26 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d80ae77..1cc3d40 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,7 +17,10 @@ correctly too. `ObjectConverters` and the `ConvertPolymake...` functions are gone, as is `ConvertMatrixToPolymakeString` - `AppendToPolymakeObject(poly, name, value)` now takes a GAP value; it used to - take a string to append to the file verbatim + take a string to append to the file verbatim. The two argument form and + `ConvertMatrixToPolymakeString` still work together as before and are + deprecated: the latter now hands the section to the former as a record rather + than as a string, so code composing the two is unaffected - polymaking will not write to a file it did not create - polymaking is now configured via the GAP user preferences `PolymakeCommand` @@ -42,8 +45,9 @@ and no longer rewrites `tst/pplane.poly` in place while running (issue #18). Set `POLYMAKING_CHULL` to test a specific backend, e.g. `POLYMAKING_CHULL=cdd gap tst/testall.g`. -- the globals `POLYMAKE_COMMAND` and `POLYMAKE_DATA_DIR` are no longer set by - the package; if you set them yourself they are still honoured +- the globals `POLYMAKE_COMMAND` and `POLYMAKE_DATA_DIR` are deprecated. They + are still set, and still honoured if you set them before loading polymaking, + but the preferences above are the ones to use ------------------ 0.8.9 (2026-04-08) diff --git a/doc/internals.xml b/doc/internals.xml index d740470..7f96cee 100644 --- a/doc/internals.xml +++ b/doc/internals.xml @@ -27,9 +27,10 @@ output Up to version 0.8.9 the polymake command and the directory for polymake files were held in the global variables POLYMAKE&uscore;COMMAND and - POLYMAKE&uscore;DATA&uscore;DIR. These are deprecated; they are still - honoured if you bind them yourself, but the package no longer sets them and - warns at load time if it finds them. Use the user preferences + POLYMAKE&uscore;DATA&uscore;DIR. These are deprecated. They are still + provided, and still honoured if you bind them before loading + polymaking, in which case a warning is issued at load + time. Use the user preferences PolymakeCommand and PolymakeDataDirectory instead, see , and and to query the effective values. diff --git a/lib/Objects.gi b/lib/Objects.gi index ee68f53..b6fe9a4 100644 --- a/lib/Objects.gi +++ b/lib/Objects.gi @@ -169,9 +169,20 @@ InstallMethod(ClearPolymakeObject, InitPolymakeObject(poly); end); +# The polymake 2.3 era type names the pre-0.9 interface accepted, and what they +# are called in polymake 4. +BindGlobal("POLYMAKING_LEGACY_TYPES", MakeImmutable(rec( + Polytope := "polytope::Polytope", + RationalPolytope := "polytope::Polytope", + FloatPolytope := "polytope::Polytope", + SchlegelDiagram := "polytope::SchlegelDiagram", + VoronoiDiagram := "polytope::VoronoiPolyhedron", + PropagatedPolytope := "polytope::PropagatedPolytope", + SimplicialComplex := "topaz::SimplicialComplex" ))); + # clear known data, then set the polymake type. The three element form takes the # [application, version, type] list the pre-0.9 interface used; polymake 4 has -# no use for the version, and wants the type qualified by the application. +# no use for the version, and names several of the types differently. InstallMethod(ClearPolymakeObject, [IsPolymakeObject,IsDenseList], function(poly,appvertyp) @@ -179,7 +190,12 @@ InstallMethod(ClearPolymakeObject, if IsString(appvertyp) then type:=appvertyp; elif CheckAppVerTypList(appvertyp) then - type:=Concatenation(appvertyp[1],"::",appvertyp[3]); + type:=NormalizedWhitespace(appvertyp[3]); + if IsBound(POLYMAKING_LEGACY_TYPES.(type)) then + type:=POLYMAKING_LEGACY_TYPES.(type); + else + type:=Concatenation(NormalizedWhitespace(appvertyp[1]),"::",type); + fi; else Error("application, version, type not well-formed"); fi; diff --git a/lib/construct.gd b/lib/construct.gd index e5e83fe..c860932 100644 --- a/lib/construct.gd +++ b/lib/construct.gd @@ -38,6 +38,13 @@ DeclareOperation("CreatePolymakeObject",[IsString,IsDirectory,IsDenseList]); DeclareOperation("AppendToPolymakeObject",[IsPolymakeObject,IsString,IsObject]); + +## +## deprecated, for code written against polymaking 0.8 +## +DeclareOperation("ConvertMatrixToPolymakeString",[IsString,IsDenseList]); +DeclareOperation("AppendToPolymakeObject",[IsPolymakeObject,IsRecord]); +DeclareOperation("AppendToPolymakeObject",[IsPolymakeObject,IsString]); DeclareGlobalFunction("POLYMAKING_WriteObject"); DeclareOperation("AppendPointlistToPolymakeObject",[IsPolymakeObject,IsDenseList]); DeclareOperation("AppendVertexlistToPolymakeObject",[IsPolymakeObject,IsDenseList]); diff --git a/lib/construct.gi b/lib/construct.gi index 2a687c5..e6a2009 100644 --- a/lib/construct.gi +++ b/lib/construct.gi @@ -163,6 +163,38 @@ BindGlobal("POLYMAKING_Homogenize", matrix -> List(matrix, p -> Concatenation([1],p))); +## +## polymaking 0.8 built a plain format section as a string and appended it +## verbatim. There is no verbatim appending to a JSON file, so the pair now +## passes the section along as a record instead; composed as before, the two +## still do what they always did. +## +InstallMethod(ConvertMatrixToPolymakeString,[IsString,IsDenseList], + function(name,matrix) + POLYMAKING_InfoDeprecatedAt(2, "ConvertMatrixToPolymakeString", + "AppendToPolymakeObject(poly, name, matrix)"); + POLYMAKING_CheckMatrix(matrix); + return rec(polymakeSection:=name, polymakeData:=matrix); +end); + + +InstallMethod(AppendToPolymakeObject,[IsPolymakeObject,IsRecord], + function(poly,section) + if not (IsBound(section.polymakeSection) and IsBound(section.polymakeData)) + then + ErrorNoReturn("
must come from ConvertMatrixToPolymakeString"); + fi; + AppendToPolymakeObject(poly,section.polymakeSection,section.polymakeData); +end); + + +InstallMethod(AppendToPolymakeObject,[IsPolymakeObject,IsString], + function(poly,string) + ErrorNoReturn("cannot append a string, use ", + "AppendToPolymakeObject(poly, name, value)"); +end); + + InstallMethod(AppendPointlistToPolymakeObject,[IsPolymakeObject,IsDenseList], function(polygon,pointlist) POLYMAKING_CheckMatrix(pointlist); diff --git a/lib/environment.gd b/lib/environment.gd index f3aa10f..5b82837 100644 --- a/lib/environment.gd +++ b/lib/environment.gd @@ -32,6 +32,7 @@ DeclareGlobalFunction("PolymakeDataDirectory"); DeclareGlobalFunction("PolymakeVersion"); DeclareGlobalFunction("POLYMAKING_Run"); DeclareGlobalFunction("POLYMAKING_CheckVersion"); +DeclareGlobalFunction("POLYMAKING_UpdateLegacyGlobals"); ## ## deprecated in favour of the user preferences PolymakeCommand and diff --git a/lib/environment.gi b/lib/environment.gi index 2ae48b7..132f30d 100644 --- a/lib/environment.gi +++ b/lib/environment.gi @@ -24,11 +24,18 @@ ## SetInfoLevel(InfoPolymaking,1); -BindGlobal("POLYMAKING_InfoDeprecated", function(name, replacement) - Info(InfoObsolete, 1, "`", name, "` is deprecated, use ", replacement, +# level 2 for shims that exist purely so that code written against polymaking +# 0.8 keeps running unchanged: warning by default would make their output, and +# so their test suites, differ. +BindGlobal("POLYMAKING_InfoDeprecatedAt", function(level, name, replacement) + Info(InfoObsolete, level, "`", name, "` is deprecated, use ", replacement, " instead."); end); +BindGlobal("POLYMAKING_InfoDeprecated", function(name, replacement) + POLYMAKING_InfoDeprecatedAt(1, name, replacement); +end); + InstallMethod(SetPolymakeCommand,[IsString], function(command) @@ -133,6 +140,34 @@ InstallGlobalFunction(POLYMAKING_CheckVersion, function() end); +## +## Keep POLYMAKE_COMMAND and POLYMAKE_DATA_DIR in existence for packages that +## still read them, hap and hapcryst among them. They are plain assignments +## rather than BindGlobal, so that the post restore hook can refresh them. +## +BindGlobal("POLYMAKING_Rebind", function(name, value) + if IsBoundGlobal(name) then + MakeReadWriteGlobal(name); + UnbindGlobal(name); + fi; + BindGlobal(name, value); +end); + +InstallGlobalFunction(POLYMAKING_UpdateLegacyGlobals, function() + if not POLYMAKING_LEGACY_SET.command then + POLYMAKING_Rebind("POLYMAKE_COMMAND", PolymakeCommand()); + fi; + if not POLYMAKING_LEGACY_SET.dataDir then + POLYMAKING_Rebind("POLYMAKE_DATA_DIR", PolymakeDataDirectory()); + fi; +end); + +POLYMAKING_UpdateLegacyGlobals(); + +# the data directory a restored workspace names is gone, see issue #17 +CallAndInstallPostRestore(POLYMAKING_UpdateLegacyGlobals); + + if PolymakeCommand() = fail then Info(InfoWarning, 1, "polymake command not found; set it via ", "SetUserPreference(\"polymaking\", \"PolymakeCommand\", )"); diff --git a/lib/userpref.gi b/lib/userpref.gi index bd5046c..15f24f0 100644 --- a/lib/userpref.gi +++ b/lib/userpref.gi @@ -8,6 +8,15 @@ #Y of the License, or (at your option) any later version. ## +# Whether the user bound the obsolete globals before loading us. Only then are +# they a configuration source; we bind them ourselves further down, for the +# benefit of packages that still read them, and must not mistake our own value +# for the user's. +BindGlobal("POLYMAKING_LEGACY_SET", + MakeImmutable(rec(command := IsBoundGlobal("POLYMAKE_COMMAND"), + dataDir := IsBoundGlobal("POLYMAKE_DATA_DIR")))); + + # Temporary directories are created on demand and re-created whenever they have # vanished, e.g. after restoring a workspace saved in an earlier session. BindGlobal("POLYMAKING_STATE", @@ -65,13 +74,19 @@ end); BindGlobal("POLYMAKING_WarnAboutObsoleteGlobals", function() local obsolete; - obsolete := Filtered(["POLYMAKE_COMMAND", "POLYMAKE_DATA_DIR"], IsBoundGlobal); + obsolete := []; + if POLYMAKING_LEGACY_SET.command then + Add(obsolete, "POLYMAKE_COMMAND"); + fi; + if POLYMAKING_LEGACY_SET.dataDir then + Add(obsolete, "POLYMAKE_DATA_DIR"); + fi; if not IsEmpty(obsolete) then Info(InfoWarning, 1, - "polymaking no longer sets the global variables ", + "polymaking honours the deprecated global variable(s) ", JoinStringsWithSeparator(obsolete, ", "), - "; they are still honoured but deprecated. Use ", - "SetUserPreference(\"polymaking\", ...) instead, see the manual."); + ", but please use SetUserPreference(\"polymaking\", ...) ", + "instead, see the manual."); fi; return obsolete; end); @@ -83,7 +98,7 @@ InstallGlobalFunction(PolymakeCommand, function() if IsString(pref) and pref <> "" then return POLYMAKING_ResolveCommand(pref); fi; - if IsBoundGlobal("POLYMAKE_COMMAND") then + if POLYMAKING_LEGACY_SET.command then cmd := POLYMAKING_ResolveCommand(VALUE_GLOBAL("POLYMAKE_COMMAND")); if cmd <> fail then return cmd; @@ -99,7 +114,7 @@ InstallGlobalFunction(PolymakeDataDirectory, function() if IsString(pref) and pref <> "" then return POLYMAKING_EnsureDirectory(pref); fi; - if IsBoundGlobal("POLYMAKE_DATA_DIR") then + if POLYMAKING_LEGACY_SET.dataDir then dir := VALUE_GLOBAL("POLYMAKE_DATA_DIR"); if IsDirectory(dir) then return POLYMAKING_EnsureDirectory(Filename(dir, "")); diff --git a/tst/userprefs.tst b/tst/userprefs.tst index 5b7929b..fab8d01 100644 --- a/tst/userprefs.tst +++ b/tst/userprefs.tst @@ -28,18 +28,20 @@ true gap> d1 = d2; false -# the obsolete globals are still honoured as a fallback -gap> POLYMAKE_DATA_DIR := tmp;; -gap> Filename(PolymakeDataDirectory(), "") = Filename(tmp, ""); -true -gap> oldwarn := InfoLevel(InfoWarning);; -gap> SetInfoLevel(InfoWarning, 0);; -gap> POLYMAKING_WarnAboutObsoleteGlobals(); -[ "POLYMAKE_DATA_DIR" ] -gap> Unbind(POLYMAKE_DATA_DIR); +# The obsolete globals are still provided, for packages that read them, and +# track the accessors. Whether the user set them before loading is decided once +# at load time, so it cannot be simulated here; POLYMAKING_LEGACY_SET says no. +gap> POLYMAKING_LEGACY_SET.command or POLYMAKING_LEGACY_SET.dataDir; +false gap> POLYMAKING_WarnAboutObsoleteGlobals(); [ ] -gap> SetInfoLevel(InfoWarning, oldwarn);; +gap> POLYMAKE_COMMAND = PolymakeCommand(); +true +gap> SetUserPreference("polymaking", "PolymakeDataDirectory", sub);; +gap> POLYMAKING_UpdateLegacyGlobals(); +gap> Filename(POLYMAKE_DATA_DIR, "") = Filename(PolymakeDataDirectory(), ""); +true +gap> SetUserPreference("polymaking", "PolymakeDataDirectory", "");; # the deprecated setters forward to the preferences gap> oldinfo := InfoLevel(InfoObsolete);; @@ -70,6 +72,22 @@ true gap> ForAll(UserPreference("polymaking", "PolymakePreferences"), IsString); true +# hap and hapcryst compose these two, so the pair has to keep working +gap> poly := CreatePolymakeObject("compat", PolymakeDataDirectory(), +> ["polytope", "2.3", "RationalPolytope"]);; +gap> AppendToPolymakeObject(poly, +> ConvertMatrixToPolymakeString("POINTS", [[1,0,0],[1,1,0],[1,0,1]])); +gap> j := JsonStringToGap(StringFile(FullFilenameOfPolymakeObject(poly)));; +gap> j._type; +"polytope::Polytope" +gap> j.POINTS; +[ [ "1", "0", "0" ], [ "1", "1", "0" ], [ "1", "0", "1" ] ] + +# but a bare string cannot be appended to a JSON file +gap> CALL_WITH_CATCH(AppendToPolymakeObject, [poly, "POINTS\n1 0 0\n"])[1]; +Error, cannot append a string, use AppendToPolymakeObject(poly, name, value) +false + # gap> SetUserPreference("polymaking", "PolymakeCommand", oldcmd);; gap> SetUserPreference("polymaking", "PolymakeDataDirectory", olddir);;