SubObjectIterator
Many of the objects in the field of Polyhedral Geometry mask a BigObject
from Polymake.jl
. These big objects have properties which can easily be accessed via julia's dot syntax. The return commonly does not adhere to the mathematical or the typing conventions of Oscar
; many properties encode information about a collection of mathematical objects within a single data object.
The SubObjectIterator
is a precise and flexible tool to directly access and/or process the desired properties of any Polymake.BigObject
, but it requires specific interface definitions to work properly for each context. The user can thus profit from an easily understandable and usable iterator.
This guide is meant to communicate the application of the SubObjectIterator
for developers, utilizing existing code as reference and examples.
Creating a working SubObjectIterator
The formal definition of the SubObjectIterator
in src/PolyhedralGeometry/iterators
is:
struct SubObjectIterator{T} <: AbstractVector{T}
Obj::Polymake.BigObject
Acc::Function
n::Int
options::NamedTuple
end
An instance can be created by passing values for all fields, while options
is optional.
Trivially, Obj
is the Polymake.BigObject
whose property is to be accessed. The other fields will each be explained in an upcoming section.
Length
As an AbstractVector
, the SubObjectIterator
has a length. Due to the nature of Polymake.BigObject
s this length is constant for any property. Sometimes the length can easily be derived as a by-product of pre-computations when creating an instance of SubObjectIterator
. To avoid performing unnecessary computations afterwards, the value is set at construction in n
.
Access function
Optimally retrieving and converting the elements varies strongly between the contexts in which a SubObjectIterator
is created. Thus its getindex
method redirects the call to the (internal) function Acc
:
function Base.getindex(iter::SubObjectIterator{T}, i::Base.Integer) where T
@boundscheck 1 <= i && i <= iter.n
return iter.Acc(T, iter.Obj, i; iter.options...)
end
From this call we can see that the access function's signature needs to satisfy certain requirements for the SubObjectIterator
to work. The arguments are:
T
: The return type.iter.Obj
: ThePolymake.BigObject
whose property is to be accessed.i
: The index.iter.options
: Additional arguments. Will be explained later.
Let us look at an example how we can utilize this interface. The following is the implementation to access the rays of a Cone
:
rays(as::Type{RayVector{T}}, C::Cone) where T = SubObjectIterator{as}(pm_object(C), _ray_cone, n_rays(C))
_ray_cone(::Type{T}, C::Polymake.BigObject, i::Base.Integer) where T = T(C.RAYS[i, :])
Typing r = rays(RayVector{Polymake.Rational}, C)
with a Cone
C
returns a SubObjectIterator
over RayVector{Polymake.Rational}
elements of length n_rays(C)
with access function _ray_cone
. With the given method of this function, getindex(r, i)
returns a RayVector{Polymake.Rational}
constructed from the i-th
row of the property RAYS
of the Polymake.BigObject
.
The user does never directly create a SubObjectIterator
, so type restrictions made where it is created can be assumed to hold. In our example _ray_cone
will always be called with T<:RayVector
.
One can define several methods of the access function to ideally read and process data. Consider facets(as::Type{T}, C::Cone)
. Depending on the return type we offer three methods:
_facet_cone(::Type{T}, C::Polymake.BigObject, i::Base.Integer) where T<:Union{Polyhedron, AffineHalfspace} = T(-C.FACETS[[i], :], 0)
_facet_cone(::Type{LinearHalfspace}, C::Polymake.BigObject, i::Base.Integer) = LinearHalfspace(-C.FACETS[[i], :])
_facet_cone(::Type{Cone}, C::Polymake.BigObject, i::Base.Integer) = cone_from_inequalities(-C.FACETS[[i], :])
Additional Methods
The SubObjectIterator
can moreover be understood as a mathematical collection the sense that one can
- ask for specific information encoded in the data or
- use this collection as an argument for construction another mathematical object.
The first case is covered by adding methods to specific internal functions. Remember implementation of rays
discussed above. It makes sense to define a vector_matrix
method on its output, encoding the rays of the cone as a single matrix based on a convention applied throughout Oscar
. The function's implementation a user calls in this case is evaluated to these lines:
vector_matrix(iter::SubObjectIterator{<:AbstractVector{Polymake.Rational}}) = matrix(QQ, Matrix{QQFieldElem}(_vector_matrix(Val(iter.Acc), iter.Obj; iter.options...)))
vector_matrix(iter::SubObjectIterator{<:AbstractVector{Polymake.Integer}}) = matrix(ZZ, _vector_matrix(Val(iter.Acc), iter.Obj; iter.options...))
_vector_matrix(::Any, ::Polymake.BigObject) = throw(ArgumentError("Vector Matrix not defined in this context."))
Two functionalities are defined this way:
- The call of
vector_matrix(iter)
is redirected to_vector_matrix(Val(iter.Acc), iter.Obj)
. If that method is not defined for the value type of the access function, it falls back to throwing an error. - The matrix received from step 1 is converted from
Polymake.jl
format toOscar
format.
So by defining the following we have a fully functional vector_matrix
method in the context of rays
:
_vector_matrix(::Val{_ray_cone}, C::Polymake.BigObject) = C.RAYS
The second case is solved with defining a special _matrix_for_polymake
method. One just hast to name the internal function that returns the desired matrix. This way one has the ability to precisely control how the iterator works internally in specific contexts, even if there happen to be multiple additional matrix functions.
Again, the call matrix_for_polymake(iter)
will either redirect to the defined method or fall back to throwing an error if there is none:
function matrix_for_polymake(iter::SubObjectIterator)
if hasmethod(_matrix_for_polymake, Tuple{Val{iter.Acc}})
return _matrix_for_polymake(Val(iter.Acc))(Val(iter.Acc), iter.Obj; iter.options...)
else
throw(ArgumentError("Matrix for Polymake not defined in this context."))
end
end
For rays(C::Cone)
this reduces the implementation to the following line:
_matrix_for_polymake(::Val{_ray_cone}) = _vector_matrix
With matrix_for_polymake
the output of rays
can be handled as a usual matrix and constructors or other functions can easily be extended by additionally allowing SubObjectIterator
as an argument type. E.g. the signature of one of the Cone
constructors now looks like this while the body has not changed:
Cone(R::Union{SubObjectIterator{<:RayVector}, Oscar.MatElem, AbstractMatrix}, L::Union{SubObjectIterator{<:RayVector}, Oscar.MatElem, AbstractMatrix, Nothing} = nothing; non_redundant::Bool = false)
There also are linear_matrix_for_polymake
and affine_matrix_for_polymake
used in the context of linear and affine halfspaces/hyperplanes. Defining this functionality in a context works the same way as for matrix_for_polymake
; you can create a new method of _linear_matrix_for_polymake
or _affine_matrix_for_polymake
. It suffices to define the most relevant of these two; the other one will be derived, if possible. Also, halfspace_matrix_pair
is defined in terms of affine_matrix_for_polymake
, so this does not need another implementation.
The example code for rays(C::Cone)
has covered every line of the implementation by now, but we had different code in between, so let us summarize and take a look at what the whole implementation actually looks like:
rays(as::Type{RayVector{T}}, C::Cone) where T = SubObjectIterator{as}(pm_object(C), _ray_cone, n_rays(C))
_ray_cone(::Type{T}, C::Polymake.BigObject, i::Base.Integer) where T = T(C.RAYS[i, :])
_vector_matrix(::Val{_ray_cone}, C::Polymake.BigObject) = C.RAYS
_matrix_for_polymake(::Val{_ray_cone}) = _vector_matrix
options
Sometimes you need further arguments to specify the returned data. These arguments are set at construction of the SubObjectIterator
and later passed to the corresponding functions as keyword arguments.
A good example how to use this is faces(C::Cone, face_dim::Int)
. It is not enough to know that our SubObjectIterator
is set in the context of faces of cones; face_dim
will be relevant for any type of access occurring in the future.
function faces(C::Cone, face_dim::Int)
n = face_dim - length(lineality_space(C))
n < 1 && return nothing
return SubObjectIterator{Cone}(C.pm_cone, _face_cone, size(Polymake.polytope.faces_of_dim(pm_object(C), n), 1), (f_dim = n,))
end
When this method is called with meaningful input, it creates a SubObjectIterator
where the last argument is a NamedTuple
specifying that f_dim = n
. The information encoded in this NamedTuple
will be passed as keyword arguments when calling the access function or any additional method (reconsider their definitions). This allows us to directly ask for that data when implementing these methods:
function _face_cone(::Type{Cone}, C::Polymake.BigObject, i::Base.Integer; f_dim::Int = 0)
return Cone(Polymake.polytope.Cone(RAYS = C.RAYS[collect(Polymake.to_one_based_indexing(Polymake.polytope.faces_of_dim(C, f_dim)[i])), :], LINEALITY_SPACE = C.LINEALITY_SPACE))
end
function _ray_indices(::Val{_face_cone}, C::Polymake.BigObject; f_dim::Int = 0)
f = Polymake.to_one_based_indexing(Polymake.polytope.faces_of_dim(C, f_dim))
return IncidenceMatrix([collect(f[i]) for i in 1:length(f)])
end
Extending the interface
The additional methods offer an intuitive way of interaction for the user, but their current selection is not carved in stone. You can easily add more similar methods by extending the list that is iterated over to generate the code. Which list that is usually depends on the output format. vector_matrix
returns matrices with either integer or rational elements. The same capabilities hold for point_matrix
and generator_matrix
:
for (sym, name) in (("point_matrix", "Point Matrix"), ("vector_matrix", "Vector Matrix"), ("generator_matrix", "Generator Matrix"))
M = Symbol(sym)
_M = Symbol(string("_", sym))
@eval begin
$M(iter::SubObjectIterator{<:AbstractVector{Polymake.Rational}}) = matrix(QQ, Matrix{QQFieldElem}($_M(Val(iter.Acc), iter.Obj; iter.options...)))
$M(iter::SubObjectIterator{<:AbstractVector{Polymake.Integer}}) = matrix(ZZ, $_M(Val(iter.Acc), iter.Obj; iter.options...))
$_M(::Any, ::Polymake.BigObject) = throw(ArgumentError(string($name, " not defined in this context.")))
end
end
The second string (name
) of each pair determines the name that is printed in error messages.
If required, one can of course write completely new functions to extend the interface.