Exemple #1
0
class TestIsFixedCallCount(unittest.TestCase):
    """ Tests for PR#1402 (669e7b2b) """
    def setup(self, skip_trivial_constraints):
        m = ConcreteModel()
        m.x = Var()
        m.y = Var()
        m.c1 = Constraint(expr=m.x + m.y == 1)
        m.c2 = Constraint(expr=m.x <= 1)
        self.assertFalse(m.c2.has_lb())
        self.assertTrue(m.c2.has_ub())
        self._model = m

        self._opt = SolverFactory("cplex_persistent")
        self._opt.set_instance(
            self._model, skip_trivial_constraints=skip_trivial_constraints)

    def test_skip_trivial_and_call_count_for_fixed_con_is_one(self):
        self.setup(skip_trivial_constraints=True)
        self._model.x.fix(1)
        self.assertTrue(self._opt._skip_trivial_constraints)
        self.assertTrue(self._model.c2.body.is_fixed())

        with unittest.mock.patch(
                "pyomo.solvers.plugins.solvers.cplex_direct.is_fixed",
                wraps=is_fixed) as mock_is_fixed:
            self.assertEqual(mock_is_fixed.call_count, 0)
            self._opt.add_constraint(self._model.c2)
            self.assertEqual(mock_is_fixed.call_count, 1)

    def test_skip_trivial_and_call_count_for_unfixed_con_is_two(self):
        self.setup(skip_trivial_constraints=True)
        self.assertTrue(self._opt._skip_trivial_constraints)
        self.assertFalse(self._model.c2.body.is_fixed())

        with unittest.mock.patch(
                "pyomo.solvers.plugins.solvers.cplex_direct.is_fixed",
                wraps=is_fixed) as mock_is_fixed:
            self.assertEqual(mock_is_fixed.call_count, 0)
            self._opt.add_constraint(self._model.c2)
            self.assertEqual(mock_is_fixed.call_count, 2)

    def test_skip_trivial_and_call_count_for_unfixed_equality_con_is_three(
            self):
        self.setup(skip_trivial_constraints=True)
        self._model.c2 = Constraint(expr=self._model.x == 1)
        self.assertTrue(self._opt._skip_trivial_constraints)
        self.assertFalse(self._model.c2.body.is_fixed())

        with unittest.mock.patch(
                "pyomo.solvers.plugins.solvers.cplex_direct.is_fixed",
                wraps=is_fixed) as mock_is_fixed:
            self.assertEqual(mock_is_fixed.call_count, 0)
            self._opt.add_constraint(self._model.c2)
            self.assertEqual(mock_is_fixed.call_count, 3)

    def test_dont_skip_trivial_and_call_count_for_fixed_con_is_one(self):
        self.setup(skip_trivial_constraints=False)
        self._model.x.fix(1)
        self.assertFalse(self._opt._skip_trivial_constraints)
        self.assertTrue(self._model.c2.body.is_fixed())

        with unittest.mock.patch(
                "pyomo.solvers.plugins.solvers.cplex_direct.is_fixed",
                wraps=is_fixed) as mock_is_fixed:
            self.assertEqual(mock_is_fixed.call_count, 0)
            self._opt.add_constraint(self._model.c2)
            self.assertEqual(mock_is_fixed.call_count, 1)

    def test_dont_skip_trivial_and_call_count_for_unfixed_con_is_one(self):
        self.setup(skip_trivial_constraints=False)
        self.assertFalse(self._opt._skip_trivial_constraints)
        self.assertFalse(self._model.c2.body.is_fixed())

        with unittest.mock.patch(
                "pyomo.solvers.plugins.solvers.cplex_direct.is_fixed",
                wraps=is_fixed) as mock_is_fixed:
            self.assertEqual(mock_is_fixed.call_count, 0)
            self._opt.add_constraint(self._model.c2)
            self.assertEqual(mock_is_fixed.call_count, 1)
Exemple #2
0
class TeamOrienteeringIlp:
    def __init__(self,
                 num_teams,
                 vertex_reward,
                 edge_cost,
                 max_edge_cost,
                 max_vertices,
                 type_coverage=None,
                 min_type_coverage=None,
                 min_avg_type_conservation=None,
                 lazy_subtour_elimination=False,
                 solver='gurobi_persistent'):

        self.logger = logging.getLogger(self.__class__.__name__)

        if num_teams < 1:
            raise ValueError('at least one team needed')

        self._model = None
        self._result = None
        self._solver = None
        self._team_max_vertices_constraints = []
        self._team_max_edge_cost_constraints = []
        self._lazy_subtour_elimination = lazy_subtour_elimination
        self._solver_type = solver
        self._vertex_reward = vertex_reward
        self._num_teams = num_teams

        self._max_edge_cost = max_edge_cost
        self._max_vertices = max_vertices
        self._type_coverage = type_coverage
        self._min_type_coverage = min_type_coverage
        self._min_avg_type_conservation = min_avg_type_conservation

        if isinstance(edge_cost, dict):
            # in this case, the edge costs is a dictionary (u, v) -> cost
            self.logger.debug('Using sparse mode to build model')
            self._is_graph_sparse = True
        else:
            # in this case, we have a matrix (array of arrays)
            self.logger.debug('Using dense mode to build model')
            self._is_graph_sparse = False
        self._edge_cost = edge_cost

    def build_model(self):
        self.logger.info('Building model...')

        self._model = aml.ConcreteModel()

        # model parameters
        self.logger.debug('Adding graph objects...')
        self._model.TeamCount = aml.Param(initialize=self._num_teams,
                                          mutable=True)
        self._model.MaxEdgeCost = aml.Param(initialize=self._max_edge_cost,
                                            mutable=True)
        self._model.MaxVertexCount = aml.Param(initialize=self._max_vertices,
                                               mutable=True)

        self._model.Teams = aml.RangeSet(0, self._num_teams - 1)
        self._model.Nodes = aml.RangeSet(0, len(self._vertex_reward) - 1)
        self._model.r = aml.Param(
            self._model.Nodes,
            initialize=lambda model, n: self._vertex_reward[n])

        if self._is_graph_sparse:
            nodes_in, nodes_out = defaultdict(list), defaultdict(list)
            for u, v in self._edge_cost:
                nodes_in[v].append(u)
                nodes_out[u].append(v)

            self._model.Edges = aml.Set(initialize=self._edge_cost.keys())
            self._model.NodesIn = aml.Set(
                self._model.Nodes,
                initialize=lambda model, node: nodes_in[node])
            self._model.NodesOut = aml.Set(
                self._model.Nodes,
                initialize=lambda model, node: nodes_out[node])
            self._model.d = aml.Param(
                self._model.Edges,
                initialize=lambda model, u, v: self._edge_cost[(u, v)])
        else:
            self._model.Edges = aml.Set(initialize=self._model.Nodes *
                                        self._model.Nodes,
                                        filter=lambda model, u, v: u != v)
            self._model.d = aml.Param(
                self._model.Edges,
                initialize=lambda model, u, v: self._edge_cost[u][v])

        # indicator variables for nodes and arcs
        self._model.x = aml.Var(self._model.Edges * self._model.Teams,
                                domain=aml.Binary,
                                initialize=0)
        self._model.y = aml.Var(self._model.Nodes * self._model.Teams,
                                domain=aml.Binary,
                                initialize=0)

        # objective of the model: maximize reward collected from visited nodes
        self._model.Objective = aml.Objective(
            rule=lambda model: sum(model.y[n, t] * model.r[n]
                                   for n in model.Nodes for t in model.Teams),
            sense=aml.maximize)

        # every team must leave and come back
        self.logger.debug('Adding leave and return constraints...')
        self._model.TeamsLeave = aml.Constraint(rule=lambda model: sum(
            model.x[(u, v), t] for u, v in model.Edges for t in model.Teams
            if u == 0) == model.TeamCount)
        self._model.TeamsReturn = aml.Constraint(rule=lambda model: sum(
            model.x[(u, v), t] for u, v in model.Edges for t in model.Teams
            if v == 0) == model.TeamCount)

        # every vertex must be visited at most once
        self.logger.debug('Adding visit count constraint...')
        self._model.VertexVisit = aml.Constraint(
            (n for n in self._model.Nodes if n != 0),
            rule=lambda model, node: sum(model.y[node, t]
                                         for t in model.Teams) <= 1)

        # incoming connnections = outgoing connections = node selected
        # i.e. no sources or sinks (implies path is connected)
        #      enforces consistency between x and y (i.e. node touched by arcs if and only if it is selected)
        #      and at most one path passes from the node
        self.logger.debug(
            'Adding consistency and connectedness constraints...')
        if self._is_graph_sparse:

            def in_rule(model, node, team):
                return sum(model.x[(v, node), team]
                           for v in model.NodesIn[node]) == model.y[node, team]

            def out_rule(model, node, team):
                return sum(model.x[(node, v), team]
                           for v in model.NodesOut[node]) == model.y[node,
                                                                     team]
        else:

            def in_rule(model, node, team):
                return sum(model.x[(node, v), team] for v in model.Nodes
                           if v != node) == model.y[node, team]

            def out_rule(model, node, team):
                return sum(model.x[(v, node), team] for v in model.Nodes
                           if v != node) == model.y[node, team]

        self._model.Incoming = aml.Constraint(
            ((n, t) for n in self._model.Nodes for t in self._model.Teams),
            rule=in_rule)
        self._model.Outgoing = aml.Constraint(
            ((n, t) for n in self._model.Nodes for t in self._model.Teams),
            rule=out_rule)

        # subtour elimination, if required
        if not self._lazy_subtour_elimination:
            self.logger.debug('Adding subtour elimination constraints...')
            self._model.u = aml.Var(self._model.Nodes * self._model.Teams,
                                    bounds=(1.0, len(self._model.Nodes) - 1))
            self._model.SubTour = aml.Constraint(
                ((u, v, t) for u, v in self._model.Edges
                 for t in self._model.Teams if u != 0 and v != 0),
                rule=lambda model, u, v, t:
                (model.u[u, t] - model.u[v, t] + 1 <= (len(model.Nodes) - 1) *
                 (1 - model.x[(u, v), t])))

        if self._type_coverage and (self._min_type_coverage
                                    or self._min_avg_type_conservation):
            assert (not self._min_type_coverage
                    or not self._min_avg_type_conservation
                    or len(self._min_avg_type_conservation) == len(
                        self._min_type_coverage))

            self.logger.debug('Adding coverage information...')
            # type_coverage is a binary tensor s.t. C_ijk = 1 iff vertex j covers option k of type i
            # min_type_coverage is a vector s.t. c_i is the minimum options of type i that the vaccine must cover
            self._model.Types = aml.RangeSet(0, len(self._type_coverage) - 1)
            self._model.Options = aml.RangeSet(
                0,
                len(self._type_coverage[0][0]) - 1)
            self._model.TypeCoverage = aml.Param(
                self._model.Types * self._model.Nodes * self._model.Options,
                initialize=lambda model, t, n, o: self._type_coverage[t][n][o])

            # indicator variable 1 iff at least one team visits at least one vertex of option i of type j
            self._model.OptionCovered = aml.Var(self._model.Types *
                                                self._model.Options,
                                                domain=aml.Binary,
                                                initialize=0)
            self._model.OptionCoveredConstraint = aml.Constraint(
                self._model.Types * self._model.Options,
                rule=lambda model, typ, option: sum(
                    model.y[n, t] * model.TypeCoverage[typ, n, option]
                    for n in model.Nodes
                    for t in model.Teams) >= model.OptionCovered[typ, option])

            self.logger.debug(
                'Enforcing minimum coverage and/or conservation with %d types and %d options per type',
                len(self._type_coverage), len(self._type_coverage[0][0]))

            if self._min_type_coverage:
                self.logger.debug('Adding minimum coverage for each type...')
                # sum of above indicator variables must be at least the minimum option coverage for each type
                self._model.MinOptionCoverage = aml.Param(
                    self._model.Types,
                    initialize=lambda model, typ: self._min_type_coverage[typ])
                self._model.MinOptionCoverageConstraint = aml.Constraint(
                    self._model.Types,
                    rule=lambda model, typ:
                    (model.MinOptionCoverage[typ],
                     sum(model.OptionCovered[typ, o]
                         for o in model.Options), None))

            if self._min_avg_type_conservation:
                # every vertex must cover a minimum number of different options
                self.logger.debug('Adding average vertex conservation...')
                self._model.MinOptionConservation = aml.Param(
                    self._model.Types,
                    initialize=lambda model, typ: self.
                    _min_avg_type_conservation[typ])
                self._model.MinOptionConservationConstraint = aml.Constraint(
                    self._model.Types,
                    rule=lambda model, typ: sum(model.y[n, t] * (
                        sum(model.TypeCoverage[typ, n, o] for o in model.
                            Options) - model.MinOptionConservation[typ])
                                                for t in model.Teams
                                                for n in model.Nodes) >= 0)
        else:
            self.logger.info('No coverage enforced')

        self._solver = SolverFactory(self._solver_type)
        self._solver.set_instance(self._model)

        self.update_max_vertices(self._max_vertices)
        self.update_max_edge_cost(self._max_edge_cost)

        self.logger.info('Model build!')
        return self

    def update_max_vertices(self, max_vertices):
        def get_constraint(team):
            if max_vertices > 0:
                return pmo.constraint(
                    expr=sum(self._model.y[n, team] for n in self._model.Nodes
                             if n != 0) <= self._model.MaxVertexCount)
            else:
                return None

        self._max_vertices = max_vertices
        self._model.MaxVertexCount.set_value(max_vertices)
        self._team_max_vertices_constraints = self._update_constraint_for_all_teams(
            self._team_max_vertices_constraints, get_constraint,
            'MaxVerticesForTeam%d')

        if max_vertices < 0:
            self.logger.info('No maximum vertex count enforced.')
        else:
            self.logger.info('Maximum vertex count for each tour is %d',
                             self._max_vertices)

    def update_max_edge_cost(self, max_edge_cost):
        def get_constraint(team):
            if max_edge_cost > 0:
                return pmo.constraint(expr=sum(
                    self._model.x[(u, v), team] * self._model.d[u, v]
                    for u, v in self._model.Edges) <= self._model.MaxEdgeCost)
            else:
                return None

        self._max_edge_cost = max_edge_cost
        self._model.MaxEdgeCost.set_value(max_edge_cost)
        self._team_max_edge_cost_constraints = self._update_constraint_for_all_teams(
            self._team_max_edge_cost_constraints, get_constraint,
            'MaxEdgeCostForTeam%d')

        if max_edge_cost > 0:
            self.logger.info('Maximum edge cost for each tour is %f',
                             self._max_edge_cost)
        else:
            self.logger.info('No maximum edge cost enforced.')

    def _update_constraint_for_all_teams(self, current_constraints,
                                         constraint_fn, name_fmt):
        for name in current_constraints:
            constr = getattr(self._model, name)
            try:
                self._solver.remove_constraint(constr)
            except KeyError:
                # happens after model is built, but not solved. not sure why
                pass

            setattr(self._model, name, None)
            del constr

        new_constraints = []
        for team in range(self._num_teams):
            name = name_fmt % team
            constr = constraint_fn(team)
            if constr is None:
                continue
            setattr(self._model, name, constr)
            new_constraints.append(name)
            self._solver.add_constraint(constr)
        return new_constraints

    def solve(self, options=None):
        # if logging is configured, gurobipy will print messages there *and* on stdout
        # so we silence its logger and redirect all stdout to our own logger
        logging.getLogger('gurobipy.gurobipy').disabled = True

        class LoggingStdOut:
            def __init__(self):
                self.logger = logging.getLogger('stdout')

            def write(self, message):
                self.logger.debug(message.rstrip())

            def flush(self, *args, **kwargs):
                pass

        sys.stdout = LoggingStdOut()

        try:
            return self._solve(options)
        except Exception:
            # restore stdout so that handlers can print normally
            # https://docs.python.org/3/library/sys.html#sys.__stdout__
            sys.stdout = sys.__stdout__
            raise
        finally:
            sys.stdout = sys.__stdout__

    def _solve(self, options=None):
        ''' solves the model optimally
        '''

        self.logger.info('Solving started')
        if self._model is None:
            raise RuntimeError('must call build_model before solve')
        self._subtour_constraints = 0

        while True:
            res = self._solver.solve(options=options or {},
                                     tee=1,
                                     save_results=False,
                                     report_timing=True)
            if res.solver.termination_condition != TerminationCondition.optimal:
                raise RuntimeError(
                    'Could not solve problem - %s . Please check your settings'
                    % res.Solution.status)

            self._solver.load_vars()
            team_tours_dict = self._extract_solution_from_model()
            self.logger.debug('Solution contains the following tours: %s',
                              team_tours_dict)

            valid_solution = True
            all_tours_edges = []
            for i, tour in enumerate(team_tours_dict):
                this_tour_edges = self._extract_tours_from_arcs(tour)
                self.logger.debug(
                    'Solution for team %d contains the following tour(s):', i)
                for tt in this_tour_edges:
                    self.logger.debug('   %s', tt)

                if len(this_tour_edges) > 1 or not any(
                        u == 0 for u, v in this_tour_edges[0]):
                    assert self._lazy_subtour_elimination, 'subtour elimination failed'
                    valid_solution = False
                    self._eliminate_subtours_dfj(this_tour_edges)
                    self.logger.debug(
                        'Subtour elimination constraints updated (%d inserted so far)'
                        % (self._subtour_constraints))
                else:
                    all_tours_edges.append(this_tour_edges[0])

            if valid_solution:
                break

        res.write(num=1)

        self.logger.info('Solved successfully')
        self._result = all_tours_edges
        return self._result

    def _eliminate_subtours_dfj(self, tours):
        ''' adds DFJ subtour elimination constraints
        '''
        for tour in tours:
            tour_nodes = set(i for i, _ in tour)
            for team in range(self._num_teams):
                self._subtour_constraints += 1
                name = 'Subtour_%d' % self._subtour_constraints
                constraint = pmo.constraint(body=sum(
                    self._model.x[i, j, team] for i in tour_nodes
                    for j in tour_nodes
                    if i != j and (i, j) in self._model.Edges),
                                            ub=len(tour_nodes) - 1)
                setattr(self._model, name, constraint)
                self._solver.add_constraint(getattr(self._model, name))

    def _extract_solution_from_model(self):
        ''' returns a list of dictionaries i -> list of j containing the tours found by the model
        '''
        tours = []
        for t in range(self._num_teams):
            vertices = [
                n for n in self._model.Nodes
                if 0.98 <= self._model.y[n, t].value
            ]
            self.logger.debug('Team %d selected nodes %s', t, vertices)

            edges = {}
            for i, j in self._model.Edges:
                if 0.98 <= self._model.x[i, j, t].value <= 1.5:
                    edges[i] = j
            tours.append(edges)

            self.logger.debug('Team %d selected edges %s', t, edges)
            assert set(vertices) == set(edges)
        return tours

    @staticmethod
    def _extract_tours_from_arcs(arcs):
        ''' given a dictionary of arcs, returns a list of tours, where every tour is a list of arcs
        '''
        assert set(arcs.keys()) == set(
            arcs.values()), 'arcs do not form a set of tours: %r' % arcs
        not_assigned, assigned = set(arcs.keys()), set()

        tours = []
        while not_assigned:
            tour, cursor = [], not_assigned.pop()
            while not tour or cursor not in assigned:
                tour.append((cursor, arcs[cursor]))
                assigned.add(cursor)
                not_assigned.discard(cursor)
                cursor = arcs[cursor]
            tours.append(tour)
        return tours

    def explore_edge_cost_vertex_reward_tradeoff(self, steps):
        # introduce variables for vertex reward and edge cost
        self._model.VertexReward = aml.Var()
        self._model.AssignVertexReward = pmo.constraint(expr=sum(
            self._model.y[n, t] * self._model.r[n] for n in self._model.Nodes
            for t in self._model.Teams) == self._model.VertexReward)

        self._model.EdgeCost = aml.Var()
        self._model.AssignEdgeCost = aml.Constraint(expr=sum(
            self._model.x[u, v, t] * self._model.d[u, v]
            for (u, v) in self._model.Edges
            for t in self._model.Teams) == self._model.EdgeCost)

        self._solver.add_var(self._model.VertexReward)
        self._solver.add_constraint(self._model.AssignVertexReward)
        self._solver.add_var(self._model.EdgeCost)
        self._solver.add_constraint(self._model.AssignEdgeCost)

        # step 1: obtain maximum vertex reward
        self.logger.info('Obtaining maximum vertex reward...')
        del self._model.Objective
        self._model.Objective = aml.Objective(expr=self._model.VertexReward,
                                              sense=aml.maximize)
        self._solver.set_objective(self._model.Objective)
        self.solve()
        max_reward = aml.value(self._model.VertexReward)
        self.logger.info('Maximum reward is %f with cost %f', max_reward,
                         aml.value(self._model.EdgeCost))

        # step 2: obtain minimum edge cost
        self.logger.info('Obtaining minumum edge cost...')
        del self._model.Objective
        self._model.Objective = aml.Objective(expr=self._model.EdgeCost,
                                              sense=aml.minimize)
        self._solver.set_objective(self._model.Objective)
        self.solve()
        max_cost = aml.value(self._model.EdgeCost)
        self.logger.info('Minimum cost is %f with reward %f', max_cost,
                         aml.value(self._model.VertexReward))

        # step 3: obtain minumum edge cost, conditioned on maximum vertex reward
        self.logger.info(
            'Obtaining minimum cost conditioned on maximum reward...')
        self._model.ForcedReward = aml.Param(initialize=max_reward)
        self._model.MaxVertexReward = pmo.constraint(
            expr=self._model.VertexReward == self._model.ForcedReward)
        self._solver.add_constraint(self._model.MaxVertexReward)
        self.solve()
        max_cost_max_reward = aml.value(self._model.EdgeCost)
        self.logger.info('Cost is %f with reward %f', max_cost_max_reward,
                         aml.value(self._model.VertexReward))

        # step 4: iterate between these two values
        self._solver.remove_constraint(self._model.MaxVertexReward)
        del self._model.MaxVertexReward
        del self._model.ForcedReward
        self._model.EdgeCostSlackValue = aml.Param(initialize=0.0,
                                                   mutable=True)
        self._model.EdgeCostSlack = aml.Var(within=aml.NonNegativeReals)
        self._model.Epsilon = aml.Param(initialize=1e-4)
        del self._model.Objective
        self._model.Objective = aml.Objective(
            rule=lambda model: model.VertexReward + model.Epsilon * model.
            EdgeCostSlack,
            sense=aml.maximize)
        self._solver.add_var(self._model.EdgeCostSlack)
        self._solver.set_objective(self._model.Objective)

        for i in range(steps):
            value = max_cost_max_reward + i * (
                max_cost - max_cost_max_reward) / float(steps - 1)
            self.logger.info('======')
            self.logger.info('Iteration %d - Cost bound is %f', i + 1, value)
            self._model.EdgeCostSlackValue.set_value(value)
            self._model.EdgeCostConstr = pmo.constraint(
                expr=self._model.EdgeCost +
                self._model.EdgeCostSlack == self._model.EdgeCostSlackValue)
            self._solver.add_constraint(self._model.EdgeCostConstr)

            vaccine = self.solve()
            reward, cost = aml.value(self._model.VertexReward), aml.value(
                self._model.EdgeCost)
            self.logger.info('Obtained reward %f with cost %f', reward, cost)
            yield vaccine, reward, cost

            self._solver.remove_constraint(self._model.EdgeCostConstr)
            del self._model.EdgeCostConstr