My team is building a CP-SAT solver that schedules assignments (think homework) over a period of days with variable availability (time available to do assignments). We're trying to speed up our model.
We've tried num_search_workers and other parameter tuning but want to check for other speed increases. The aim being to solve problems with ~100days and up to 2000 assignments in 5-10seconds (benchmarked on M1 mac). Any ideas?
Problem Description: Place a assignments across d days respecting these requirements
Assignment time on a day must not exceed that day's time available
Assignment dependencies should be respected (if A needs B then B should not occur after A)
Assignments can be split (in order to better fit across days with little time)
Optimize for diversity of assignment types on a day
Solving slows dramatically with # days and # assignments. This is expected but we'd like to know if you can suggest possible speedups
Here's an example unit test. Hopefully shows the splitting, ordering, and time constraints.
days = [{"secondsAvailable": 1200}, {"secondsAvailable": 1200}, {"secondsAvailable": 1200}, {"secondsAvailable": 1200}]
assignments = [
{"id": 1, "resourceType": "Type0", "seconds": 2400, "deps": [], "instances": 2},
{"id": 2, "resourceType": "Type0", "seconds": 1200, "deps": [1], "instances": 1},
{"id": 3, "resourceType": "Type0", "seconds": 1200, "deps": [1, 2], "instances": 1},
]
result = cp_sat.CP_SAT_FAST.schedule(days, assignments, options=solver_options)
# expect a list of lists where each inner list is a day with the included assignments
expected = shared.SolverOutput(feasible=True, solution=[
[{"id": 1, "resourceType": "Type0", "time": 1200, "instances": 2}],
[{"id": 1, "resourceType": "Type0", "time": 1200, "instances": 2}],
[{"id": 2, "resourceType": "Type0", "time": 1200, "instances": 1}],
[{"id": 3, "resourceType": "Type0", "time": 1200, "instances": 1}],
])
self.assertEqual(result, expected)
And here's the solver:
import math
from typing import List, Dict
from ortools.sat.python import cp_model
import numpy as np
import planner.solvers as solvers
from planner.shared import SolverOutput, SolverOptions
class CP_SAT_FAST(solvers.Solver):
"""
CP_SAT_FAST is a CP_SAT solver with speed optimizations and a time limit (passed in through options).
"""
#staticmethod
def schedule(days: List[Dict], assignments: List[Dict], options: SolverOptions) -> SolverOutput:
"""
Schedules a list of assignments on a studyplan of days
Arguments:
days: list of dicts containing available time for that day
assignments: list of assignments to place on schedule
"""
model = cp_model.CpModel()
num_assignments = len(assignments)
num_days = len(days)
# x[d, a] shows is assignment a is on day d
x = np.zeros((num_days, num_assignments), cp_model.IntVar)
# used for resource diversity optimization
total_resource_types = 4
unique_today = []
# upper and lower bounds used for dependency ordering (if a needs b then b must be before or on the day of a)
day_ub = {}
day_lb = {}
# track assignment splitting
instances = {}
assignment_times = {}
id_to_assignment = {}
for a, asm in enumerate(assignments):
# track upper and lower bounds
day_ub[a] = model.NewIntVar(0, num_days, "day_ub")
day_lb[a] = model.NewIntVar(0, num_days, "day_lb")
asm["ub"] = day_ub[a]
asm["lb"] = day_lb[a]
id_to_assignment[asm["id"]] = asm
max_instances = min(num_days, asm.get("instances", num_days))
# each assignment must occur at least once
instances[a] = model.NewIntVar(1, max_instances, f"instances_{a}")
model.AddHint(instances[a], max_instances)
# when split keep a decision variable of assignment time
assignment_times[a] = model.NewIntVar(asm.get("seconds") // max_instances, asm.get("seconds"), f"assignment_time_{a}")
model.AddDivisionEquality(assignment_times[a], asm.get("seconds"), instances[a])
for d in range(num_days):
time_available = days[d].get("secondsAvailable", 0)
if time_available <= 0:
# no assignments on zero-time days
model.Add(sum(x[d]) == 0)
else:
# track resource diversity on this day
type0_today = model.NewBoolVar(f"type0_on_{d}")
type1_today = model.NewBoolVar(f"type1_on_{d}")
type2_today = model.NewBoolVar(f"type2_on_{d}")
type3_today = model.NewBoolVar(f"type3_on_{d}")
types_today = model.NewIntVar(0, total_resource_types, f"unique_on_{d}")
task_times = []
for a, asm in enumerate(assignments):
# x[d, a] = True if assignment a is on day d
x[d, a] = model.NewBoolVar(f"x[{d},{a}]")
# set assignment upper and lower bounds for ordering
model.Add(day_ub[a] >= d).OnlyEnforceIf(x[d, a])
model.Add(day_lb[a] >= (num_days - d)).OnlyEnforceIf(x[d, a])
# track if a resource type is on a day for resource diversity optimization
resourceType = asm.get("resourceType")
if resourceType == "Type0":
model.AddImplication(x[d, a], type0_today)
elif resourceType == "Type1":
model.AddImplication(x[d, a], type1_today)
elif resourceType == "Type2":
model.AddImplication(x[d, a], type2_today)
elif resourceType == "Type3":
model.AddImplication(x[d, a], type3_today)
else:
raise RuntimeError(f"Unknown resource type {asm.get('resourceType')}")
# track of task time (considering splitting), for workload requirements
task_times.append(model.NewIntVar(0, asm.get("seconds"), f"time_{a}_on_{d}"))
model.Add(task_times[a] == assignment_times[a]).OnlyEnforceIf(x[d, a])
# time assigned to day d cannot exceed the day's available time
model.Add(time_available >= sum(task_times))
# sum the unique resource types on this day for later optimization
model.Add(sum([type0_today, type1_today, type2_today, type3_today]) == types_today)
unique_today.append(types_today)
"""
Resource Diversity:
Keeps track of what instances of a resource type appear on each day
and the minimum number of unique resource types on any day. (done above ^)
Then the model objective is set to maximize that minimum
"""
total_diversity = model.NewIntVar(0, num_days * total_resource_types, "total_diversity")
model.Add(sum(unique_today) == total_diversity)
avg_diversity = model.NewIntVar(0, total_resource_types, "avg_diversity")
model.AddDivisionEquality(avg_diversity, total_diversity, num_days)
# Set objective
model.Maximize(avg_diversity)
# Assignment Occurance/Splitting and Dependencies
for a, asm in enumerate(assignments):
# track how many times an assignment occurs (since we can split)
model.Add(instances[a] == sum(x[d, a] for d in range(num_days)))
# Dependencies
for needed_asm in asm.get("deps", []):
needed_ub = id_to_assignment[needed_asm]["ub"]
# this asm's lower bound must be greater than or equal to the upper bound of the dependency
model.Add(num_days - asm["lb"] >= needed_ub)
# Solve
solver = cp_model.CpSolver()
# set time limit
solver.parameters.max_time_in_seconds = float(options.time_limit)
solver.parameters.preferred_variable_order = 1
solver.parameters.initial_polarity = 0
# solver.parameters.stop_after_first_solution = True
# solver.parameters.num_search_workers = 8
intermediate_printer = SolutionPrinter()
status = solver.Solve(model, intermediate_printer)
print("\nStats")
print(f" - conflicts : {solver.NumConflicts()}")
print(f" - branches : {solver.NumBranches()}")
print(f" - wall time : {solver.WallTime()}s")
print()
if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE:
sp = []
for i, d in enumerate(days):
day_time = 0
days_tasks = []
for a, asm in enumerate(assignments):
if solver.Value(x[i, a]) >= 1:
asm_time = math.ceil(asm.get("seconds") / solver.Value(instances[a]))
day_time += asm_time
days_tasks.append({"id": asm["id"], "resourceType": asm.get("resourceType"), "time": asm_time, "instances": solver.Value(instances[a])})
sp.append(days_tasks)
return SolverOutput(feasible=True, solution=sp)
else:
return SolverOutput(feasible=False, solution=[])
class SolutionPrinter(cp_model.CpSolverSolutionCallback):
def __init__(self):
cp_model.CpSolverSolutionCallback.__init__(self)
self.__solution_count = 0
def on_solution_callback(self):
print(f"Solution {self.__solution_count} objective value = {self.ObjectiveValue()}")
self.__solution_count += 1
Before answering your actual question I want to point out a few things in your model that I suspect are not working as you intended.
The constraints on the assignment types present on a given day
model.AddImplication(x[d, a], type0_today)
etc., do enforce that type0_today == 1 if there is an assignment of that type on that day. However, it does not enforce that type0_today == 0 if there are no assignments of that type on that day. The solver is still free to choose type0_today == 1, and it will do so, because that fulfills this constraint and also directly increases the objective function. You will probably discover in the optimal solution to the test case you gave that all the type0_today to type3_today variables are 1 and that avg_diversity == 4 in the optimal solution, even though there are no assignments of any type but 0 in the input data. In the early stages of modelling, it's always a good idea to check the value of all the variables in the model for plausibility.
Since I don't have a Python installation, I translated your model to c# to be able to do some experiments. Sorry, you'll have to translate into the equivalent Python code. I reformulated the constraint on the type0_today variables to use an array type_today[d, t] (for day d and type t) and use the AddMaxEquality constraint, which for Boolean variables is equivalent to the logical OR of all the participating variables:
// For each day...
for (int d = 0; d < num_days; d++)
{
// ... make a list for each assignment type of all x[d, a] where a has that type.
List<IntVar>[] assignmentsByType = new List<IntVar>[total_resource_types];
for (int t = 0; t < total_resource_types; t++)
{
assignmentsByType[t] = new List<IntVar>();
}
for (int a = 0; a < num_assignments; a++)
{
int t = getType(assignments[a].resourceType);
assignmentsByType[t].Add(x[d, a]);
}
// Constrain the types present on the day to be the logical OR of assignments with that type on that day
for (int t = 0; t < total_resource_types; t++)
{
if (assignmentsByType[t].Count > 0)
{
model.AddMaxEquality(type_today[d, t], assignmentsByType[t]);
}
else
{
model.Add(type_today[d, t] == 0);
}
}
}
You compute the average diversity as
avg_diversity = model.NewIntVar(0, total_resource_types, "avg_diversity")
model.AddDivisionEquality(avg_diversity, total_diversity, num_days)
Since the solver only works with integer variables, avg_diversity will be exactly one of the values 0, 1, 2, 3 or 4 with no fractional part. The constraint AddDivisionEquality will also ensure that total_diversity is an exact integer multiple of both avg_diversity and num_days. This is a very strong restriction on the solutions and will lead to infeasibility in many cases that I don't think you intended.
For example, avg_diversity == 3, num_days == 20 and total_diversity == 60 would be an allowed solution, but total_diversity == 63 would not be allowed, although there are three days in that solution with higher diversity than in the one with total_diversity == 60.
Instead, I recommend that you eliminate the variable avg_diversity and its constraint and simply use total_diversity as your objective function. Since the number of days is a fixed constant during the solution, maximizing the total diversity will be equivalent without introducing artificial infeasibilities.
That said, here is my answer.
Generic constraint satisfaction problems are in general NP problems and should not be expected to scale well. Although many specific problem formulations can actually be solved quickly, small changes in the input data or the formulation can push the problem into a black hole of exponentiality. There is really no other approach than trying out various methods to see what works best with your exact problem.
Although it sounds paradoxical, it is easier for the solver to find optimal solutions for strongly constrained problems than for lightly constrained ones (assuming they are feasible!). The search space in a strongly constrained problem is smaller than in the lightly constrained one, so the solver has fewer choices about what to experiment with to optimize and therefore completes the job faster.
First suggestion
In your problem, you have variables day_ub and day_lb for each assignment. These have a range from 0 to num_days. The constraints on them
model.Add(day_ub[a] >= d).OnlyEnforceIf(x[d, a])
model.Add(day_lb[a] >= (num_days - d)).OnlyEnforceIf(x[d, a])
allow the solver freedom to choose any value between 0 and the largest d resp. largest (num_days - d) (inclusive). During the optimization, the solver probably spends time trying out different values for these variables but rarely discovers that it leads to an improvement; that would happen only when the placement of a dependent assignment would be changed.
You can eliminate the variables day_ub and day_lb and their constraints and instead formulate the dependencies directly with the x variables.
In my c# model I reformulated the assignment dependency constraint as follows:
for (int a = 0; a < num_assignments; a++)
{
Assignment assignment = assignments[a];
foreach (int predecessorIndex in getPredecessorAssignmentIndicesFor(assignment))
{
for (int d1 = 0; d1 < num_days; d1++)
{
for (int d2 = 0; d2 < d1; d2++)
{
model.AddImplication(x[d1, predecessorIndex], x[d2, a].Not());
}
}
}
}
In words: if an assignment B (predecessorIndex) on which assignment A (a) depends is placed on day d1, then all the x[0..d1, a] must be false. This directly relates the dependencies using the x variables insteading of introducing helping variables with additional freedom which bog down the solver. This change reduces the number of variables in the problem and increases the number of constraints, both of which help the solver.
In an experiment I did with 25 days and 35 assignments, checking the model stats showed
Original:
#Variables: 2020
#kIntDiv: 35
#kIntMax: 100
#kLinear1: 1750
#kLinear2: 914
#kLinearN: 86
Total constraints 2885
New formulation:
#Variables: 1950
#kBoolOr: 11700
#kIntDiv: 35
#kIntMax: 100
#kLinear2: 875
#kLinearN: 86
Total constraints 12796
So the new formulation has fewer variables but far more constraints.
The solution times in the experiment were improved, the solver took only 2,6 s to achieve total_diversity == 68 instead of over 90 s.
Original formulation
Time Objective
0,21 56
0,53 59
0,6 60
0,73 61
0,75 62
0,77 63
2,9 64
3,82 65
3,84 66
91,01 67
91,03 68
91,05 69
New formulation
Time Objective
0,2347 41
0,3066 42
0,4252 43
0,4602 44
0,5014 49
0,6437 50
0,6777 51
0,6948 52
0,7108 53
0,9593 54
1,0178 55
1,1535 56
1,2023 57
1,2351 58
1,2595 59
1,2874 60
1,3097 61
1,3325 62
1,388 63
1,5698 64
2,4948 65
2,5993 66
2,6198 67
2,6431 68
32,5665 69
Of course, the solution times you get will be strongly dependent on the input data.
Second suggestion
During my experiments I observed that solutions are found much more quickly when the assignments have a lot of dependencies. This is consistent with more highly constrained models being easier to solve.
If you often have assignments of the same type and duration (like the numbers 2 and 3 in your test data) and they both have instance == 1` and either no dependencies or the same ones, then exchanging their position in the solution will not improve the objective.
In a pre-processing step you could look for such duplicates and make one of them dependent on the other. This is essentially a symmetry-breaking constraint. This will prevent the solver from wasting time with an attempt to see if exchanging their positions would improve the objective.
Third suggestion
The solution needs to deal with determining how many instances of each assignment will be present in a solution. That requires two variables for each assignment instances[a] and assignment_times[a] with an associated constraint.
Instead of doing this, you could get rid of the variables instances[a] and assignment_times[a] and instead split assignments with instances > 1 into multiple assignments in a preprocessing step. For example, in your test data, assignment 1 would be split into two assignments 1_1 and 1_2 each having instances == 1 and seconds = 1200. For this test case where instances == 2 for assignment 1, this will not have any effect on the final solution-- maybe the solver will schedule 1_1 and 1_2 on the same day, maybe not, but the final result is equivalent to splitting or not but doesn't need the extra variables.
In the preprocessing step, when an assignment is split, you should add symmetry breaking constraints to make 1_2 dependent on 1_1, etc., for the reasons mentioned above.
When an assignment has instances > 2, splitting it into multiple assignments before the run is actually a change to the model. For example, if instances == 3 and seconds = 2400 you cannot get a solution in which the assignment is split over two days with 1200 s each; the solver will always be scheduling 3 assignments of 800 s each.
So this suggestion is actually a change to the model and you'll have to determine if that is acceptable or not.
The total diversity will usually be helped by having more instances of an assignment to place, so the change may not have large practical consequences. It would also allow scheduling 2/3 of an assignment on one day and the remaining 1/3 on another day, so it even adds some flexibility.
But this may or may not be acceptable in terms of your overall requirements.
In all cases, you'll have to test changes with your exact data to see if they really result in an improvement or not.
I hope this helps (and that that this is a real world problem and not a homework assignment, as I did spend a few hours investigating...).
I am using scipy (SLSQP method) for structural optimization (SciPy Version 1.6.1).
For each part, I have 13 design variables but only one used for the cost function (mass) - the other ones exist to fulfill certain inequality constraints. Since I do not know in the beginning, how many parts (PIDs) are to be optimized, I generate the optimization using iterators.
I define some inequality constraints as smooth functions because they describe the feasible domain, but also some inequality constraints that are defined as numpy arrays using a Taylor approximation:
def generate_ineq_list(PID_list,x,crit_list):
ineq_list = []
for i in np.arange(len(PID_list)):
f01 = -2*x[i*13]**2 + x[i*13+2] + 1
...
f25 = 4*(x[i*13+9] + 1)*(x[i*13+1] + 1) - (x[i*13+1] + 1)**4 - 3*(x[i*13+5])**2
ineq_list.append([f01,...,f25])
V1A = x[i*13]
V3A = x[i*13+2]
V1D = x[i*13+8]
V3D = x[i*13+10]
H = x[i*13+12]
# Now iterating over all defined criteria (crit), parts (PID) and variables to
# set up a Taylor approximation.
# The values with _0 denote the last starting point resp. the point where
# the gradients were calculated using the finite difference method. These values
# are taken from a pandas dataframe which is dispensed here for the
# sake of visibility
SF = -2.0 + results_old[crit]['sf'].values + \
gradient[PID]['V1A'][crit]['sf'] * (V1A - V1A_0) + \
gradient[PID]['V3A'][crit]['sf'] * (V3A - V3A_0) + \
gradient[PID]['V1D'][crit]['sf'] * (V1D - V1D_0) + \
gradient[PID]['V3D'][crit]['sf'] * (V3D - V3D_0) + \
gradient[PID]['H'][crit]['sf'] * (H - H_0)
ineq_list.append(list(SF))
ineq_list_flattended = list(itertools.chain(*ineq_list))
return ineq_list_flattened
The result for f01 is a float while the result for SF is a numpy array (which is turned into a list). So what I did was to set up a list where the results are stored to (a list of lists) and then finally flattened into a single list with only floats. This list is then returned to the optimizer as inequality constraint:
ineq_functions = lambda x: np.array(generate_ineq_list(self.PID_list,x,crit_list))
ineq_cons = {'type' : 'ineq',
'fun' : ineq_functions,
}
Now when I execute the optimizer with
res = minimize(cost_function_f_,
x0,
method='SLSQP',
constraints = [ineq_cons,
eq_cons],
options = {'ftol' : 1e-9,
'disp' : True,
'iprint':2,
},
bounds = bounds,
)
I can observe that the first 25 equations (f01-f25) of the inequality constraints are within their domain, but the values of SF are invalid, for instance they have values of e.g. -0.9.
Does anyone know what I am doing wrong, not considering, misunderstanding?
Any help and/or ideas would be highly appreciated. Thanks in advance!
This is my first post so I tried to keep everything as basic and simple as possible, I hope I did it right :)
I am trying to simulate a battery dispatch models with charging and discharging constraints. The BESS is charging from a Solar PV system. When I run the model currently, there are some time periods when the BESS is charging and discharging at the same time. How can I add a flag such that when Charge >, Discharge =0 and vice-versa.
def market_constraintx0(model, t):
return (model.Charge[t] <= df.loc[t,'PVGeneration']*stripeff)
model.market_rulex0 = Constraint(model.T, rule=market_constraintx0)
def market_constraintx1(model, t):
return (model.Charge[t] + model.RegDown[t] <= model.ChargeMax)
model.market_rulex1 = Constraint(model.T, rule=market_constraintx1)
def market_constraintx2(model, t):
return ( model.Discharge[t] + model.RegUp[t] <= model.DischargeMax)
model.market_rulex2 = Constraint(model.T, rule=market_constraintx2)
def charge_soc(model, t):
return model.RegUp[t] + model.Discharge[t] <= model.SoC[t] * stripeff ###Battery discharge and regup capacity is limited by SOC
model.charge_soc = Constraint(model.T, rule=charge_soc)
def discharge_soc(model, t):
return model.RegDown[t] + model.Charge[t] <= (model.SoCmax - model.SoC[t])/stripeff ### Battery can be charged by the amount of capacity left to charge.
model.discharge_soc = Constraint(model.T, rule=discharge_soc)
The constraint
x >= 0 or y >= 0
is sometimes called a complementarity condition. It can also be written as:
x * y = 0
(I assume x and y are non-negative variables). There are different ways to solve this:
complementarity solver. Some solvers support this kind of constraints directly. Complementarity constraints inside a math programming model is known as MPEC (Mathematical Programming with Equilibrium Constraints). So these solvers are sometimes called MPEC solvers.
nonlinear formulation. The constraint x*y=0 is not very easy, but a global solver should be able to handle this reliably. However these solvers only handle relatively small models (compared to local solvers).
discrete formulation. Formulate the OR condition using binary variables or a SOS1 construct. This is especially useful if the rest of the model is linear.
You may want to look into pyomo.mpec. For further information see link.
If you want to stick to (mixed-integer) linear formulations, you can also look for indicator constraints, which are discussed generally in this question. Some solvers like CPLEX and Gurobi seem to have specific constraint types for indicator constraints, but I'm not familiar with how to use those within Pyomo.
In general, you can get similar functionality by using a "Big M" formulation. In your case, something like:
model.Indicator = Var(model.T, within=Binary)
model.M = Param(initialize=1000)
def charge_indicator_constraint(model, t):
return model.M * model.Indicator[t] >= model.Charge[t]
...
def discharge_indicator_constraint(model, t):
return (1 - model.M) * model.Indicator >= model.Discharge[t]
...
As the discussed in the question I linked to, picking the right value of model.M is important to keep your model formulation "tight", and in your case, you would probably tie it directly to the power rating of your BESS.
I am working on some very large scale linear programming problems. (Matrices are currently roughly 1000x1000 and these are the 'mini' ones.)
I thought that I had the program running successfully, only I have realized that I am getting some very unintuitive answers. For example, let's say I were to maximize x+y+z subject to a set of constraints x+y<10 and y+z <5. I run this and get an optimal solution. Then, I run the same equation but with different constraints: x+y<20 and y+z<5. Yet in the second iteration, my maximization decreases!
I have painstakingly gone through and assured myself that the constraints are loading correctly.
Does anyone know what the problem might be?
I found something in the documentation about lpx_check_kkt which seems to tell you when your solution is likely to be correct or high confidence (or low confidence for that matter), but I don't know how to use it.
I made an attempt and got the error message lpx_check_kkt not defined.
I am adding some code as an addendum in hopes that someone can find an error.
The result of this is that it claims an optimal solution has been found. And yet every time I raise an upper bound, it gets less optimal.
I have confirmed that my bounds are going up and not down.
size = 10000000+1
ia = intArray(size)
ja = intArray(size)
ar = doubleArray(size)
prob = glp_create_prob()
glp_set_prob_name(prob, "sample")
glp_set_obj_dir(prob, GLP_MAX)
glp_add_rows(prob, Num_constraints)
for x in range(Num_constraints):
Variables.add_variables(Constraints_for_simplex)
glp_set_row_name(prob, x+1, Variables.variers[x])
glp_set_row_bnds(prob, x+1, GLP_UP, 0, Constraints_for_simplex[x][1])
print 'we set the row_bnd for', x+1,' to ',Constraints_for_simplex[x][1]
glp_add_cols(prob, len(All_Loops))
for x in range(len(All_Loops)):
glp_set_col_name(prob, x+1, "".join(["x",str(x)]))
glp_set_col_bnds(prob,x+1,GLP_LO,0,0)
glp_set_obj_coef(prob,x+1,1)
for x in range(1,len(All_Loops)+1):
z=Constraints_for_simplex[0][0][x-1]
ia[x] = 1; ja[x] = x; ar[x] = z
x=len(All_Loops)+1
while x<Num_constraints + len(All_Loops):
for y in range(2, Num_constraints+1):
z=Constraints_for_simplex[y-1][0][0]
ia[x] = y; ja[x] =1 ; ar[x] = z
x+=1
x=Num_constraints+len(All_Loops)
while x <len(All_Loops)*(Num_constraints-1):
for z in range(2,len(All_Loops)+1):
for y in range(2,Num_constraints+1):
if x<len(All_Loops)*Num_constraints+1:
q = Constraints_for_simplex[y-1][0][z-1]
ia[x] = y ; ja[x]=z; ar[x] = q
x+=1
glp_load_matrix(prob, len(All_Loops)*Num_constraints, ia, ja, ar)
glp_exact(prob,None)
Z = glp_get_obj_val(prob)
Start by solving your problematic instances with different solvers and checking the objective function value. If you can export your model to .mps format (I don't know how to do this with GLPK, sorry), you can upload the mps file to http://www.neos-server.org/neos/solvers/index.html and solve it with several different LP solvers.