diff --git a/Project.toml b/Project.toml index b0f141e..4471dde 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "XPORTA" uuid = "8c143463-af6f-456f-8aed-72447cb569d2" authors = ["Brian Doolittle"] -version = "0.1.0" +version = "0.1.1" [deps] PORTA_jll = "c3fa2e09-48e0-5371-872a-ed3ac32dd1fc" diff --git a/README.md b/README.md index fa7b339..2e09f72 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,13 @@ | Documentation | Test Coverage | Linux/Mac | Windows | FreeBSD | |:-------------:|:-------------:|:---------:|:-------:|:-------:| -|[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://JuliaPolyhedra.github.io/XPORTA.jl/dev) | [![Coverage Status](https://coveralls.io/repos/github/JuliaPolyhedra/XPORTA.jl/badge.svg?branch=master)](https://coveralls.io/github/JuliaPolyhedra/XPORTA.jl?branch=master)[![codecov](https://codecov.io/gh/JuliaPolyhedra/XPORTA.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/JuliaPolyhedra/XPORTA.jl) | [![Linux/Mac Build Status](https://travis-ci.org/JuliaPolyhedra/XPORTA.jl.svg?branch=master)](https://travis-ci.org/github/JuliaPolyhedra/XPORTA.jl) | [![Windows Build status](https://ci.appveyor.com/api/projects/status/2kjsbavtulwhsamu?svg=true)](https://ci.appveyor.com/project/bdoolittle/xporta-jl) | [![FreeBSD Build Status](https://api.cirrus-ci.com/github/JuliaPolyhedra/XPORTA.jl.svg)](https://cirrus-ci.com/github/JuliaPolyhedra/XPORTA.jl) | +|[![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://JuliaPolyhedra.github.io/XPORTA.jl/stable) [![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://JuliaPolyhedra.github.io/XPORTA.jl/dev) | [![Coverage Status](https://coveralls.io/repos/github/JuliaPolyhedra/XPORTA.jl/badge.svg?branch=master)](https://coveralls.io/github/JuliaPolyhedra/XPORTA.jl?branch=master)[![codecov](https://codecov.io/gh/JuliaPolyhedra/XPORTA.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/JuliaPolyhedra/XPORTA.jl) | [![Linux/Mac Build Status](https://travis-ci.org/JuliaPolyhedra/XPORTA.jl.svg?branch=master)](https://travis-ci.org/github/JuliaPolyhedra/XPORTA.jl) | [![Windows Build status](https://ci.appveyor.com/api/projects/status/2kjsbavtulwhsamu?svg=true)](https://ci.appveyor.com/project/bdoolittle/xporta-jl) | [![FreeBSD Build Status](https://api.cirrus-ci.com/github/JuliaPolyhedra/XPORTA.jl.svg)](https://cirrus-ci.com/github/JuliaPolyhedra/XPORTA.jl) | ## Documentation -* XPORTA.jl documentation is published at [JuliaPolyhedra.github.io/XPORTA.jl/dev/](https://JuliaPolyhedra.github.io/XPORTA.jl/dev/). -* PORTA documentation is easily accessible at [github.com/bdoolittle/julia-porta](https://github.com/bdoolittle/julia-porta). -* PORTA source code may be downloaded from [http://porta.zib.de](http://porta.zib.de/). +* XPORTA.jl documentation: [JuliaPolyhedra.github.io/XPORTA.jl/dev/](https://JuliaPolyhedra.github.io/XPORTA.jl/stable/). +* PORTA documentation: [github.com/bdoolittle/julia-porta](https://github.com/bdoolittle/julia-porta). +* Official PORTA software: [http://porta.zib.de](http://porta.zib.de/). ## Licensing diff --git a/docs/src/Internals/binaries.md b/docs/src/Internals/binaries.md index 47866fd..6f43f13 100644 --- a/docs/src/Internals/binaries.md +++ b/docs/src/Internals/binaries.md @@ -15,5 +15,7 @@ run_xporta ## valid -!!! danger "Not Implemented" - Please reach out if you are interested in the `valid` subroutines. +```@docs +run_valid +iespo +``` diff --git a/docs/src/exports.md b/docs/src/exports.md index da3ac42..d960cfa 100644 --- a/docs/src/exports.md +++ b/docs/src/exports.md @@ -17,6 +17,18 @@ IEQ ## Methods +!!! note "Temp Files" + By default, files created by the PORTA binaries are deleted. When performing + longer computations with PORTA, it may be desirable to keep intermediate files. + The argument, `cleanup = false`, causes XPORTA.jl methods to write files to + the directory specified by the `dir` argument. + ```@docs traf +dim +fmel +vint +portsort +posie +fctp ``` diff --git a/docs/src/index.md b/docs/src/index.md index 66e8cbe..9d55aec 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -34,7 +34,7 @@ PORTA and XPORTA.jl are licensed under the GNU General Public License (GPL) v2.0 ## Acknowledgments -Development of Porta.jl was made possible by the advisory +Development of XPORTA.jl was made possible by the advisory of Dr. Eric Chitambar and general support from the Physics Department at the University of Illinois Urbana-Champaign. Funding was provided by NSF Award 1914440. diff --git a/src/XPORTA.jl b/src/XPORTA.jl index ca9976a..872a3b5 100644 --- a/src/XPORTA.jl +++ b/src/XPORTA.jl @@ -5,9 +5,18 @@ historical names from the PORTA software. # Exports -- [`POI`](@ref) - *Type*, The vertex representation of a polyhedra. -- [`IEQ`](@ref) - *Type*, The intersecting halfspace representation of a polyhedra. -- [`traf`](@ref) - *Method*, Converts a `POI` -> `IEQ` or `IEQ` -> `POI`. +*Types* +- [`POI`](@ref) - The vertex representation of a polyhedra. +- [`IEQ`](@ref) - The intersecting halfspace representation of a polyhedra. + +*Methods* +- [`traf`](@ref) - Converts a `POI` -> `IEQ` or `IEQ` -> `POI`. +- [`dim`](@ref) - Given a `POI` computes the dimension and constraining equalities of the `POI` convex hull. +- [`fmel`](@ref) - Projects the linear system of `IEQ` onto a subspace using fourier-motzkin elimination. +- [`vint`](@ref) - Enumerates the integral points which satisfy the linear system specified by an `IEQ`. +- [`portsort`](@ref) - Sorts the elements of `POI` and `IEQ` structs. +- [`posie`](@ref) - Enumerates the points and rays of a `POI` which satisfy the linear system of an `IEQ`. +- [`fctp`](@ref) - Determines if inequalities are tight or violated by elements of a `POI`. The compiled PORTA binaries are accessed through [PORTA_jll.jl](https://github.com/JuliaBinaryWrappers/PORTA_jll.jl) @@ -31,12 +40,14 @@ using PORTA_jll using Suppressor export POI, IEQ # types -export traf # xporta methods +export traf, portsort, dim, fmel # xporta methods +export fctp, posie, vint # valid methods # including local files include("./types.jl") include("./filesystem.jl") # utilities for create and removing directories include("./file_io.jl") # read/write functionality -include("./xporta_subroutines.jl") # wrapper for the xporta binaries. +include("./xporta_subroutines.jl") # wrapper for the xporta binaries. +include("./valid_subroutines.jl") # wrapper for the valid binaries end # module diff --git a/src/file_io.jl b/src/file_io.jl index 211a14c..461b8fc 100644 --- a/src/file_io.jl +++ b/src/file_io.jl @@ -137,12 +137,13 @@ function read_ieq(filepath::String)::IEQ{Rational{Int}} end # match in/equality sign and rhs value - rhs_match = match(r"(<=|=<|>=|=>|==|=)\s*([-+]?)\s*(\d+)", line) + rhs_match = match(r"(<=|=<|>=|=>|==|=)\s*([-+]?)\s*(\d+)(?:/(\d+))?", line) rel_sign = rhs_match.captures[1] - int_sign = (rhs_match.captures[2] === nothing) ? "+" : rhs_match.captures[2] - rhs_int = parse(Int, int_sign*rhs_match.captures[3]) - data_vector[end] = rhs_int + rhs_sign = (rhs_match.captures[2] === nothing) ? "+" : rhs_match.captures[2] + rhs_num = parse(Int, rhs_sign*rhs_match.captures[3]) + rhs_den = (rhs_match.captures[4] === nothing) ? 1 : parse(Int, rhs_match.captures[4]) + data_vector[end] = Rational(rhs_num, rhs_den) if (rel_sign == "<=") || (rel_sign == "=<") push!(inequality_rows, data_vector) diff --git a/src/types.jl b/src/types.jl index dede80a..ca1a1ad 100644 --- a/src/types.jl +++ b/src/types.jl @@ -27,7 +27,8 @@ converted to type `Rational{Int}`. Fields: * `conv_section` - each matrix row is a vertex. * `cone_section` - each matrix row is a ray. -* `valid` - a feasible point for the vertex representation. +* `valid` - a feasible point for the vertex representation. In the context of a `POI`, + this field has no known use. * `dim` - the dimension of vertices and rays. This field is auto-populated on construction. A `DomainError` is thrown if the column dimension of rays and vertices is not equal. @@ -113,8 +114,8 @@ both have the following form. `IEQ` Fields: * `inequalities`: each matrix row is a linear inequality, the first M elements indexed `1:(end-1)` are α and the last element indexed `end` is β. * `equalities`: each matrix row is linear equality, the first M elements indexed `1:(end-1)` are α and the last element indexed `end` is β. -* `lower_bounds`: each matrix row is a lower bound for enumerating integral points with `vint`. -* `upper_bounds`: each matrix row is an upper bound for enumerating integral points with `vint`. +* `lower_bounds`: a row vector which specifies the lower bound on each individual parameter. Used for enumerating integral points with `vint`. +* `upper_bounds`: a row vector which specifies the upper bound on each individual parameter. Used for enumerating integral points with `vint`. * `valid`: a feasible point for the linear system. * `dim`: the dimension of in/equalities, upper/lower bounds, etc. This field is auto-populated on construction. diff --git a/src/valid_subroutines.jl b/src/valid_subroutines.jl new file mode 100644 index 0000000..901f319 --- /dev/null +++ b/src/valid_subroutines.jl @@ -0,0 +1,282 @@ +""" + run_valid( method_flag::String, args::Array{String,1}; verbose::Bool=false ) :: String + +!!! warning + This method is intended for advanced use of the `valid` binary. User knowledge + of flags and arguments is required for successful execution. Users + must explicitly handle file IO for the `valid` binary. + +Runs the `valid` binary through `PORTA_jll` and returns a string containing `STDOUT`. +The `method_flag` specifies which `valid` subroutine to call. + +Valid options for `method_flag` are: +* `"-C"` runs the `fctp` subroutine +* `"-I"` runs the `iespo` subroutine +* `"-P"` runs the `posie` subroutine +* `"-V"` runs the `vint` subroutine + +The `args` parameter is uniquely specified by `method_flag`, for more information +regarding methods and arguments see the [`valid` documentation](https://github.com/bdoolittle/julia-porta#valid). + +If `verbose=true` the `valid` binary prints to `STDOUT`. +""" +function run_valid(method_flag::String, args::Array{String,1}; verbose::Bool=false) :: String + if !(method_flag in ["-C", "-I", "-P", "-V"]) + throw(DomainError(method_flag, "method_flag is invalid. Valid options are \"-C\", \"-I\", \"-P\", \"-V\".")) + end + + stdout = valid() do valid_path + @capture_out run(`$valid_path $method_flag $args`) + end + + if verbose + print(stdout) + end + + stdout +end + +""" + posie( ieq::IEQ, poi::POI; kwargs... ) :: POI{Rational{Int}} + +Enumerates the points and rays in the `POI` which satisfy the linear system +of the `IEQ`. A `POI` containing the valid points and rays is returned. + +`kwargs` is shorthand for the following keyword arguments: +* `dir :: String = "./"` - The directory to which files are written. +* `filename :: String = "posie_tmp"`- The name of produced files. +* `cleanup :: Bool = true` - If `true`, created files are removed after computation. +* `verbose :: Bool = false`- If `true`, PORTA will print progress to `STDOUT`. + +For more details regarding `posie()` please refer to the [PORTA posie documentation](https://github.com/bdoolittle/julia-porta#posie). +""" +function posie(ieq::IEQ, poi::POI; + dir::String="./", + filename::String="posie_tmp", + cleanup::Bool=true, + verbose::Bool=false +) :: POI{Rational{Int}} + + workdir = cleanup ? make_porta_tmp(dir) : dir + + ieq_filepath = write_ieq(filename, ieq, dir=workdir) + poi_filepath = write_poi(filename, poi, dir=workdir) + + run_valid("-P", [ieq_filepath, poi_filepath], verbose=verbose) + + valid_poi = isfile(ieq_filepath * ".poi") ? read_poi(ieq_filepath * ".poi") : POI(vertices = Array{Rational{Int}}(undef,0,0)) + + if cleanup + rm_porta_tmp(dir) + end + + return valid_poi +end + +""" + fctp( inequalities::PortaMatrix, poi::POI; kwargs... ) :: Dict{ String, Dict{Int, POI{Rational{Int}}} } + +For the provided `inequalities`, determines which ones tightly bound the polytope +specified by the input `POI` and which inequalities are violated by the input `POI`. +Tight bounds are labeled `"valid"` and violations are labeled `"invalid"`. In each case +the points which saturate or violate the inequalities are returned in a poi. Inequalities +which are loose bounds are not returned. + +Each row of the `inequalities` input corresponds to a distinct inequality in the +form specified by the `IEQ.inequalites` field. + +The output has a nested dictionary structure: +``` +Dict( + "valid" => Dict( + id => saturating_poi, # points/rays which saturate inequalities[id] + ... + ), + "invalid" => Dict( + id => violating_poi, # points/rays which violate inequalities[id] + ... + ) +) +``` + +where `ineq_id` corresponds to the row index of the input `inequalities`. The `"valid"` +and `"invalid"` dictionaries may include zero or more elements. + +`kwargs` is shorthand for the following keyword arguments: +* `dir::String = "./"` - The directory to which files are written. +* `filename::String = "fctp_tmp"`- The name of produced files. +* `cleanup::Bool = true` - If `true`, created files are removed after computation. +* `verbose::Bool = false`- If `true`, PORTA will print progress to `STDOUT`. + +For more details regarding `fctp()` please refer to the [PORTA fctp documentation](https://github.com/bdoolittle/julia-porta#fctp). +""" +function fctp( inequalities::PortaMatrix, poi::POI; + dir::String="./", + filename::String="fctp_tmp", + cleanup::Bool=true, + verbose::Bool=false +) :: Dict{ String, Dict{Int, POI{Rational{Int}}} } + + workdir = cleanup ? make_porta_tmp(dir) : dir + + ieq_filepath = write_ieq(filename, IEQ(inequalities=inequalities), dir=workdir) + poi_filepath = write_poi(filename, poi, dir=workdir) + + # there exists an error "sh: dim: command not found" coming from PORTA + # TODO: fix PORTA error in source code. + @suppress_err run_valid("-C", [ieq_filepath, poi_filepath], verbose=verbose) + + poi_files = filter(file -> occursin(r"^.*\.ieq\d+\.poi$", file), readdir(workdir)) + + tight_poi_tuples = Vector{Tuple{Int, POI{Rational{Int}}}}(undef, 0) + invalid_poi_tuples = Vector{Tuple{Int, POI{Rational{Int}}}}(undef, 0) + for i in 1:length(poi_files) + + poi_match = match(r"^.*\.ieq(\d+).poi$", poi_files[i]) + + ineq_id = parse(Int, poi_match.captures[1]) + poi = read_poi(workdir * "/" * poi_files[i]) + + poi_contains_points = (length(poi.conv_section) == 0) ? false : true + poi_contains_rays = (length(poi.cone_section) == 0) ? false : true + + ineq = inequalities[ineq_id,:] + + # PORTA does not indicate which .poi files are valid or invalid. The + # check is implemented manually below. A .poi is either valid or invalid + # thus it is sufficient to check a single element. + if poi_contains_points + # A valid point must satisfy the inequality with equality. + if ineq[1:end-1]' * poi.conv_section[1,:] == ineq[end] + push!(tight_poi_tuples, (ineq_id, poi)) + else + push!(invalid_poi_tuples, (ineq_id, poi)) + end + elseif poi_contains_rays + # A valid ray must make an obtuse angle with the normal vector of the + # bounding halfspace. + if ineq[1:end-1]' * poi.cone_section[1,:] <= 0 + push!(tight_poi_tuples, (ineq_id, poi)) + else + push!(invalid_poi_tuples, (ineq_id, poi)) + end + end + end + + if cleanup + rm_porta_tmp(dir) + end + + tight_poi_dict = Dict{Int, POI{Rational{Int}}}(tight_poi_tuples) + invalid_poi_dict = Dict{Int, POI{Rational{Int}}}(invalid_poi_tuples) + + Dict{String, Dict{Int, POI{Rational{Int}}}}( + "valid" => tight_poi_dict, + "invalid" => invalid_poi_dict + ) +end + +""" + iespo( ieq::IEQ, poi::POI; kwargs...) :: IEQ{Rational{Int}} + +Enumerates the valid equations and inequalities of the `IEQ` which satisfy the +points and rays in the `POI`. An `IEQ` containing valid inequalities and equations +is returned. + +!!! danger + This method does not work as described by the [PORTA iespo documentation](https://github.com/bdoolittle/julia-porta#iespo). Invalid + in/equalities are not filtered out. However, the strong validity check does + work. + + To run the strong validity check, use arguments `cleanup=false` and `opt_flag="-v"`. + With these arguments, `iespo()` will print a table to the output `.ieq` file which + can manually be read/parsed. + + In a future update, a parser may be written for the strong validity table or + the PORTA source code may be update to properly execute the in/equality filtering. + +`kwargs` is shorthand for the following keyword arguments: +* `dir::String = "./"` - The directory to which files are written. +* `filename::String = "iespo_tmp"`- The name of produced files. +* `strong_validity::Bool = false` - Prints the strong validity table to the output `IEQ`, (requires manual parsing). +* `cleanup::Bool = true` - If `true`, created files are removed after computation. +* `verbose::Bool = false`- If `true`, PORTA will print progress to `STDOUT`. + +For more details regarding `iespo()` please refer to the [PORTA iespo documentation](https://github.com/bdoolittle/julia-porta#iespo). +""" +function iespo(ieq::IEQ, poi::POI; + dir::String="./", + filename::String="iespo_tmp", + strong_validity::Bool=false, + cleanup::Bool=true, + verbose::Bool=false +) :: IEQ{Rational{Int}} + + workdir = cleanup ? make_porta_tmp(dir) : dir + + valid_args = Array{String,1}(undef,0) + if strong_validity + push!(valid_args, "-v") + end + + ieq_filepath = write_ieq(filename, ieq, dir=workdir) + poi_filepath = write_poi(filename, poi, dir=workdir) + push!(valid_args, ieq_filepath, poi_filepath) + + run_valid("-I", valid_args, verbose=verbose) + + valid_ieq = read_ieq(poi_filepath * ".ieq") + + if cleanup + rm_porta_tmp(dir) + end + + return valid_ieq +end + +""" + vint( ieq::IEQ; kwargs... ) :: POI{Rational{Int}} + +Enumerates all integral (integer) points which satisfy the linear system specified by the `IEQ` +and bounds specified by `upper_bounds` and `lower_bounds` fields of the `IEQ`. +These points may lie on the facets, vertices, or interior of the polytope specified by +`IEQ`. If no equalities or inequalities are specified, all integral points within +the bounds will be enumerated. + +`kwargs` specifes the keyword args: +* `dir::String = "./"` - The directory to which files are written. +* `filename::String = "vint_tmp"`- The name of produced files. +* `cleanup::Bool = true` - If `true`, created files are removed after computation. +* `verbose::Bool = false`- If `true`, PORTA will print progress to `STDOUT`. + +A `DomainError` is thrown if the `IEQ` does not contain upper/lower bounds or +if upper/lower bounds contain more than one row. + +For more details regarding `vint()` please refer to the [PORTA vint documentation](https://github.com/bdoolittle/julia-porta#vint). +""" +function vint(ieq::IEQ; + dir::String="./", + filename::String="vint_tmp", + cleanup::Bool=true, + verbose::Bool=false +) :: POI{Rational{Int}} + + workdir = cleanup ? make_porta_tmp(dir) : dir + + if (length(ieq.upper_bounds) != ieq.dim) || (length(ieq.lower_bounds) != ieq.dim) + throw(DomainError((ieq.upper_bounds, ieq.lower_bounds), "upper/lower bounds are required for vint and there should one row for each.")) + end + + ieq_filepath = write_ieq(filename, ieq, dir=workdir) + + run_valid("-V", [ieq_filepath]) + + # replace .ieq file extension with .poi extension + poi = read_poi(replace(ieq_filepath, r"\.ieq$"=>".poi")) + + if cleanup + rm_porta_tmp(dir) + end + + return poi +end diff --git a/src/xporta_subroutines.jl b/src/xporta_subroutines.jl index 51ed41e..d54f047 100644 --- a/src/xporta_subroutines.jl +++ b/src/xporta_subroutines.jl @@ -1,59 +1,60 @@ """ - run_xporta( method_flag::String, args::Array{String,1}; verbose::Bool = false) + run_xporta( method_flag::String, args::Array{String,1}; verbose::Bool = false) :: String !!! warning This method is intended for advanced use of the xporta binary. User knowledge of flags and arguments is required for successful execution. Furthermore, users - must explicitly handle file IO for the xporta binary. + must explicitly handle file IO for the `xporta` binary. -Runs the xporta binary through `PORTA_jll`. The `method_flag` argument tells the xporta -binary which method to call. Valid options include: -* `"-D"` runs the `dim` method -* `"-F"` runs the `fmel` method -* `"-S"` runs the `portsort` method -* `"-T"` runs the `traf` method +Runs the `xporta` binary through `PORTA_jll` and returns a string containing `STDOUT`. +The `method_flag` argument specifies which `xporta` subroutine to call. + +Valid options for `method_flag` are: +* `"-D"` runs the `dim` subroutine +* `"-F"` runs the `fmel` subroutine +* `"-S"` runs the `portsort` subroutine +* `"-T"` runs the `traf` subroutine The `args` parameter is uniquely specified by `method_flag`, for more information -regarding methods and arguments see the [xporta documentation](https://github.com/bdoolittle/julia-porta/blob/master/README.md#xporta). +regarding methods and arguments see the [`xporta` documentation](https://github.com/bdoolittle/julia-porta#xporta). -The `verbose` argument determines whether the xporta prints to `STDOUT`. +If `verbose=true` the `xporta` prints to `STDOUT`. """ -function run_xporta(method_flag::String, args::Array{String,1}; verbose::Bool=false) +function run_xporta(method_flag::String, args::Array{String,1}; verbose::Bool=false) :: String if !(method_flag in ["-D", "-F", "-S", "-T"]) throw(DomainError(method_flag, "method_flag is invalid. Valid options are \"-D\", \"-F\", \"-S\", \"-T\".")) end - xporta() do xporta_path - if !verbose - @suppress run(`$xporta_path $method_flag $args`) - else - run(`$xporta_path $method_flag $args`) - end + stdout = xporta() do xporta_path + @capture_out run(`$xporta_path $method_flag $args`) + end + + if verbose + print(stdout) end + + stdout end """ The `traf` method computes an `IEQ` struct given a `POI` struct, - traf( poi::POI; kwargs... ) :: IEQ + traf( poi::POI; kwargs... ) :: IEQ{Rational{Int}} or computes the `POI` struct from the `IEQ` struct. - traf(ieq::IEQ; kwargs... ) :: POI + traf(ieq::IEQ; kwargs... ) :: POI{Rational{Int}} -where `kwargs` is shorthand for the following keyword arguments: +When converting an `IEQ` -> `POI` the `valid` field of the `IEQ` must be populated +if the origin is not a feasible point of the linear system. -* `cleanup :: Bool = true` - Remove created files after computation. -* `dir :: String = "./"` - The directory in which to write files. -* `filename :: String = "traf_tmp"`- The name of produced files -* `opt_flag :: String = ""` - Optional flags to pass the `traf` method of the xporta binary. -* `verbose :: Bool = false`- If true, PORTA will print progress to `STDOUT`. +`kwargs` is shorthand for the following keyword arguments: -!!! note "Temp Files" - By default files created by the PORTA binaries are deleted. When performing - longer computations with PORTA, it may be desirable to keep intermediate files. - Passing the argument `cleanup = false` will cause the `traf` method to write all - files to directroy `dir`. +* `dir::String = "./"` - The directory in which to write files. +* `filename::String = "traf_tmp"`- The name of produced files. +* `cleanup::Bool = true` - If `true`, created files are removed after computation. +* `opt_flag::String = ""` - Optional flags to pass the `traf` method of the xporta binary. +* `verbose::Bool = false`- If `true`, PORTA will print progress to `STDOUT`. The following excerpt from the PORTA documentation lists valid optional flags and their behavior: @@ -78,13 +79,19 @@ The following excerpt from the PORTA documentation lists valid optional flags an are written in hexadecimal format (hex). Such hexadecimal format can not be reread as input. -For more details regarding `traf` please refer to the [PORTA traf documentation](https://github.com/bdoolittle/julia-porta/blob/master/README.md#traf). +For more details regarding `traf` please refer to the [PORTA traf documentation](https://github.com/bdoolittle/julia-porta#traf). """ -function traf(poi::POI; dir::String="./", filename::String="traf_tmp", opt_flag::String="", cleanup::Bool=true, verbose::Bool=false) :: IEQ +function traf(poi::POI; + dir::String="./", + filename::String="traf_tmp", + opt_flag::String="", + cleanup::Bool=true, + verbose::Bool=false +) :: IEQ{Rational{Int}} xporta_args = Array{String,1}(undef,0) if opt_flag != "" if !occursin(r"^-[poscvl]{1,6}$", opt_flag) || (length(opt_flag) != length(unique(opt_flag))) - throw(DomainError(opt_flags, "invalid opt_flags argument. Valid options any ordering of '-poscvl' and substrings.")) + throw(DomainError(opt_flags, "invalid `opt_flag` argument. Valid options are any ordering of '-poscvl' and substrings.")) end push!(xporta_args, opt_flag) end @@ -105,11 +112,17 @@ function traf(poi::POI; dir::String="./", filename::String="traf_tmp", opt_flag: return ieq end -function traf(ieq::IEQ; dir::String="./", filename::String="traf_tmp", opt_flag::String="", cleanup::Bool=true, verbose::Bool=false) :: POI +function traf(ieq::IEQ; + dir::String="./", + filename::String="traf_tmp", + opt_flag::String="", + cleanup::Bool=true, + verbose::Bool=false +) :: POI{Rational{Int}} xporta_args = Array{String,1}(undef,0) if opt_flag != "" if !occursin(r"^-[poscvl]{1,6}$", opt_flag) || (length(opt_flag) != length(unique(opt_flag))) - throw(DomainError(opt_flags, "invalid opt_flags argument. Valid options any ordering of '-poscvl' and permuted substrings.")) + throw(DomainError(opt_flags, "invalid `opt_flag` argument. Valid options are any ordering of '-poscvl' and permuted substrings.")) end end @@ -122,9 +135,226 @@ function traf(ieq::IEQ; dir::String="./", filename::String="traf_tmp", opt_flag: poi = read_poi(file_path * ".poi") - if (cleanup) + if cleanup + rm_porta_tmp(dir) + end + + return poi +end + +""" + portsort( ieq::IEQ; kwargs... ) :: IEQ{Rational{Int}} + +Sorts the inequalities and equalities of the provided `IEQ`. + + portsort( poi::POI; kwargs... ) :: POI{Rational{Int}} + +Sorts the vertices and rays of the provided `POI`. + +Sorting is performed in the following hierarchy: +1. Right-hand-side of in/equalities from high to low. +2. Scale factors from low to high. +3. Lexicographical order. + +`kwargs` is shorthand for the keyword arguments: +* `dir::String = "./"` - The directory in which to write files. +* `filename::String = "portsort_tmp"`- The name of produced files. +* `cleanup::Bool = true` - If `true`, created files are removed after computation. +* `verbose::Bool = false`- If `true`, PORTA will print progress to `STDOUT`. + +For more details regarding `portsort` please refer to the [PORTA portsort documentation](https://github.com/bdoolittle/julia-porta#portsort). +""" +function portsort(ieq::IEQ; + dir::String="./", + filename::String="portsort_tmp", + cleanup::Bool=true, + verbose::Bool=false +) :: IEQ{Rational{Int}} + + workdir = cleanup ? make_porta_tmp(dir) : dir + + ieq_filepath = write_ieq(filename, ieq, dir=workdir) + + run_xporta("-S", [ieq_filepath], verbose=verbose) + + ieq = read_ieq(ieq_filepath * ".ieq") + + if cleanup + rm_porta_tmp(dir) + end + + return ieq +end + +function portsort(poi::POI; + dir::String="./", + filename::String="portsort_tmp", + cleanup::Bool=true, + verbose::Bool=false +) :: POI{Rational{Int}} + + workdir = cleanup ? make_porta_tmp(dir) : dir + + poi_filepath = write_poi(filename, poi, dir=workdir) + + run_xporta("-S", [poi_filepath], verbose=verbose) + + poi = read_poi(poi_filepath * ".poi") + + if cleanup rm_porta_tmp(dir) end return poi end + +""" + dim( poi::POI; kwargs... ) :: Dict{String, Union{ Int, IEQ{Rational{Int}} } } + +Given a `POI` computes the minimal dimension and constraining equalities for the +convex hull of the `POI`. The returned dictionary has the form + +``` +Dict( + "dim" => , + "ieq" => +) +``` + +`kwargs` specifies keyword args: +* `dir::String = "./"` - The directory in which to write files. +* `filename::String = "dim_tmp"`- The name of produced files. +* `cleanup::Bool = true` - If `true`, created files are removed after computation. +* `verbose::Bool = false`- If `true`, PORTA will print progress to `STDOUT`. + +For more details regarding `dim` please refer to the [PORTA dim documentation](https://github.com/bdoolittle/julia-porta#dim). +""" +function dim(poi::POI; + dir::String="./", + filename::String="dim_tmp", + cleanup::Bool=true, + verbose::Bool=false +) :: Dict{String, Union{Int, IEQ{Rational{Int}}}} + + workdir = cleanup ? make_porta_tmp(dir) : dir + + poi_filepath = write_poi(filename, poi, dir=workdir) + + stdout = run_xporta("-D", [poi_filepath], verbose=verbose) + + dim_match = match(r"DIMENSION OF THE POLYHEDRON : (\d+)", stdout) + + eq_matches = collect(eachmatch(r"\(\s*\d+\)(.*)==\s*(.*)\n", stdout)) + + num_eq = length(eq_matches) + + equations = zeros(Rational{Int}, (num_eq, poi.dim + 1)) + for i in 1:num_eq + eq_match = eq_matches[i] + + # parse left hand side matches + for lhs_match in eachmatch(r"\s*([+-])\s*(?:(\d+)(?:/(\d+))?)?x(\d+)", eq_match.captures[1]) + + sign = lhs_match.captures[1] + num = (lhs_match.captures[2] === nothing) ? parse(Int, sign*"1") : parse(Int, sign*lhs_match.captures[2]) + den = (lhs_match.captures[3] === nothing) ? 1 : parse(Int, lhs_match.captures[3]) + col_id = parse(Int, lhs_match.captures[4]) + + equations[i,col_id] += Rational(num, den) + end + + # parse right hand side match + rhs_match = match(r"([+-])?\s*(\d+)(?:/(\d+))?", eq_match.captures[2]) + + sign = (rhs_match.captures[1] === nothing) ? "+" : rhs_match.captures[1] + num = parse(Int, sign*rhs_match.captures[2]) + den = (rhs_match.captures[3] === nothing) ? 1 : parse(Int, rhs_match.captures[3]) + + equations[i,end] += Rational(num, den) + end + + if cleanup + rm_porta_tmp(dir) + end + + Dict( + "dim" => parse(Int, dim_match.captures[1]), + "ieq" => IEQ(equalities = equations) + ) +end + +""" + fmel( ieq::IEQ; kwargs... ) :: IEQ{Rational{Int}} + +Projects the linear system of `IEQ` onto a subspace using fourier-motzkin elimination. +The projected linear system is returned in an `IEQ`. Note that redundant inequalities +may be present in the returned `IEQ`. + +The `elimination_order` field of the `IEQ` must be populated and of length `dim`. +A `0` value indicates a parameter which will not be eliminated and `1,2,...` specifies +the order in which the parameters will be eliminated. Performance may change based +on the order of elimination. + +`kwargs` specifies keyword args: +* `dir::String = "./"` - The directory in which to write files. +* `filename::String = "fmel_tmp"`- The name of produced files. +* `opt_flag::String = ""` - optional flags for the `fmel` subroutine. +* `cleanup::Bool = true` - If `true`, created files are removed after computation. +* `verbose::Bool = false`- If `true`, PORTA will print progress to `STDOUT`. + +A `DomainError` is thrown if the `elimination_order` field is not of length `dim`. +A `DomainError` is thrown if the `opt_flag` argument is not a substring of `"-pcl"` +or its permutations. + +The valid options for the `opt_flag`: +``` +-p Unbuffered redirection of the terminal messages into the file + input_fname_with_suffix_'prt' + +-c Generation of new inequalities without the rule of Chernikov. + +-l Use a special integer arithmetic allowing the integers to have arbitrary + lengths. This arithmetic is not as efficient as the system's integer + arithmetic with respect to time and storage requirements. + + Note: Output values which exceed the 32-bit integer storage size are written + in hexadecimal format (hex). Such hexadecimal format can not be reread as input. +``` + +For more details regarding `fmel` please refer to the [PORTA fmel documentation](https://github.com/bdoolittle/julia-porta#fmel). +""" +function fmel(ieq::IEQ; + dir::String="./", + filename::String="fmel_tmp", + opt_flag::String="", + cleanup::Bool=true, + verbose::Bool=false +) :: IEQ{Rational{Int}} + + workdir = cleanup ? make_porta_tmp(dir) : dir + + if length(ieq.elimination_order) != ieq.dim + throw(DomainError(ieq.elimination_order, "elimination_order field is required for `fmel` and there should be a single row.")) + end + + xporta_args = Array{String,1}(undef,0) + if opt_flag != "" + if !occursin(r"^-[pcl]{1,3}$", opt_flag) || (length(opt_flag) != length(unique(opt_flag))) + throw(DomainError(opt_flags, "invalid `opt_flag` argument. Valid options are any ordering of '-pcl' and substrings.")) + end + push!(xporta_args, opt_flag) + end + + ieq_filepath = write_ieq(filename, ieq, dir=workdir) + push!(xporta_args, ieq_filepath) + + run_xporta("-F", xporta_args, verbose=verbose) + + ieq = read_ieq(ieq_filepath * ".ieq") + + if cleanup + rm_porta_tmp(dir) + end + + return ieq +end diff --git a/test/Manifest.toml b/test/Manifest.toml index 6fdd690..db86d66 100644 --- a/test/Manifest.toml +++ b/test/Manifest.toml @@ -38,6 +38,11 @@ uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" [[Sockets]] uuid = "6462fe0b-24de-5631-8697-dd941f90decc" +[[Suppressor]] +git-tree-sha1 = "a819d77f31f83e5792a76081eee1ea6342ab8787" +uuid = "fd094767-a336-5f1f-9728-57cf17d0bbfb" +version = "0.2.0" + [[Test]] deps = ["Distributed", "InteractiveUtils", "Logging", "Random"] uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/test/Project.toml b/test/Project.toml index 6fbca65..3fc6ee6 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,4 +1,5 @@ [deps] LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +Suppressor = "fd094767-a336-5f1f-9728-57cf17d0bbfb" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" diff --git a/test/integration/valid_subroutines.jl b/test/integration/valid_subroutines.jl new file mode 100644 index 0000000..09201d1 --- /dev/null +++ b/test/integration/valid_subroutines.jl @@ -0,0 +1,324 @@ +using Test, Suppressor, XPORTA + +@testset "src/valid_subroutines.jl" begin + +dir = "./test/files/" + +@testset "run_valid()" begin + @testset "throws DomainError if method_flag is not recognized" begin + @test_throws DomainError XPORTA.run_valid("-X", [dir*"example1.poi"]) + end + + @testset "verbose prints to STDOUT" begin + # setup files for first run + XPORTA.rm_porta_tmp(dir) + XPORTA.make_porta_tmp(dir) + ex1_poi_filepath = cp(dir*"example1.poi", dir*"porta_tmp/example1.poi") + ex1_ieq_filepath = cp(dir*"example1.ieq", dir*"porta_tmp/example1.ieq") + + # stdout is returned when verbose=false (also returned when true) + return_string = XPORTA.run_valid("-P", [ex1_ieq_filepath, ex1_poi_filepath], verbose=false) + + # setup files for second run + XPORTA.rm_porta_tmp(dir) + XPORTA.make_porta_tmp(dir) + ex1_poi_filepath = cp(dir*"example1.poi", dir*"porta_tmp/example1.poi") + ex1_ieq_filepath = cp(dir*"example1.ieq", dir*"porta_tmp/example1.ieq") + + # capturing stdout when verbose=true + capture_string = @capture_out XPORTA.run_valid("-P", [ex1_ieq_filepath, ex1_poi_filepath], verbose=true) + + @test return_string == capture_string + + XPORTA.rm_porta_tmp(dir) + end +end + +@testset "posie()" begin + @testset "valid in/equalities for a simple 3-simplex" begin + simplex_ieq = IEQ(inequalities = [-1 0 0 0; 0 -1 0 0; 0 0 -1 0], equalities = [1 1 1 1]) + + @testset "all points are valid" begin + poi = POI(vertices = [1 0 0; 0 1 0; 0 0 1]) + + valid_poi = posie(simplex_ieq, poi, dir=dir) + + @test poi.conv_section == valid_poi.conv_section + @test poi.cone_section == valid_poi.cone_section + end + + @testset "no points are valid by equality and inequality" begin + poi = POI(vertices = [-1 0 0;0 -1 0;0 0 -1]) + + valid_poi = posie(simplex_ieq, poi, dir=dir) + + @test valid_poi.conv_section == Array{Rational{Int}}(undef,0,0) + @test valid_poi.cone_section == Array{Rational{Int}}(undef,0,0) + end + + @testset "points satisfy inequalities, but not equality" begin + poi = POI(vertices = [1//2 0 0;0 1//2 0;0 0 1//2]) + + valid_poi = posie(simplex_ieq, poi, dir=dir) + + @test valid_poi.conv_section == Array{Rational{Int}}(undef,0,0) + @test valid_poi.cone_section == Array{Rational{Int}}(undef,0,0) + end + + @testset "points satisfy equality, but not inequality" begin + poi = POI(vertices = [3//2 -1//2 0;0 3//2 -1//2;-1//2 0 3//2]) + + valid_poi = posie(simplex_ieq, poi, dir=dir) + + @test valid_poi.conv_section == Array{Rational{Int}}(undef,0,0) + @test valid_poi.cone_section == Array{Rational{Int}}(undef,0,0) + end + + @testset "all points satisfy, but are not tight with inequalities" begin + poi = POI(vertices = [1//2 1//2 0;0 1//2 1//2;1//2 0 1//2]) + + valid_poi = posie(simplex_ieq, poi, dir=dir) + + @test valid_poi.conv_section == poi.conv_section + @test valid_poi.cone_section == Array{Rational{Int}}(undef,0,0) + end + + @testset "one point does not satisfy and is excluded from output" begin + poi = POI(vertices = [1 0 0;0 1//2 0;0 0 1]) + + valid_poi = posie(simplex_ieq, poi, dir=dir) + + @test valid_poi.conv_section == [1 0 0;0 0 1] + @test valid_poi.cone_section == Array{Rational{Int}}(undef,0,0) + end + end +end + +@testset "fctp()" begin + @testset "tight bounds on a simple 3-cube with sidelength 2" begin + cube_poi = POI(vertices=[ + 1 1 1; 1 1 -1; 1 -1 1; 1 -1 -1; + -1 1 1; -1 1 -1; -1 -1 1; -1 -1 -1; + ]) + + @testset "single tight facet returns points on facet" begin + poi_dict = fctp([0 0 1 1], cube_poi, dir=dir) + + @test collect(keys(poi_dict)) == ["valid", "invalid"] + @test collect(keys(poi_dict["valid"])) == [1] + @test poi_dict["valid"][1].conv_section == [1 1 1;1 -1 1;-1 1 1;-1 -1 1] + @test length(poi_dict["invalid"]) == 0 + end + + @testset "single loose upper bound facet will return nothing" begin + poi_dict = fctp([0 0 1 2], cube_poi, dir=dir) + @test length(poi_dict["valid"]) == 0 + @test length(poi_dict["invalid"]) == 0 + end + + @testset "invalid facet for polytope returns violating points" begin + poi_dict = fctp([0 0 1 1//2], cube_poi, dir=dir) + @test length(poi_dict["valid"]) == 0 + @test length(poi_dict["invalid"]) == 1 + @test collect(keys(poi_dict["invalid"])) == [1] + @test poi_dict["invalid"][1].conv_section == [1 1 1;1 -1 1;-1 1 1;-1 -1 1] + end + + @testset "loose, tight, and invalid facet" begin + poi_dict = fctp([0 0 1 2;0 0 1 1;0 0 1 1//2], cube_poi, dir=dir) + @test length(poi_dict["valid"]) == 1 + @test poi_dict["valid"][2].conv_section == [1 1 1;1 -1 1;-1 1 1;-1 -1 1] + @test poi_dict["invalid"][3].conv_section == [1 1 1;1 -1 1;-1 1 1;-1 -1 1] + end + + @testset "halfspace containing single vertex is valid for that vertex" begin + poi_dict = fctp([1 1 1 3], cube_poi, dir=dir) + + @test length(poi_dict["valid"]) == 1 + @test length(poi_dict["invalid"]) == 0 + + @test poi_dict["valid"][1].conv_section == [1 1 1] + end + end + + @testset "square cone" begin + square_cone_rays = [1 1 1;1 -1 1;-1 1 1;-1 -1 1] + square_cone_poi = POI(rays=square_cone_rays) + + @testset "tight bounds" begin + inequalities = [0 1 -1 0;1 0 -1 0;-1 0 -1 0;0 -1 -1 0] + + poi_dict = fctp(inequalities, square_cone_poi, dir=dir) + + @test length(poi_dict["valid"]) == 4 + + @test poi_dict["valid"][1].cone_section == vcat(square_cone_rays[1,:]',square_cone_rays[3,:]') + @test poi_dict["valid"][2].cone_section == vcat(square_cone_rays[1,:]',square_cone_rays[2,:]') + @test poi_dict["valid"][3].cone_section == vcat(square_cone_rays[3,:]',square_cone_rays[4,:]') + @test poi_dict["valid"][4].cone_section == vcat(square_cone_rays[2,:]',square_cone_rays[4,:]') + + @test length(poi_dict["invalid"]) == 0 + end + + @testset "positive z halfspace" begin + poi_dict = fctp([0 0 -1 0], square_cone_poi, dir=dir) + + @test length(poi_dict["valid"]) == 0 + @test length(poi_dict["invalid"]) == 0 + end + + @testset "loose positive z halfspace" begin + poi_dict = fctp([0 0 -1 1], square_cone_poi, dir=dir) + + @test length(poi_dict["valid"]) == 0 + @test length(poi_dict["invalid"]) == 0 + end + + @testset "loose parallel bound" begin + poi_dict = fctp([0 1 -1 1], square_cone_poi, dir=dir) + + @test length(poi_dict["invalid"]) == 0 + @test poi_dict["valid"][1].cone_section == [1 1 1;-1 1 1] + end + + @testset "loose bound non-parallel" begin + poi_dict = fctp([0 1 -2 0], square_cone_poi, dir=dir) + + @test length(poi_dict["invalid"]) == 0 + @test length(poi_dict["valid"]) == 0 + end + + @testset "excluding bound" begin + poi_dict = fctp([0 2 -1 0], square_cone_poi, dir=dir) + + @test length(poi_dict["valid"]) == 0 + @test length(poi_dict["invalid"]) == 1 + @test poi_dict["invalid"][1].cone_section == [1 1 1;-1 1 1] + end + + @testset "excluding, loose, and tight bounds" begin + poi_dict = fctp([0 2 -1 0;0 0 -1 1;0 -1 -1 0], square_cone_poi, dir=dir) + + @test length(poi_dict["valid"]) == 1 + @test length(poi_dict["invalid"]) == 1 + + @test poi_dict["valid"][3].cone_section == [1 -1 1;-1 -1 1] + @test poi_dict["invalid"][1].cone_section == [1 1 1;-1 1 1] + end + end +end + +@testset "XPORTA.iespo()" begin + + # verifes functionality broken within porta. + # TODO: fix PORTA src to let iespo work properly + @testset "XPORTA.iespo() fails to do its task" begin + @testset "valid in/equalities for a simple 3-simplex" begin + simplex_poi = POI(vertices=[1 0 0;0 1 0;0 0 1]) + + @testset "tight in/equalities are all valid" begin + ieq = IEQ(inequalities = [-1 0 0 0; 0 -1 0 0; 0 0 -1 0], equalities = [1 1 1 1]) + + valid_ieq = XPORTA.iespo(ieq, simplex_poi, dir=dir) + + @test ieq.inequalities == valid_ieq.inequalities + @test ieq.equalities == valid_ieq.equalities + end + + @testset "loose upper bounds inequalities" begin + ieq = IEQ(inequalities = [-1 0 0 1; 0 -1 0 1; 0 0 -1 1], equalities = [1 1 1 1]) + + valid_ieq = XPORTA.iespo(ieq, simplex_poi, dir=dir) + + @test ieq.inequalities == valid_ieq.inequalities + @test ieq.equalities == valid_ieq.equalities + end + + @testset "tight inequalities invalid equality" begin + ieq = IEQ(inequalities = [-1 0 0 0; 0 -1 0 0; 0 0 -1 0], equalities = [1 1 1 2]) + + valid_ieq = XPORTA.iespo(ieq, simplex_poi, dir=dir) + + @test ieq.inequalities == valid_ieq.inequalities + @test ieq.equalities == valid_ieq.equalities + end + + @testset "invalid inequalities and equation" begin + ieq = IEQ(inequalities = [-1 0 0 -2; 0 -1 0 -2; 0 0 -1 -2], equalities = [1 1 1 2]) + + valid_ieq = XPORTA.iespo(ieq, simplex_poi, dir=dir) + + @test ieq.inequalities == valid_ieq.inequalities + @test ieq.equalities == valid_ieq.equalities + end + end + end +end + +@testset "vint()" begin + @testset "invalid upper/lower bounds" begin + @test_throws DomainError vint(IEQ(inequalities=[-1 0 0 0;0 -1 0 0;0 0 -1 0], upper_bounds = [1 1 1;2 2 2], lower_bounds = [0 0 0]), dir=dir) + @test_throws DomainError vint(IEQ(inequalities=[-1 0 0 0;0 -1 0 0;0 0 -1 0], upper_bounds = [1 1 1], lower_bounds = [0 0 0;-1 -1 -1]), dir=dir) + @test_throws DomainError vint(IEQ(inequalities=[-1 0 0 0;0 -1 0 0;0 0 -1 0], upper_bounds = [1 1 1]), dir=dir) + @test_throws DomainError vint(IEQ(inequalities=[-1 0 0 0;0 -1 0 0;0 0 -1 0], lower_bounds = [0 0 0]), dir=dir) + end + + @testset "finds vertices of simple polyhedra given complete H-representation" begin + @testset "unit cube with corner at origin has vertices as integer points" begin + int_cube_ieq = IEQ(inequalities=[0 0 1 1;0 1 0 1;1 0 0 1;-1 0 0 0;0 -1 0 0;0 0 -1 0], upper_bounds=[1 1 1], lower_bounds=[0 0 0]) + int_cube_poi = vint(int_cube_ieq, dir=dir) + + @test int_cube_poi.conv_section == [ + 0 0 0;0 0 1;0 1 0;0 1 1; + 1 0 0;1 0 1;1 1 0;1 1 1; + ] + @test length(int_cube_poi.cone_section) == 0 + end + + @testset "vint example from http://co-at-work.zib.de/berlin2009/downloads/2009-09-22/2009-09-22-0900-CR-AW-Introduction-Porta-Polymake.pdf" begin + example_ieq = IEQ(inequalities=[ + -1 0 0 0 0;0 -1 0 0 0;0 0 -1 0 0;0 0 0 -1 0;1 1 0 0 1;0 1 1 0 1;0 0 1 1 1;1 0 0 1 1;0 1 0 1 1 + ], upper_bounds=[1 1 1 1], lower_bounds=[0 0 0 0]) + + example_poi = vint(example_ieq, dir=dir) + @test example_poi.conv_section == [0 0 0 0;0 0 0 1;0 0 1 0;0 1 0 0;1 0 0 0;1 0 1 0] + end + + @testset "unit cube centered at origin only has the origin as an integral point" begin + cube_ieq = IEQ(inequalities=[ + 0 0 2 1;0 2 0 1;2 0 0 1;-2 0 0 1;0 -2 0 1;0 0 -2 1 + ], upper_bounds=[1 1 1], lower_bounds=[-1 -1 -1]) + + cube_int_poi = vint(cube_ieq, dir=dir) + + @test cube_int_poi.conv_section == [0 0 0] + end + + @testset "simplex with integral vertices" begin + simplex_ieq = IEQ( + inequalities=[-1 0 0 0;0 -1 0 0;0 0 -1 0], equalities=[1 1 1 1], + upper_bounds=[1 1 1], lower_bounds=[0 0 0] + ) + simplex_poi = vint(simplex_ieq, dir=dir) + + @test simplex_poi.conv_section == [0 0 1;0 1 0;1 0 0] + end + end + + @testset "open linear system" begin + @testset "positive octant in 3-space" begin + # bounds completely specify a valid range w/o inequalities + ieq = IEQ( + upper_bounds=[1 1 1], + lower_bounds=[0 0 0] + ) + poi = vint(ieq, dir=dir) + @test poi.conv_section == [ + 0 0 0;0 0 1;0 1 0;0 1 1; + 1 0 0;1 0 1;1 1 0;1 1 1; + ] + end + end +end + +end diff --git a/test/integration/xporta_subroutines.jl b/test/integration/xporta_subroutines.jl index 11d5a94..f9719fa 100644 --- a/test/integration/xporta_subroutines.jl +++ b/test/integration/xporta_subroutines.jl @@ -1,4 +1,4 @@ -using Test, XPORTA +using Test, Suppressor, XPORTA @testset "src/xporta_subroutines.jl" begin @@ -6,7 +6,29 @@ dir = "./test/files/" @testset "run_xporta()" begin @testset "throws DomainError if method_flag is not recognized" begin - @test_throws DomainError XPORTA.run_xporta("-X", ["dir/example1.poi"]) + @test_throws DomainError XPORTA.run_xporta("-X", [dir *"example1.poi"]) + end + + @testset "verbose prints to STDOUT" begin + # setup files for first run + XPORTA.rm_porta_tmp(dir) + XPORTA.make_porta_tmp(dir) + ex1_poi_filepath = cp(dir*"example1.poi", dir*"porta_tmp/example1.poi") + + # stdout is returned when verbose=false (also returned when true) + return_string = XPORTA.run_xporta("-T", [ex1_poi_filepath], verbose=false) + + # setup files for second run + XPORTA.rm_porta_tmp(dir) + XPORTA.make_porta_tmp(dir) + ex1_poi_filepath = cp(dir*"example1.poi", dir*"porta_tmp/example1.poi") + + # capturing stdout when verbose=true + capture_string = @capture_out XPORTA.run_xporta("-T", [ex1_poi_filepath], verbose=true) + + @test return_string == capture_string + + XPORTA.rm_porta_tmp(dir) end @testset "test traf (xporta -T) with example1.poi" begin @@ -158,4 +180,191 @@ end end end +@testset "portsort()" begin + @testset "ieq inputs" begin + @testset "sorts inequalities by scale factor (high to low)" begin + sort_ieq = portsort(IEQ(inequalities=[-1 0 0 0;-2 0 0 0;1 0 0 0], equalities=[1 0 -1 0;2 0 -1 0]), dir=dir) + @test sort_ieq.inequalities == [1 0 0 0;-1 0 0 0;-2 0 0 0] + @test sort_ieq.equalities == [2 0 -1 0;1 0 -1 0] + + sort_ieq = portsort(IEQ(inequalities=[3 0 0 0;0 2 0 0;0 0 1 0]), dir=dir) + @test sort_ieq.inequalities == [3 0 0 0;0 2 0 0;0 0 1 0] + end + + @testset "sorts inequalities by lexicographical order" begin + sort_ieq = portsort(IEQ(inequalities=[0 -1 0 0;-1 0 0 0;0 0 -1 0], equalities = [2 0 -1 0;2 -1 0 0]), dir=dir) + @test sort_ieq.inequalities == [-1 0 0 0;0 -1 0 0;0 0 -1 0] + @test sort_ieq.equalities == [2 -1 0 0;2 0 -1 0] + end + + @testset "sorts inequalites by rhs bound (low to high)" begin + sort_ieq = portsort(IEQ(inequalities=[-1 0 0 3;-1 0 0 2;-1 0 0 1], equalities = [1 1 1 1;1 1 1 -1]), dir=dir) + @test sort_ieq.inequalities == [-1 0 0 1;-1 0 0 2;-1 0 0 3] + @test sort_ieq.equalities == [1 1 1 -1;1 1 1 1] + end + + @testset "sorts inequalities by scale factor over lexicographical order" begin + sort_ieq = portsort(IEQ(inequalities=[-3 0 0 0;0 -2 0 0;0 0 -1 0]), dir=dir) + @test sort_ieq.inequalities == [0 0 -1 0;0 -2 0 0;-3 0 0 0] + end + + @testset "sorts inequalities by rhs bound, scale factor, then lexicographical order" begin + sort_ieq = portsort(IEQ(inequalities=[0 -3 0 0;0 -1 0 2;0 -1 0 1;-2 0 0 2]), dir=dir) + @test sort_ieq.inequalities == [0 -3 0 0;0 -1 0 1;0 -1 0 2;-2 0 0 2] + end + end + + @testset "poi inputs" begin + @testset "sorts points by scale factor (high to low)" begin + sort_poi = portsort(POI(vertices=[-1 0 0 0;-2 0 0 0;1 0 0 0], rays=[0 1 0 0;0 2 0 0;0 3 0 0]), dir=dir) + @test sort_poi.conv_section == [1 0 0 0;-1 0 0 0;-2 0 0 0] + @test sort_poi.cone_section == [0 3 0 0;0 2 0 0;0 1 0 0] + + sort_poi = portsort(POI(vertices=[3 0 0 0;0 2 0 0;0 0 1 0]), dir=dir) + @test sort_poi.conv_section == [3 0 0 0;0 2 0 0;0 0 1 0] + end + + @testset "sorts points by lexicographical order" begin + sort_poi = portsort(POI(vertices=[0 -1 0 0;-1 0 0 0;0 0 -1 0]), dir=dir) + @test sort_poi.conv_section == [-1 0 0 0;0 -1 0 0;0 0 -1 0] + end + + @testset "sorts points by scale factor over lexicographical order" begin + sort_poi = portsort(POI(vertices=[-3 0 0 0;0 -2 0 0;0 0 -1 0], rays=[0 0 0 3;1 0 0 0;0 -1 0 0]), dir=dir) + @test sort_poi.conv_section == [0 0 -1 0;0 -2 0 0;-3 0 0 0] + @test sort_poi.cone_section == [0 0 0 3;1 0 0 0;0 -1 0 0] + + sort_poi = portsort(POI(vertices=[0 -3 0 0;0 -1 0 2;0 -1 0 1;-2 0 0 2]), dir=dir) + @test sort_poi.conv_section == [0 -1 0 2;0 -1 0 1;-2 0 0 2;0 -3 0 0] + end + end +end + +@testset "dim()" begin + + @testset "cube" begin + cube_poi = POI(vertices=[ + 1 1 1; 1 1 -1; 1 -1 1; 1 -1 -1; + -1 1 1; -1 1 -1; -1 -1 1; -1 -1 -1; + ]) + + dim_dict = dim(cube_poi, dir=dir) + + @test dim_dict["dim"] == 3 + @test dim_dict["ieq"].dim == 3 + @test dim_dict["ieq"].equalities == Matrix{Rational{Int}}(undef, (0,4)) + end + + @testset "3-simplex in 3 dimensions" begin + dim_dict = dim(POI(vertices=[1 0 0;0 1 0;0 0 1]), dir=dir) + @test dim_dict["dim"] == 2 + @test dim_dict["ieq"].dim == 3 + @test dim_dict["ieq"].equalities == [1 1 1 1] + end + + @testset "3-simplex in 9 dimensions" begin + dim_dict = dim(POI(vertices = [ + 1 1 1 0 0 0 0 0 0; + 0 0 0 1 1 1 0 0 0; + 0 0 0 0 0 0 1 1 1; + ]), dir=dir) + + @test length(dim_dict) == 2 + @test dim_dict["dim"] == 2 + @test dim_dict["ieq"].dim == 9 + @test dim_dict["ieq"].equalities == [ + 0 0 0 0 0 0 0 1 -1 0; + 0 0 0 0 0 0 1 -1 0 0; + 0 0 0 0 1 -1 0 0 0 0; + 0 0 0 1 -1 0 0 0 0 0; + 0 1 -1 0 0 0 0 0 0 0; + 1 -1 0 0 0 0 0 0 0 0; + 0 0 1 0 0 1 0 0 1 1; + ] + end +end + +@testset "fmel()" begin + @testset "invalid elimination order fields" begin + # too short + @test_throws DomainError fmel(IEQ(inequalities = [-1 0 1; 0 -1 1]), dir=dir) + # too long + @test_throws DomainError fmel(IEQ(inequalities =[-1 0 1; 0 -1 1], elimination_order = [1 2 0]), dir=dir) + # too many rows + @test_throws DomainError fmel(IEQ(inequalities =[-1 0 1; 0 -1 1], elimination_order = [1 0;0 1]), dir=dir) + end + + @testset "invalid opt_flag" begin + @test_throws DomainError fmel(IEQ(inequalities = [-1 0 1; 0 -1 1]), dir=dir, opt_flag="c") + @test_throws DomainError fmel(IEQ(inequalities = [-1 0 1; 0 -1 1]), dir=dir, opt_flag="-x") + @test_throws DomainError fmel(IEQ(inequalities = [-1 0 1; 0 -1 1]), dir=dir, opt_flag="-pcll") + @test_throws DomainError fmel(IEQ(inequalities = [-1 0 1; 0 -1 1]), dir=dir, opt_flag="-cc") + @test_throws DomainError fmel(IEQ(inequalities = [-1 0 1; 0 -1 1]), dir=dir, opt_flag="-pcx") + end + + @testset "octahedron projections" begin + octahedron_ineqs = [ + 1 1 1 1; -1 1 1 1; 1 -1 1 1; 1 1 -1 1; + -1 -1 1 1; -1 1 -1 1; 1 -1 -1 1; -1 -1 -1 1; + ] + + @testset "project onto coordinate 3" begin + fmel_ieq = fmel(IEQ(inequalities = octahedron_ineqs, elimination_order = [1 2 0]), dir=dir) + @test fmel_ieq.dim == 3 + @test fmel_ieq.inequalities == [ + 0 0 0 1; 0 0 1 1; 0 0 1 1; 0 0 -1 1; 0 0 -1 1 + ] + end + + @testset "project onto coordinate 3 (switched order of elimination)" begin + fmel_ieq = fmel(IEQ(inequalities = octahedron_ineqs, elimination_order = [2 1 0]), dir=dir) + @test fmel_ieq.dim == 3 + @test fmel_ieq.inequalities == [ + 0 0 0 1; 0 0 1 1; 0 0 1 1; 0 0 -1 1; 0 0 -1 1 + ] + end + + @testset "projection onto coordinate 1" begin + fmel_ieq = fmel(IEQ(inequalities = octahedron_ineqs, elimination_order = [0 1 2]), dir=dir) + @test fmel_ieq.dim == 3 + @test fmel_ieq.inequalities == [ + 0 0 0 1; 0 0 0 1; 1 0 0 1; 1 0 0 1; -1 0 0 1; -1 0 0 1 + ] + end + + @testset "projection onto coordinate 2" begin + fmel_ieq = fmel(IEQ(inequalities = octahedron_ineqs, elimination_order = [1 0 2]), dir=dir) + @test fmel_ieq.dim == 3 + @test fmel_ieq.inequalities == [ + 0 0 0 1; 0 0 0 1; 0 1 0 1; 0 1 0 1; 0 -1 0 1; 0 -1 0 1 + ] + end + + @testset "projection onto coordinates 2,3 " begin + fmel_ieq = fmel(IEQ(inequalities = octahedron_ineqs, elimination_order = [1 0 0]), dir=dir) + @test fmel_ieq.dim == 3 + @test fmel_ieq.inequalities == [ + 0 0 1 1; 0 0 1 1; 0 1 0 1; 0 1 0 1; 0 1 1 1; 0 -1 0 1; 0 -1 0 1; + 0 0 -1 1; 0 0 -1 1; 0 -1 1 1; 0 1 -1 1; 0 -1 -1 1; + ] + end + + @testset "inequalities return if no terms eliminated" begin + fmel_ieq = fmel(IEQ(inequalities = octahedron_ineqs, elimination_order = [0 0 0]), dir=dir) + @test fmel_ieq.dim == 3 + @test fmel_ieq.inequalities == octahedron_ineqs + end + + @testset "many redundant inequalities without rule of chernikov" begin + fmel_ieq = fmel(IEQ(inequalities = octahedron_ineqs, elimination_order = [1 2 0]), dir=dir, opt_flag="-c") + @test fmel_ieq.dim == 3 + @test fmel_ieq.inequalities == [ + 0 0 0 1;0 0 0 1;0 0 0 1;0 0 1 1;0 0 1 1;0 0 1 1; + 0 0 -1 1;0 0 -1 1;0 0 -1 1;0 0 1 2;0 0 1 2;0 0 1 2; + 0 0 1 2;0 0 -1 2;0 0 -1 2;0 0 -1 2; 0 0 -1 2; + ] + end + end +end + end diff --git a/test/regression/simple_polyhedra.jl b/test/regression/simple_polyhedra.jl new file mode 100644 index 0000000..46b4445 --- /dev/null +++ b/test/regression/simple_polyhedra.jl @@ -0,0 +1,19 @@ +using Test, XPORTA + +@testset "./test/regression/simple_polyhedra" begin + @testset "Unit 3-cube centered at origin" begin + cube_poi = POI(vertices=[ + 1//2 1//2 1//2; 1//2 1//2 -1//2; 1//2 -1//2 1//2; 1//2 -1//2 -1//2; + -1//2 1//2 1//2; -1//2 1//2 -1//2; -1//2 -1//2 1//2; -1//2 -1//2 -1//2; + ]) + + ieq = traf(cube_poi) + + @test ieq.dim == 3 + @test ieq.inequalities == [ + 0 0 2 1; 0 2 0 1; 2 0 0 1; + -2 0 0 1; 0 -2 0 1; 0 0 -2 1; + ] + @test ieq.equalities == Array{Rational{Int64}}(undef,0,0) + end +end