#  ___________________________________________________________________________
#
#  Pyomo: Python Optimization Modeling Objects
#  Copyright 2017 National Technology and Engineering Solutions of Sandia, LLC
#  Under the terms of Contract DE-NA0003525 with National Technology and
#  Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
#  rights in this software.
#  This software is distributed under the 3-clause BSD License.
#  ___________________________________________________________________________

from pyomo.environ import *
from pyomo.gdp import *
from pyomo.common.fileutils import this_file_dir
from os.path import join

#
# Jobshop example from http://www.gams.com/modlib/libhtml/logmip4.htm
#
# This model solves a jobshop scheduling, which has a set of jobs
# which must be processed in sequence of stages but not all jobs
# require all stages. A zero wait transfer policy is assumed between
# stages. To obtain a feasible solution it is necessary to eliminate
# all clashes between jobs. It requires that no two jobs be performed
# at any stage at any time. The objective is to minimize the makespan,
# the time to complete all jobs.
#
# References:
#
# Raman & Grossmann, Computers and Chemical Engineering 18, 7, p.563-578, 1994.
#
# Aldo Vecchietti, LogMIP User's Manual, http://www.logmip.ceride.gov.ar/, 2007
#

def build_model():
    model = AbstractModel()

    model.JOBS = Set(ordered=True)
    model.STAGES = Set(ordered=True)
    model.I_BEFORE_K = RangeSet(0,1)

    # Task durations
    model.tau = Param(model.JOBS, model.STAGES, default=0)

    # Total Makespan (this will be the objective)
    model.ms = Var()
    # Start time of each job
    def t_bounds(model, I):
        return (0, sum(value(model.tau[idx]) for idx in model.tau))
    model.t = Var( model.JOBS, within=NonNegativeReals, bounds=t_bounds )

    # Auto-generate the L set (potential collisions between 2 jobs at any stage.
    def _L_filter(model, I, K, J):
        return I < K and model.tau[I,J] and model.tau[K,J]
    model.L = Set( initialize=model.JOBS * model.JOBS * model.STAGES,
                   dimen=3, filter=_L_filter)

    # Makespan is greater than the start time of every job + that job's
    # total duration
    def _feas(model, I):
        return model.ms >= model.t[I] + sum(model.tau[I,M] for M in model.STAGES)
    model.Feas = Constraint(model.JOBS, rule=_feas)

    # Disjunctions to prevent clashes at a stage: This creates a set of
    # disjunct pairs: one if job I occurs before job K and the other if job
    # K occurs before job I.
    def _NoClash(disjunct, I, K, J, IthenK):
        model = disjunct.model()
        lhs = model.t[I] + sum([M<J and model.tau[I,M] or 0 for M in model.STAGES])
        rhs = model.t[K] + sum([M<J and model.tau[K,M] or 0 for M in model.STAGES])
        if IthenK:
            disjunct.c = Constraint(expr=lhs+model.tau[I,J]<=rhs)
        else:
            disjunct.c = Constraint(expr=rhs+model.tau[K,J]<=lhs)
    model.NoClash = Disjunct(model.L, model.I_BEFORE_K, rule=_NoClash)

    # Define the disjunctions: either job I occurs before K or K before I
    def _disj(model, I, K, J):
        return [model.NoClash[I,K,J,IthenK] for IthenK in model.I_BEFORE_K]
    model.disj = Disjunction(model.L, rule=_disj)

    # minimize makespan
    model.makespan = Objective(expr=model.ms)
    return model


def build_small_concrete():
    return build_model().create_instance(join(this_file_dir(), 'jobshop-small.dat'))


if __name__ == "__main__":
    m = build_small_concrete()
    TransformationFactory('gdp.bigm').apply_to(m)
    SolverFactory('gams').solve(m, solver='baron', tee=True, add_options=['option optcr=1e-6;'])
    m.makespan.display()
