def solve(self):
        """ Returns the optimal schedule

        Returns
        -------
        time_of_op : dict
            Timeslot for each operation in the DAG
        ops_at_time : defaultdic
            List of operations in each timeslot
        length : int
            Maximum latency of optimal schedule
        """
        init_drmt_schedule = None

        cpath, cplat = self.G.critical_path()
        Q_MAX = int(math.ceil(1.5 * cplat / self.period_duration))

        print ('{:*^80}'.format(' Running DRMT ILP solver '))
        T = self.period_duration
        nodes = self.G.nodes()
        match_nodes = self.G.nodes(select='match')
        action_nodes = self.G.nodes(select='action')
        edges = self.G.edges()

        match_action_pairs = self.get_match_action_pairs(edges)


        m = Model()
        m.setParam("LogToConsole", 0)

        # Create variables
        # t is the start time for each DAG node in the first scheduling period
        t = m.addVars(nodes, lb=0, ub=GRB.INFINITY, vtype=GRB.INTEGER, name="t")

        # The quotients and remainders when dividing by T (see below)
        # qr[v, q, r] is 1 when t[v]
        # leaves a quotient of q and a remainder of r, when divided by T.



        qr  = m.addVars(list(itertools.product(nodes, range(Q_MAX), range(T))), vtype=GRB.BINARY, name="qr")
        qra  = m.addVars(list(itertools.product(match_action_pairs, range(Q_MAX), range(T))), vtype=GRB.BINARY, name="qra")
        qrm  = m.addVars(list(itertools.product(match_action_pairs, range(Q_MAX), range(T))), vtype=GRB.BINARY, name="qrm")
        qru  = m.addVars(list(itertools.product(match_action_pairs, range(Q_MAX), range(T))), vtype=GRB.BINARY, name="qru")

        # Is there any match/action from packet q in time slot r?
        # This is required to enforce limits on the number of packets that
        # can be performing matches or actions concurrently on any processor.
        any_match = m.addVars(list(itertools.product(range(Q_MAX), range(T))), vtype=GRB.BINARY, name = "any_match")
        any_action = m.addVars(list(itertools.product(range(Q_MAX), range(T))), vtype=GRB.BINARY, name = "any_action")

        # The length of the schedule
        length = m.addVar(lb=0, ub=GRB.INFINITY, vtype=GRB.INTEGER, name="length")

        # Set objective: minimize length of schedule
        m.setObjective(length, GRB.MINIMIZE)

        # Set constraints

        # The length is the maximum of all t's
        m.addConstrs((t[v]  <= length for v in nodes), "constr_length_is_max")

        # Given v, qr[v, q, r] is 1 for exactly one q, r, i.e., there's a unique quotient and remainder
        m.addConstrs((sum(qr[v, q, r] for q in range(Q_MAX) for r in range(T)) == 1 for v in nodes),\
                     "constr_unique_quotient_remainder")

        # This is just a way to write dividend = quotient * divisor + remainder
        m.addConstrs((t[v] == \
                      sum(q * qr[v, q, r] for q in range(Q_MAX) for r in range(T)) * T + \
                      sum(r * qr[v, q, r] for q in range(Q_MAX) for r in range(T)) \
                      for v in nodes), "constr_division")

        
        m.addConstrs((t[v] - t[u] >= self.G.edge[u][v]['delay'] for (u,v) in edges),\
                     "constr_dag_dependencies")
        
        # add constraints for monitoring scratch space
        for (u,v) in match_action_pairs:
            for q in range(Q_MAX):
                for r in range(T):
                    m.addConstr(q*T + r + 1 - t[v] <= BIG_M - BIG_M*qra[(u,v), q, r], "action_util"+str(u)+str(v))
                    m.addConstr( -q*T - r + 1 + t[u] <= BIG_M - BIG_M*qrm[(u,v), q, r], "match_util"+str(u)+str(v))
                    m.addConstr(qru[(u,v), q, r] >= qra[(u,v), q, r] + qrm[(u,v), q, r] - 1, "real_util"+str(u)+str(v))
        

        #m.addConstrs((sum(qru[(u,v) q, r] for (u,v) in match_action_pairs for q in range(Q_MAX)) <= scratch_max for r in range(T)), "constr_scratch")
        m.addConstrs((sum(qru[(u,v), q, r] for (u,v) in match_action_pairs) <= self.scratch_max for q in range(Q_MAX) for r in range(T) ),
                      "scratch_util")


        # Number of match units does not exceed match_unit_limit
        # for every time step (j) < T, check the total match unit requirements
        # across all nodes (v) that can be "rotated" into this time slot.
        m.addConstrs((sum(math.ceil((1.0 * self.G.node[v]['key_width']) / self.input_spec.match_unit_size) * qr[v, q, r]\
                      for v in match_nodes for q in range(Q_MAX))\
                      <= self.input_spec.match_unit_limit for r in range(T)),\
                      "constr_match_units")

        # The action field resource constraint (similar comments to above)
        m.addConstrs((sum(self.G.node[v]['num_fields'] * qr[v, q, r]\
                      for v in action_nodes for q in range(Q_MAX))\
                      <= self.input_spec.action_fields_limit for r in range(T)),\
                      "constr_action_fields")

        # Any time slot (r) can have match or action operations
        # from only match_proc_limit/action_proc_limit packets
        # We do this in two steps.

        # First, detect if there is any (at least one) match/action operation from packet q in time slot r
        # if qr[v, q, r] = 1 for any match node, then any_match[q,r] must = 1 (same for actions)
        # Notice that any_match[q, r] may be 1 even if all qr[v, q, r] are zero
        m.addConstrs((sum(qr[v, q, r] for v in match_nodes) <= (len(match_nodes) * any_match[q, r]) \
                      for q in range(Q_MAX)\
                      for r in range(T)),\
                      "constr_any_match1");

        m.addConstrs((sum(qr[v, q, r] for v in action_nodes) <= (len(action_nodes) * any_action[q, r]) \
                      for q in range(Q_MAX)\
                      for r in range(T)),\
                      "constr_any_action1");

        # Second, check that, for any r, the summation over q of any_match[q, r] is under proc_limits
        m.addConstrs((sum(any_match[q, r] for q in range(Q_MAX)) <= self.input_spec.match_proc_limit\
                      for r in range(T)), "constr_match_proc")
        m.addConstrs((sum(any_action[q, r] for q in range(Q_MAX)) <= self.input_spec.action_proc_limit\
                      for r in range(T)), "constr_action_proc")

        # Seed initial values
        if init_drmt_schedule:
          for i in nodes:
            t[i].start = init_drmt_schedule[i]

        # Solve model
        m.setParam('TimeLimit', self.minute_limit * 60)
        m.optimize()
        ret = m.Status

        if (ret == GRB.INFEASIBLE):
          print ('Infeasible')
          return None
        elif ((ret == GRB.TIME_LIMIT) or (ret == GRB.INTERRUPTED)):
          if (m.SolCount == 0):
            print ('Hit time limit or interrupted, no solution found yet')
            return None
          else:
            print ('Hit time limit or interrupted, suboptimal solution found with gap ', m.MIPGap)
        elif (ret == GRB.OPTIMAL):
          print ('Optimal solution found with gap ', m.MIPGap)
        else:
          print ('Return code is ', ret)
          assert(False)

        # Construct and return schedule
        self.time_of_op = {}
        self.ops_at_time = collections.defaultdict(list)
        self.length = int(length.x + 1)
        assert(self.length == length.x + 1)
        for v in nodes:
            tv = int(t[v].x)
            self.time_of_op[v] = tv
            self.ops_at_time[tv].append(v)

        # Compute periodic schedule to calculate resource usage
        self.compute_periodic_schedule()

        # Populate solution
        solution = Solution()
        solution.time_of_op = self.time_of_op
        solution.ops_at_time = self.ops_at_time
        solution.ops_on_ring = self.ops_on_ring
        solution.length = self.length
        solution.match_key_usage     = self.match_key_usage
        solution.action_fields_usage = self.action_fields_usage
        solution.match_units_usage   = self.match_units_usage
        solution.match_proc_usage    = self.match_proc_usage
        solution.action_proc_usage   = self.action_proc_usage
        return solution