Skip to content

Commit 2075ce6

Browse files
add support for serializing linear programs over non-rational fields (#5246)
Co-authored-by: Antony Della Vecchia <vecchia@math.tu-berlin.de> Co-authored-by: antonydellavecchia <antonydellavecchia@gmail.com>
1 parent 1ab253e commit 2075ce6

7 files changed

Lines changed: 144 additions & 12 deletions

File tree

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ LazyArtifacts = "1.6"
4040
Markdown = "1.6"
4141
Nemo = "0.51.1"
4242
Pkg = "1.6"
43-
Polymake = "0.13.0"
43+
Polymake = "0.13.1"
4444
ProgressMeter = "1.10.2"
4545
Random = "1.6"
4646
RandomExtensions = "0.4.3"

src/PolyhedralGeometry/Optimization/linear_program.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ function linear_program(
3030
throw(ArgumentError("convention must be set to :min or :max."))
3131
end
3232
ambDim = ambient_dim(P)
33+
objective = coefficient_field(P).(objective)
3334
size(objective, 1) == ambDim || error("objective has wrong dimension.")
3435
lp = Polymake.polytope.LinearProgram{_scalar_type_to_polymake(T)}(;
3536
LINEAR_OBJECTIVE=homogenize(objective, k)

src/PolyhedralGeometry/Optimization/mixed_integer_linear_program.jl

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ function mixed_integer_linear_program(
3737
integer_variables = 1:ambDim
3838
end
3939
size(objective, 1) == ambDim || error("objective has wrong dimension.")
40+
objective = coefficient_field(P).(objective)
4041
milp = Polymake.polytope.MixedIntegerLinearProgram{_scalar_type_to_polymake(T)}(;
4142
LINEAR_OBJECTIVE=homogenize(objective, k), INTEGER_VARIABLES=Vector(integer_variables)
4243
)
@@ -128,8 +129,9 @@ function objective_function(
128129
milp::MixedIntegerLinearProgram{T}; as::Symbol=:pair
129130
) where {T<:scalar_types}
130131
if as == :pair
131-
return Vector{T}(milp.polymake_milp.LINEAR_OBJECTIVE[2:end]),
132-
convert(T, milp.polymake_milp.LINEAR_OBJECTIVE[1])
132+
cf = coefficient_field(milp)
133+
return T[cf(x) for x in milp.polymake_milp.LINEAR_OBJECTIVE[2:end]],
134+
cf.(milp.polymake_milp.LINEAR_OBJECTIVE[1])
133135
elseif as == :function
134136
(c, k) = objective_function(milp; as=:pair)
135137
return x -> sum(x .* c) + k

src/PolyhedralGeometry/Polyhedron/constructors.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ Get the underlying polymake `Polytope`.
175175
pm_object(P::Polyhedron) = P.pm_polytope
176176

177177
function ==(P0::Polyhedron{T}, P1::Polyhedron{T}) where {T<:scalar_types}
178+
@req coefficient_field(P0) == coefficient_field(P1) "Cannot compare polyhedra over different coefficient fields."
178179
Polymake.polytope.equal_polyhedra(pm_object(P0), pm_object(P1))
179180
end
180181

src/Serialization/PolyhedralGeometry.jl

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ end
3434

3535
type_params(obj::T) where {S <: Union{QQFieldElem, Float64}, T <: PolyhedralObject{S}} = TypeParams(T, coefficient_field(obj))
3636

37+
type_params(obj::T) where {S <: Union{QQFieldElem, Float64}, T <: LinearProgram{S}} = TypeParams(T, coefficient_field(obj))
38+
type_params(obj::T) where {S <: Union{QQFieldElem, Float64}, T <: MixedIntegerLinearProgram{S}} = TypeParams(T, coefficient_field(obj))
39+
3740
function type_params(obj::T) where {S, T <: PolyhedralObject{S}}
3841
p_dict = _polyhedral_object_as_dict(obj)
3942
field = p_dict[:_coeff]
@@ -44,15 +47,19 @@ function type_params(obj::T) where {S, T <: PolyhedralObject{S}}
4447
:pm_params => type_params(p_dict))
4548
end
4649

50+
function type_params(obj::T) where {S, T <: Union{LinearProgram{S}, MixedIntegerLinearProgram{S}}}
51+
par = params(type_params(feasible_region(obj)))
52+
return TypeParams(
53+
T,
54+
par...)
55+
end
56+
57+
4758
function save_object(s::SerializerState, obj::PolyhedralObject{S}) where S <: Union{QQFieldElem, Float64}
4859
save_object(s, pm_object(obj))
4960
end
5061

5162
function save_object(s::SerializerState, obj::PolyhedralObject{<:FieldElem})
52-
if typeof(obj) <: Union{MixedIntegerLinearProgram, LinearProgram}
53-
T = typeof(obj)
54-
error("Unsupported type $T for serialization")
55-
end
5663
p_dict = _polyhedral_object_as_dict(obj)
5764
delete!(p_dict, :_coeff)
5865
save_data_dict(s) do
@@ -105,6 +112,15 @@ function save_object(s::SerializerState, lp::LinearProgram{QQFieldElem})
105112
end
106113
end
107114

115+
function save_object(s::SerializerState, lp::LinearProgram{<:FieldElem})
116+
lpcoeffs = lp.polymake_lp.LINEAR_OBJECTIVE
117+
save_data_dict(s) do
118+
save_object(s, lp.feasible_region, :feasible_region)
119+
save_object(s, lp.convention, :convention)
120+
save_object(s, _pmdata_for_oscar(lpcoeffs, coefficient_field(lp)), :lpcoeffs)
121+
end
122+
end
123+
108124
function save_object(s::SerializerState{<: LPSerializer}, lp::LinearProgram{QQFieldElem})
109125
lp_filename = basepath(s.serializer) * "-$(objectid(lp)).lp"
110126
save_lp(lp_filename, lp)
@@ -134,6 +150,28 @@ function load_object(s::DeserializerState, ::Type{<:LinearProgram}, field::QQFie
134150
return LinearProgram{coeff_type}(fr, lp, Symbol(conv))
135151
end
136152

153+
function load_object(s::DeserializerState, ::Type{<:LinearProgram}, params::Dict)
154+
if s.obj isa String
155+
error("Loading this file requires using the LPSerializer")
156+
end
157+
field = params[:field]
158+
coeff_type = elem_type(field)
159+
fr = load_object(s, Polyhedron, params, :feasible_region)
160+
conv = load_object(s, String, :convention)
161+
lpcoeffs = load_object(s, Vector{coeff_type}, field, :lpcoeffs)
162+
all = Polymake._lookup_multi(pm_object(fr), "LP")
163+
lp = nothing
164+
for i in 1:length(all)
165+
lo = _pmdata_for_oscar(all[i].LINEAR_OBJECTIVE, field)
166+
if lpcoeffs == lo
167+
lp = all[i]
168+
break
169+
end
170+
end
171+
@req lp !== nothing "could not identify LP subobject"
172+
return LinearProgram{coeff_type}(fr, lp, Symbol(conv), field)
173+
end
174+
137175
function load_object(s::DeserializerState{LPSerializer},
138176
::Type{<:LinearProgram}, field::QQField)
139177
load_node(s) do _
@@ -158,7 +196,19 @@ function save_object(s::SerializerState, milp::MixedIntegerLinearProgram{QQField
158196
save_object(s, milp.feasible_region, :feasible_region)
159197
save_object(s, milp.convention, :convention)
160198
save_json(s, coeffs_jsonstr, :milp_coeffs)
161-
save_json(s, int_vars_jsonstr, :int_vars)
199+
# we can probably changes this line to just store a vector{Int} but will need upgrade
200+
save_json(s, int_vars_jsonstr, :int_vars)
201+
end
202+
end
203+
204+
function save_object(s::SerializerState, milp::MixedIntegerLinearProgram{<:FieldElem})
205+
milp_coeffs = milp.polymake_milp.LINEAR_OBJECTIVE
206+
int_vars = milp.polymake_milp.INTEGER_VARIABLES
207+
save_data_dict(s) do
208+
save_object(s, milp.feasible_region, :feasible_region)
209+
save_object(s, milp.convention, :convention)
210+
save_object(s, _pmdata_for_oscar(milp_coeffs, coefficient_field(milp)), :milp_coeffs)
211+
save_object(s, _pmdata_for_oscar(int_vars, QQ), :int_vars)
162212
end
163213
end
164214

@@ -193,6 +243,26 @@ function load_object(s::DeserializerState, ::Type{<: MixedIntegerLinearProgram},
193243
return MixedIntegerLinearProgram{T}(fr, lp, Symbol(conv), field)
194244
end
195245

246+
function load_object(s::DeserializerState, ::Type{<: MixedIntegerLinearProgram}, params::Dict)
247+
conv = load_object(s, String, :convention)
248+
field = params[:field]
249+
coeff_type = elem_type(field)
250+
fr = load_object(s, Polyhedron, params, :feasible_region)
251+
milp_coeffs = load_object(s, Vector{coeff_type}, field, :milp_coeffs)
252+
int_vars = load_object(s, Vector{Int}, :int_vars)
253+
254+
all = Polymake._lookup_multi(pm_object(fr), "MILP")
255+
index = 0
256+
for i in 1:length(all)
257+
if _pmdata_for_oscar(all[i].LINEAR_OBJECTIVE, field) == milp_coeffs && all[i].INTEGER_VARIABLES == Set(int_vars)
258+
index = i
259+
break
260+
end
261+
end
262+
milp = Polymake._lookup_multi(pm_object(fr), "MILP", index-1)
263+
return MixedIntegerLinearProgram{coeff_type}(fr, milp, Symbol(conv), field)
264+
end
265+
196266
# use generic serialization for the other types:
197267
@register_serialization_type Cone
198268
@register_serialization_type PolyhedralComplex

src/Serialization/polymake.jl

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,14 +116,22 @@ _pmdata_for_oscar(s::Polymake.Set, coeff::Field) = Set(_pmdata_for_oscar(e, coef
116116

117117
function _bigobject_to_dict(bo::Polymake.BigObject, coeff::Field, parent_key::String="")
118118
data = Dict{Symbol,Any}()
119+
bot = Polymake.bigobject_type(bo)
119120
for pname in Polymake.list_properties(bo)
120121
p = Polymake.give(bo, pname)
121122
key_str = parent_key == "" ? pname : parent_key * "." * pname
122123
if p isa Polymake.PropertyValue
123124
@debug "missing c++ mapping: skipping $pname of type $(Polymake.typeinfo_string(p, true))"
124125
elseif p isa Polymake.BigObject
125-
obj = _bigobject_to_dict(p, coeff, key_str)
126-
merge!(data, obj)
126+
if Polymake.bigobject_prop_is_multiple(bot, pname)
127+
arr = Polymake._lookup_multi(bo, pname)
128+
# this must be without parent key (since will be stored below a parent key)
129+
d = Oscar.Serialization._bigobject_to_dict.(arr, Ref(coeff))
130+
data[Symbol(pname)] = Tuple(d)
131+
else
132+
obj = _bigobject_to_dict(p, coeff, key_str)
133+
merge!(data, obj)
134+
end
127135
else
128136
try
129137
obj = _pmdata_for_oscar(p, coeff)
@@ -155,13 +163,20 @@ end
155163

156164
function _load_bigobject_from_dict!(obj::Polymake.BigObject, dict::Dict, parent_key::String="")
157165
delay_loading = Tuple{Symbol,Any}[]
166+
bot = Polymake.bigobject_type(obj)
158167
for (k, v) in dict
159168
# keys of dict are symbols
160169
k = string(k)
161170
key_str = parent_key == "" ? k : parent_key * "." * k
162171
first(k) == '_' && continue
163-
164-
if v isa Dict
172+
if v isa Tuple && Polymake.bigobject_prop_is_multiple(bot, key_str)
173+
subobjtype = Polymake.bigobject_prop_type(bot, key_str)
174+
for e in v
175+
subobj = Polymake.BigObject(subobjtype)
176+
Polymake.add(obj, key_str, subobj)
177+
_load_bigobject_from_dict!(subobj, e, "") # the parent string is empty because we assign to the subobject
178+
end
179+
elseif v isa Dict
165180
_load_bigobject_from_dict!(obj, v, key_str)
166181
else
167182
pmv = convert(Polymake.PolymakeType, v)

test/Serialization/PolyhedralGeometry.jl

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,21 @@ using Oscar: _integer_variables
137137
@test objective_function(LP) == objective_function(loaded)
138138
@test feasible_region(LP) == feasible_region(loaded)
139139
end
140+
141+
P = dodecahedron()
142+
F = coefficient_field(P)
143+
a = gen(number_field(F))
144+
LP1 = linear_program(P, F.([3, -2, 4]);k=2,convention = :min)
145+
LP2 = linear_program(P, F.([-1, a - 2, a + 5]);k=2,convention = :min)
146+
ov1 = optimal_value(LP1)
147+
ov2 = optimal_value(LP2)
148+
test_save_load_roundtrip(path, [LP1, LP2]) do (loaded1, loaded2)
149+
@test objective_function(LP1) == objective_function(loaded1)
150+
@test feasible_region(LP1) == feasible_region(loaded1)
151+
@test ov1 == optimal_value(loaded1)
152+
@test ov2 == optimal_value(loaded2)
153+
@test feasible_region(LP1) == feasible_region(LP2)
154+
end
140155
end
141156

142157
@testset "MixedIntegerLinearProgram" begin
@@ -153,6 +168,34 @@ using Oscar: _integer_variables
153168
@test feasible_region(MILP) == feasible_region(loaded)
154169
@test Oscar._integer_variables(MILP) == Oscar._integer_variables(loaded)
155170
end
171+
172+
P = dodecahedron()
173+
F = coefficient_field(P)
174+
a = gen(number_field(F))
175+
176+
MILP1 = mixed_integer_linear_program(
177+
P,
178+
[3,-2, a];
179+
k=2,
180+
convention = :min,
181+
integer_variables=[1, 2]
182+
)
183+
184+
MILP2 = mixed_integer_linear_program(
185+
P,
186+
[-3 * a,-2, 3];
187+
k=2,
188+
convention = :max,
189+
integer_variables=[1, 2]
190+
)
191+
192+
test_save_load_roundtrip(path, [MILP1, MILP2]) do (loaded1, loaded2)
193+
@test objective_function(MILP1) == objective_function(loaded1)
194+
@test feasible_region(MILP1) == feasible_region(loaded1)
195+
@test feasible_region(MILP1) == feasible_region(loaded2)
196+
@test Oscar._integer_variables(MILP1) == Oscar._integer_variables(loaded1)
197+
@test Oscar._integer_variables(MILP2) == Oscar._integer_variables(loaded2)
198+
end
156199
end
157200

158201
@testset "SubdivisionOfPoints" begin

0 commit comments

Comments
 (0)