diff --git a/examples/python/hidato_sat.py b/examples/python/hidato_sat.py index dac3a2efbe..c96d18c3e6 100644 --- a/examples/python/hidato_sat.py +++ b/examples/python/hidato_sat.py @@ -14,8 +14,8 @@ from __future__ import print_function -from ortools.sat.python import cp_model from ortools.sat.python import visualization +from ortools.sat.python import cp_model def build_pairs(rows, cols): @@ -31,10 +31,13 @@ def build_pairs(rows, cols): rows: the number of rows in the grid cols: the number of columns in the grid """ - return [(x * cols + y, (x + dx) * cols + (y + dy)) for x in range(rows) - for y in range(cols) for dx in (-1, 0, 1) for dy in (-1, 0, 1) - if (x + dx >= 0 and x + dx < rows and y + dy >= 0 and y + dy < cols - and (dx != 0 or dy != 0))] + return [ + (x * cols + y, (x + dx) * cols + (y + dy)) for x in range(rows) + for y in range(cols) for dx in (-1, 0, 1) + for dy in (-1, 0, 1) + if (x + dx >= 0 and x + dx < rows and y + dy >= 0 and y + dy < cols and + (dx != 0 or dy != 0)) + ] def print_solution(positions, rows, cols): diff --git a/examples/python/jobshop_ft06_distance_sat.py b/examples/python/jobshop_ft06_distance_sat.py index d1c0b49b04..06c12ab0c0 100644 --- a/examples/python/jobshop_ft06_distance_sat.py +++ b/examples/python/jobshop_ft06_distance_sat.py @@ -90,8 +90,7 @@ def jobshop_ft06_distance(): start_lit = model.NewBoolVar('%i is first job' % j1) arcs.append([0, j1 + 1, start_lit]) # Final arc from an arc to the dummy node. - arcs.append([j1 + 1, 0, model.NewBoolVar( - '%i is last job' % j1)]) + arcs.append([j1 + 1, 0, model.NewBoolVar('%i is last job' % j1)]) for j2 in range(len(job_intervals)): if j1 == j2: @@ -103,8 +102,8 @@ def jobshop_ft06_distance(): # We add the reified precedence to link the literal with the # times of the two tasks. min_distance = distance_between_jobs(j1, j2) - model.Add(job_starts[j2] >= - job_ends[j1] + min_distance).OnlyEnforceIf(lit) + model.Add(job_starts[j2] >= job_ends[j1] + + min_distance).OnlyEnforceIf(lit) model.AddCircuit(arcs) diff --git a/examples/python/jobshop_ft06_sat.py b/examples/python/jobshop_ft06_sat.py index b832c95740..fa1fcbd0a9 100644 --- a/examples/python/jobshop_ft06_sat.py +++ b/examples/python/jobshop_ft06_sat.py @@ -10,18 +10,27 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""ft06 jobshop using the CP-SAT solver.""" +"""This model implements a simple jobshop named ft06. +A jobshop is a standard scheduling problem when you must sequence a +series of task_types on a set of machines. Each job contains one task_type per +machine. The order of execution and the length of each job on each +machine is task_type dependent. + +The objective is to minimize the maximum completion time of all +jobs. This is called the makespan. +""" from __future__ import print_function import collections -from ortools.sat.python import cp_model + from ortools.sat.python import visualization +from ortools.sat.python import cp_model -def main(): +def jobshop_ft06(): """Solves the ft06 jobshop.""" - # Creates the model. + # Creates the solver. model = cp_model.CpModel() machines_count = 6 @@ -38,7 +47,7 @@ def main(): # Computes horizon dynamically. horizon = sum([sum(durations[i]) for i in all_jobs]) - Task = collections.namedtuple('Task', 'start end interval') + task_type = collections.namedtuple('task_type', 'start end interval') # Creates jobs. all_tasks = {} @@ -49,7 +58,7 @@ def main(): end_var = model.NewIntVar(0, horizon, 'end_%i_%i' % (i, j)) interval_var = model.NewIntervalVar(start_var, duration, end_var, 'interval_%i_%i' % (i, j)) - all_tasks[(i, j)] = Task( + all_tasks[(i, j)] = task_type( start=start_var, end=end_var, interval=interval_var) # Create disjuctive constraints. @@ -89,5 +98,4 @@ def main(): print('Optimal makespan: %i' % solver.ObjectiveValue()) -if __name__ == '__main__': - main() +jobshop_ft06() diff --git a/examples/python/shift_scheduling_sat.py b/examples/python/shift_scheduling_sat.py index 0de1ada719..25f269a78f 100644 --- a/examples/python/shift_scheduling_sat.py +++ b/examples/python/shift_scheduling_sat.py @@ -15,12 +15,11 @@ from __future__ import print_function import argparse -import time - -from google.protobuf import text_format from ortools.sat.python import cp_model +from google.protobuf import text_format + PARSER = argparse.ArgumentParser() PARSER.add_argument( '--output_proto', @@ -30,48 +29,23 @@ PARSER.add_argument( PARSER.add_argument('--params', default="", help='Sat solver parameters.') -class ObjectiveSolutionPrinter(cp_model.CpSolverSolutionCallback): - """Print intermediate solutions objective and time.""" +def negated_bounded_span(works, start, length): + """Filters an isolated sub-sequence of variables assined to True. - def __init__(self): - cp_model.CpSolverSolutionCallback.__init__(self) - self.__solution_count = 0 - self.__start_time = time.time() + Extract the span of Boolean variables [start, start + length), negate them, + and if there is variables to the left/right of this span, surround the span by + them in non negated form. - def on_solution_callback(self): - """Called on each new solution.""" - current_time = time.time() - objective = self.ObjectiveValue() - print('Solution %i, time = %f s, objective = [%i, %i]' % - (self.__solution_count, current_time - self.__start_time, - objective, self.BestObjectiveBound())) - self.__solution_count += 1 + Args: + works: a list of variables to extract the span from. + start: the start to the span. + length: the length of the span. - def solution_count(self): - """Returns the number of solutions found.""" - return self.__solution_count - - -def negated_bounded_span(works, start, length, right): - """Filters a sub-sequence of works with left and right borders. - - This methods will create an array of variables that will match a span of - works variables assigned to one starting at 'start' with the given length. - - If right is True, it will also checks that the a variable assigned to False - terminates the span. - - Args: - works: a list of variables to extract the span from. - start: the start to the span. - length: the end of the span. - right: indicates if we need to check termination of the span. - - Returns: - a list of variables which conjunction will be false if the sub-list is - assigned to True, and correctly bounded by variables assigned to False, - or by the start or end of works. - """ + Returns: + a list of variables which conjunction will be false if the sub-list is + assigned to True, and correctly bounded by variables assigned to False, + or by the start or end of works. + """ sequence = [] # Left border (start of works, or works[start - 1]) if start > 0: @@ -79,56 +53,55 @@ def negated_bounded_span(works, start, length, right): for i in range(length): sequence.append(works[start + i].Not()) # Right border (end of works or works[start + length]) - if start + length < len(works) and right: + if start + length < len(works): sequence.append(works[start + length]) return sequence -def add_sequence_constraint(model, works, hard_min, soft_min, min_cost, - soft_max, hard_max, max_cost, basename): +def add_soft_sequence_constraint(model, works, hard_min, soft_min, min_cost, + soft_max, hard_max, max_cost, prefix): """Sequence constraint on true variables with soft and hard bounds. - This constraint look at every maximal contiguous sequence of variables - assigned to true. If forbids sequence of length < hard_min or > hard_max. - Then it creates penalty terms if the length is < soft_min or > soft_max. + This constraint look at every maximal contiguous sequence of variables + assigned to true. If forbids sequence of length < hard_min or > hard_max. + Then it creates penalty terms if the length is < soft_min or > soft_max. - Args: - model: the sequence constraint is built on this model. - works: a list of Boolean variables. - hard_min: any sequence of true variables must have a length of at least - hard_min. - soft_min: any sequence should have a length of at least soft_min, or a - linear penalty on the delta will be added to the objective. - min_cost: the coefficient of the linear penalty if the length is less - than soft_min. - soft_max: any sequence should have a length of at most soft_max, or a - linear penalty on the delta will be added to the objective. - hard_max: any sequence of true variables must have a length of at most - hard_max. - max_cost: the coefficient of the linear penalty if the length is more - than soft_max. - basename: a base name for penalty literals. + Args: + model: the sequence constraint is built on this model. + works: a list of Boolean variables. + hard_min: any sequence of true variables must have a length of at least + hard_min. + soft_min: any sequence should have a length of at least soft_min, or a + linear penalty on the delta will be added to the objective. + min_cost: the coefficient of the linear penalty if the length is less than + soft_min. + soft_max: any sequence should have a length of at most soft_max, or a linear + penalty on the delta will be added to the objective. + hard_max: any sequence of true variables must have a length of at most + hard_max. + max_cost: the coefficient of the linear penalty if the length is more than + soft_max. + prefix: a base name for penalty literals. - Returns: - a tuple (variables_list, coefficient_list) containing the different - penalties created by the sequence constraint. - """ + Returns: + a tuple (variables_list, coefficient_list) containing the different + penalties created by the sequence constraint. + """ cost_literals = [] cost_coefficients = [] # Forbid sequences that are too short. for length in range(1, hard_min): for start in range(len(works) - length - 1): - model.AddBoolOr(negated_bounded_span(works, start, length, True)) + model.AddBoolOr(negated_bounded_span(works, start, length)) # Penalize sequences that are below the soft limit. if min_cost > 0: for length in range(hard_min, soft_min): for start in range(len(works) - length - 1): - span = negated_bounded_span(works, start, length, True) - name = basename + ': under_span(start=%i, length=%i)' % ( - start, length) - lit = model.NewBoolVar(name) + span = negated_bounded_span(works, start, length) + name = ': under_span(start=%i, length=%i)' % (start, length) + lit = model.NewBoolVar(prefix + name) span.append(lit) model.AddBoolOr(span) cost_literals.append(lit) @@ -140,16 +113,14 @@ def add_sequence_constraint(model, works, hard_min, soft_min, min_cost, if max_cost > 0: for length in range(soft_max + 1, hard_max + 1): for start in range(len(works) - length - 1): - span = negated_bounded_span(works, start, length, False) - name = basename + ': over_span(start=%i, length=%i)' % (start, - length) - lit = model.NewBoolVar(name) + span = negated_bounded_span(works, start, length) + name = ': over_span(start=%i, length=%i)' % (start, length) + lit = model.NewBoolVar(prefix + name) span.append(lit) model.AddBoolOr(span) cost_literals.append(lit) - # We will penalize all sequences with length > soft_max. - # Thus we count the penalty once per possible length. - cost_coefficients.append(max_cost) + # Cost paid is max_cost * excess length. + cost_coefficients.append(max_cost * (length - soft_max)) # Just forbid any sequence of true variables with length hard_max + 1 for start in range(len(works) - hard_max - 1): @@ -158,35 +129,35 @@ def add_sequence_constraint(model, works, hard_min, soft_min, min_cost, return cost_literals, cost_coefficients -def add_weekly_sum_constraint(model, works, hard_min, soft_min, min_cost, - soft_max, hard_max, max_cost, basename): +def add_soft_sum_constraint(model, works, hard_min, soft_min, min_cost, + soft_max, hard_max, max_cost, prefix): """Sum constraint with soft and hard bounds. - This constraint counts the variables assigned to true from works. - If forbids sum < hard_min or > hard_max. - Then it creates penalty terms if the sum is < soft_min or > soft_max. + This constraint counts the variables assigned to true from works. + If forbids sum < hard_min or > hard_max. + Then it creates penalty terms if the sum is < soft_min or > soft_max. - Args: - model: the sequence constraint is built on this model. - works: a list of Boolean variables. - hard_min: any sequence of true variables must have a sum of at least - hard_min. - soft_min: any sequence should have a sum of at least soft_min, or a - linear penalty on the delta will be added to the objective. - min_cost: the coefficient of the linear penalty if the sum is less than - soft_min. - soft_max: any sequence should have a sum of at most soft_max, or a - linear penalty on the delta will be added to the objective. - hard_max: any sequence of true variables must have a sum of at most - hard_max. - max_cost: the coefficient of the linear penalty if the sum is more than - soft_max. - basename: a base name for penalty variables. + Args: + model: the sequence constraint is built on this model. + works: a list of Boolean variables. + hard_min: any sequence of true variables must have a sum of at least + hard_min. + soft_min: any sequence should have a sum of at least soft_min, or a linear + penalty on the delta will be added to the objective. + min_cost: the coefficient of the linear penalty if the sum is less than + soft_min. + soft_max: any sequence should have a sum of at most soft_max, or a linear + penalty on the delta will be added to the objective. + hard_max: any sequence of true variables must have a sum of at most + hard_max. + max_cost: the coefficient of the linear penalty if the sum is more than + soft_max. + prefix: a base name for penalty variables. - Returns: - a tuple (variables_list, coefficient_list) containing the different - penalties created by the sequence constraint. - """ + Returns: + a tuple (variables_list, coefficient_list) containing the different + penalties created by the sequence constraint. + """ cost_variables = [] cost_coefficients = [] sum_var = model.NewIntVar(hard_min, hard_max, '') @@ -195,9 +166,10 @@ def add_weekly_sum_constraint(model, works, hard_min, soft_min, min_cost, # Penalize sums below the soft_min target. if soft_min > hard_min and min_cost > 0: - delta = model.NewIntVar(-7, 7, '') + delta = model.NewIntVar(-len(works), len(works), '') model.Add(delta == soft_min - sum_var) - excess = model.NewIntVar(0, 7, basename + ': under_sum') + # TODO(user): Compare efficiency with only excess >= soft_min - sum_var. + excess = model.NewIntVar(0, 7, prefix + ': under_sum') model.AddMaxEquality(excess, [delta, 0]) cost_variables.append(excess) cost_coefficients.append(min_cost) @@ -206,7 +178,7 @@ def add_weekly_sum_constraint(model, works, hard_min, soft_min, min_cost, if soft_max < hard_max and max_cost > 0: delta = model.NewIntVar(-7, 7, '') model.Add(delta == sum_var - soft_max) - excess = model.NewIntVar(0, 7, basename + ': over_sum') + excess = model.NewIntVar(0, 7, prefix + ': over_sum') model.AddMaxEquality(excess, [delta, 0]) cost_variables.append(excess) cost_coefficients.append(max_cost) @@ -221,38 +193,36 @@ def solve_shift_scheduling(params, output_proto): num_weeks = 3 shifts = ['O', 'M', 'A', 'N'] - # Fixed assignment: (employee, day, shift). + # Fixed assignment: (employee, shift, day). # This fixes the first 2 days of the schedule. fixed_assignments = [ (0, 0, 0), (1, 0, 0), - (2, 0, 1), - (3, 0, 1), - (4, 0, 2), - (5, 0, 2), - (6, 0, 2), - (7, 0, 3), + (2, 1, 0), + (3, 1, 0), + (4, 2, 0), + (5, 2, 0), + (6, 2, 3), + (7, 3, 0), (0, 1, 1), (1, 1, 1), - (2, 1, 2), - (3, 1, 2), - (4, 1, 2), - (5, 1, 0), - (6, 1, 0), - (7, 1, 3), + (2, 2, 1), + (3, 2, 1), + (4, 2, 1), + (5, 0, 1), + (6, 0, 1), + (7, 3, 1), ] - # Request: (employee, day, shift, weight) - positive_requests = [ + # Request: (employee, shift, day, weight) + # A negative weight indicates that the employee desire this assignment. + requests = [ # Employee 3 wants the first Saturday off. - (3, 5, 0, 2), + (3, 0, 5, -2), # Employee 5 wants a night shift on the second Thursday. - (5, 10, 3, 2) - ] - - negative_requests = [ + (5, 3, 10, -2), # Employee 2 does not want a night shift on the third Friday. - (2, 4, 3, 4) + (2, 3, 4, 4) ] # Shift constraints on continuous sequence : @@ -303,7 +273,6 @@ def solve_shift_scheduling(params, output_proto): num_days = num_weeks * 7 num_shifts = len(shifts) - # Create model. model = cp_model.CpModel() work = {} @@ -324,24 +293,20 @@ def solve_shift_scheduling(params, output_proto): model.Add(sum(work[e, s, d] for s in range(num_shifts)) == 1) # Fixed assignments. - for e, d, s in fixed_assignments: + for e, s, d in fixed_assignments: model.Add(work[e, s, d] == 1) # Employee requests - for e, d, s, w in positive_requests: + for e, s, d, w in requests: obj_bool_vars.append(work[e, s, d]) - obj_bool_coeffs.append(-w) # We gain 'w' is the shift is selected. - - for e, d, s, w in negative_requests: - obj_bool_vars.append(work[e, s, d]) - obj_bool_coeffs.append(w) # We loose 'w' is the shift is selected. + obj_bool_coeffs.append(w) # Shift constraints for ct in shift_constraints: shift, hard_min, soft_min, min_cost, soft_max, hard_max, max_cost = ct for e in range(num_employees): works = [work[e, shift, d] for d in range(num_days)] - variables, coeffs = add_sequence_constraint( + variables, coeffs = add_soft_sequence_constraint( model, works, hard_min, soft_min, min_cost, soft_max, hard_max, max_cost, 'shift_constraint(employee %i, shift %i)' % (e, shift)) @@ -354,7 +319,7 @@ def solve_shift_scheduling(params, output_proto): for e in range(num_employees): for w in range(num_weeks): works = [work[e, shift, d + w * 7] for d in range(7)] - variables, coeffs = add_weekly_sum_constraint( + variables, coeffs = add_soft_sum_constraint( model, works, hard_min, soft_min, min_cost, soft_max, hard_max, max_cost, 'weekly_sum_constraint(employee %i, shift %i, week %i)' % @@ -415,7 +380,7 @@ def solve_shift_scheduling(params, output_proto): solver = cp_model.CpSolver() if params: text_format.Merge(params, solver.parameters) - solution_printer = ObjectiveSolutionPrinter() + solution_printer = cp_model.ObjectiveSolutionPrinter() status = solver.SolveWithSolutionCallback(model, solution_printer) # Print solution. @@ -453,7 +418,6 @@ def solve_shift_scheduling(params, output_proto): print(' - conflicts : %i' % solver.NumConflicts()) print(' - branches : %i' % solver.NumBranches()) print(' - wall time : %f ms' % solver.WallTime()) - print(' - solutions found : %i' % solution_printer.solution_count()) def main(args): diff --git a/examples/python/sudoku_sat.py b/examples/python/sudoku_sat.py index 70a44e86a0..c1dd516548 100644 --- a/examples/python/sudoku_sat.py +++ b/examples/python/sudoku_sat.py @@ -18,6 +18,7 @@ from ortools.sat.python import cp_model def solve_sudoku(): + """Solves the sudoku problem with the CP-SAT solver.""" # Create the model. model = cp_model.CpModel() @@ -67,7 +68,7 @@ def solve_sudoku(): status = solver.Solve(model) if status == cp_model.FEASIBLE: for i in line: - print([solver.Value(grid[(i, j)]) for j in line]) + print([int(solver.Value(grid[(i, j)])) for j in line]) solve_sudoku() diff --git a/examples/python/zebra_sat.py b/examples/python/zebra_sat.py index 18cc89af0b..80c504c167 100644 --- a/examples/python/zebra_sat.py +++ b/examples/python/zebra_sat.py @@ -31,7 +31,6 @@ The Norwegian lives next to the blue house. Who owns a zebra and who drinks water? """ - from __future__ import print_function from ortools.sat.python import cp_model diff --git a/ortools/sat/python/cp_model.py b/ortools/sat/python/cp_model.py index 4cf1810e42..8469d5001c 100644 --- a/ortools/sat/python/cp_model.py +++ b/ortools/sat/python/cp_model.py @@ -22,6 +22,7 @@ from __future__ import print_function import collections import numbers +import time from six import iteritems from ortools.sat import cp_model_pb2 @@ -1315,8 +1316,8 @@ class CpModel(object): return pywrapsat.SatHelper.ModelStats(self.__model) def Validate(self): - """Returns a non empty string is the model is not valid.""" - return pywrapsat.SatHelper.ValidateModel(self.__model) + """Returns a string explaining the issue is the model is not valid.""" + return pywrapsat.SatHelper.ValidateModel(self.__model) def AssertIsBooleanVariable(self, x): if isinstance(x, IntVar): @@ -1346,8 +1347,7 @@ def EvaluateLinearExpression(expression, solution): elif isinstance(expr, IntVar): value += coef * solution.solution[expr.Index()] elif isinstance(expr, _NotBooleanVariable): - raise TypeError( - 'Cannot interpret literals in a linear expression.') + raise TypeError('Cannot interpret literals in a linear expression.') return value @@ -1367,81 +1367,6 @@ def EvaluateBooleanExpression(literal, solution): 'Cannot interpret %s as a boolean expression.' % literal) -class CpSolverSolutionCallback(pywrapsat.SolutionCallback): - """Solution callback. - - This class implements a callback that will be called at each new solution - found during search. - - The method OnSolutionCallback() will be called by the solver, and must be - implemented. The current solution can be queried using the BooleanValue() - and Value() methods. - """ - - def OnSolutionCallback(self): - """Proxy to the same method with different naming convention.""" - self.on_solution_callback() - - - def BooleanValue(self, lit): - """Returns the boolean value of a boolean literal. - - Args: - lit: A boolean variable or its negation. - - Returns: - The boolean value of the literal in the solution. - - Raises: - RuntimeError: if 'lit' is not a boolean variable or its negation. - """ - if not self.Response().solution: - raise RuntimeError('Solve() has not be called.') - if isinstance(lit, numbers.Integral): - return bool(lit) - elif isinstance(lit, IntVar) or isinstance(lit, _NotBooleanVariable): - index = lit.Index() - return self.SolutionBooleanValue(index) - else: - raise TypeError( - 'Cannot interpret %s as a boolean expression.' % lit) - - def Value(self, expression): - """Evaluates an linear expression in the current solution. - - Args: - expression: a linear expression of the model. - - Returns: - An integer value equal to the evaluation of the linear expression - against the current solution. - - Raises: - RuntimeError: if 'expression' is not a LinearExpression. - """ - if not self.Response().solution: - raise RuntimeError('Solve() has not be called.') - if isinstance(expression, numbers.Integral): - return expression - value = 0 - to_process = [(expression, 1)] - while to_process: - expr, coef = to_process.pop() - if isinstance(expr, _ProductCst): - to_process.append((expr.Expression(), - coef * expr.Coefficient())) - elif isinstance(expr, _SumArray): - for e in expr.Array(): - to_process.append((e, coef)) - value += expr.Constant() * coef - elif isinstance(expr, IntVar): - value += coef * self.SolutionIntegerValue(expr.Index()) - elif isinstance(expr, _NotBooleanVariable): - raise TypeError( - 'Cannot interpret literals in a linear expression.') - return value - - class CpSolver(object): """Main solver class. @@ -1544,3 +1469,95 @@ class CpSolver(object): def ResponseStats(self): """Returns some statistics on the solution found as a string.""" return pywrapsat.SatHelper.SolverResponseStats(self.__solution) + + +class CpSolverSolutionCallback(pywrapsat.SolutionCallback): + """Solution callback. + + This class implements a callback that will be called at each new solution + found during search. + + The method OnSolutionCallback() will be called by the solver, and must be + implemented. The current solution can be queried using the BooleanValue() + and Value() methods. + """ + + def OnSolutionCallback(self): + """Proxy to the same method in snake case.""" + self.on_solution_callback() + + def BooleanValue(self, lit): + """Returns the boolean value of a boolean literal. + + Args: + lit: A boolean variable or its negation. + + Returns: + The boolean value of the literal in the solution. + + Raises: + RuntimeError: if 'lit' is not a boolean variable or its negation. + """ + if not self.Response().solution: + raise RuntimeError('Solve() has not be called.') + if isinstance(lit, numbers.Integral): + return bool(lit) + elif isinstance(lit, IntVar) or isinstance(lit, _NotBooleanVariable): + index = lit.Index() + return self.SolutionBooleanValue(index) + else: + raise TypeError( + 'Cannot interpret %s as a boolean expression.' % lit) + + def Value(self, expression): + """Evaluates an linear expression in the current solution. + + Args: + expression: a linear expression of the model. + + Returns: + An integer value equal to the evaluation of the linear expression + against the current solution. + + Raises: + RuntimeError: if 'expression' is not a LinearExpression. + """ + if not self.Response().solution: + raise RuntimeError('Solve() has not be called.') + if isinstance(expression, numbers.Integral): + return expression + value = 0 + to_process = [(expression, 1)] + while to_process: + expr, coef = to_process.pop() + if isinstance(expr, _ProductCst): + to_process.append((expr.Expression(), + coef * expr.Coefficient())) + elif isinstance(expr, _SumArray): + for e in expr.Array(): + to_process.append((e, coef)) + value += expr.Constant() * coef + elif isinstance(expr, IntVar): + value += coef * self.SolutionIntegerValue(expr.Index()) + elif isinstance(expr, _NotBooleanVariable): + raise TypeError( + 'Cannot interpret literals in a linear expression.') + return value + + +class ObjectiveSolutionPrinter(CpSolverSolutionCallback): + """Print intermediate solutions objective and time.""" + + def __init__(self): + CpSolverSolutionCallback.__init__(self) + self.__solution_count = 0 + self.__start_time = time.time() + + def on_solution_callback(self): + """Called on each new solution.""" + current_time = time.time() + objective = self.ObjectiveValue() + print('Solution %i, time = %f s, objective = [%i, %i]' % + (self.__solution_count, current_time - self.__start_time, + objective, self.BestObjectiveBound())) + self.__solution_count += 1 diff --git a/ortools/sat/python/visualization.py b/ortools/sat/python/visualization.py index cabbfe78dc..bfe21bd694 100644 --- a/ortools/sat/python/visualization.py +++ b/ortools/sat/python/visualization.py @@ -12,6 +12,7 @@ # limitations under the License. """Collection of helpers to visualize cp_model solutions in colab.""" +# pylint: disable=g-import-not-at-top import random try: from IPython.display import display diff --git a/ortools/sat/swig_helper.h b/ortools/sat/swig_helper.h index f3027f6736..fe4aa50ce3 100644 --- a/ortools/sat/swig_helper.h +++ b/ortools/sat/swig_helper.h @@ -179,11 +179,12 @@ class SatHelper { return CpSolverResponseStats(response); } - // Returns a non empty string the model is not valid. - static std::string ValidateModel(const operations_research::sat::CpModelProto& model_proto) { + // Returns a non empty std::string explaining the issue if the model is not + // valid. + static std::string ValidateModel( + const operations_research::sat::CpModelProto& model_proto) { return ValidateCpModel(model_proto); } - }; } // namespace sat