Search code examples
pythonoptimizationpyomo

Pyomo, how to write a constraint that ensures a maximum up time of a device in Pyomo?


I am working on an optimisation problem, where there is a device that is shiftable and can be scheduled for other hours rather than its initial schedule. The problem is that this device has a specific duty cycle, I mean if it is ON, it has to be ON for at least T_ON hours and cannot be on for more than T_ON hours as well.

There is a binary variable, I(t) showing ON (1) or OFF (0) status of the device. In Pyomo, as far as I know, we cannot define an if statement on variables, so I am quite stuck in how to formulise this problem in Pyomo. I also tried to make a graphic version of what I am looking for in figure below.

I really appreciate it if anyone can help me with an idea on how to formulise this problem.

Cheers! :)

See the graphic here! :)

If the "if" statement was working in Pyomo, I would write it as below:


model.Gen  = Set(initialize=RangeSet(0,End_uses -1), doc='set of enduses') #index of end uses

model.T  = Set(initialize=RangeSet(0,50-1), doc='Time') #index of data samples

model.taw = SetOf(model.T) # references whatever is in T


#Yes/No of each item (binary variable) --> include or exclude an item

model.I_test=Var(model.Gen,model.T,within=Binary)

#Min Up time
def MinUp(model,g,t):
    if t > 0:
        if model.I_test[g,t]-model.I_test[g,t-1]>0:
                return sum(model.I_test[g,t2] for t2 in model.taw if t2>=t and t2<=t+T_on[g]) >= T_on[g]*(model.I_test[g,t]-model.I_test[g,t-1])
    else:
        return Constraint.Skip

model.cons5 = Constraint(model.Gen,model.T,rule=MinUp)

I am looking for a help on formulising the problem or even a similar piece of code that can help me to solve this issue.


Solution

  • The piece you are missing is an additional variable to keep track of which periods the machine was started in. This will allow several other types of constraints to be drawn using some integer arithmetic. Below is an example of several.

    It isn't super clear from your problem which of these would be most useful to you. You seem to imply that the min_runtime == max_runtime and then you mention a duty cycle, but don't define it.

    If your specification simplifies down to exactly n hours on followed by minimum of n hours off, you could re-write one of these types of constraints below such that the n periods after a start are "on" and then n+1 to 2n periods are off and delete the other constraints.

    Code:

    # cycling machine example
    
    import pyomo.environ as pyo
    
    # machine characteristics
    min_runtime = 2        # minimum number of periods to run after startup
    min_reset_time = 3     # off periods after being turned off
    duty_cycle = (4, 8)    # 50% duty cycle evaluated over 8 periods "4 of 8"
    
    time_periods = 12
    demand = 7 # periods of 'runtime' from the machine
    
    m = pyo.ConcreteModel()
    
    # SETS
    m.T = pyo.Set(initialize=range(time_periods))
    
    # VARS
    m.start   = pyo.Var(m.T, domain=pyo.Binary, doc='start machine')
    m.running = pyo.Var(m.T, domain=pyo.Binary, doc='machine running')
    
    # OBJ
    m.obj = pyo.Objective(expr=sum(m.running[t] for t in m.T))   # minimze the runtime
    
    # CONSTRAINTS
    
    # satisfy the demand
    m.satisfy_demand = pyo.Constraint(expr=sum(m.running[t] for t in m.T) >= demand)
    
    @m.Constraint(m.T)
    def link_running(m, t):
        """machine can only be running if it was running in prior period or started in this one"""
        if t == m.T.first():
            return pyo.Constraint.Skip
        return m.running[t] <= m.running[t-1] + m.start[t]
    
    @m.Constraint(m.T)
    def min_runtime(m, t):
        """force the minimum runtime after a start event"""
        # make a subset of the time periods of interest...
        next_time_periods = {t + offset for offset in range(min_runtime) if t + offset < time_periods}
        return sum(m.running[tt] for tt in next_time_periods) >= len(next_time_periods) * m.start[t]
    
    @m.Constraint(m.T)
    def honor_reset_time(m, t):
        if t == m.T.first():
            return pyo.Constraint.Skip
        previous_time_periods = {t - offset for offset in range(1, min_reset_time + 1) if t - offset >=0}
        return len(previous_time_periods) * m.start[t] <= sum(1-m.running[tt] for tt in previous_time_periods)
    
    @m.Constraint(m.T)
    def duty_cycle_limit(m, t):
        """ evaluate all possible duty cycles and impose limit """
        if t + duty_cycle[1] > time_periods:
            return pyo.Constraint.Skip
        duty_cycle_periods = list(range(t, t + duty_cycle[1]))
        return sum(m.running[tt] for tt in duty_cycle_periods) <= duty_cycle[0]
    
    
    m.pprint()
    
    solver = pyo.SolverFactory('glpk')
    soln = solver.solve(m)
    print(soln)
    m.running.display()
    

    Output (partial):

    Solver: 
    - Status: ok
      Termination condition: optimal
      Statistics: 
        Branch and bound: 
          Number of bounded subproblems: 1
          Number of created subproblems: 1
      Error rc: 0
      Time: 0.0057790279388427734
    Solution: 
    - number of solutions: 0
      number of solutions displayed: 0
    
    running : machine running
        Size=12, Index=T
        Key : Lower : Value : Upper : Fixed : Stale : Domain
          0 :     0 :   1.0 :     1 : False : False : Binary
          1 :     0 :   1.0 :     1 : False : False : Binary
          2 :     0 :   1.0 :     1 : False : False : Binary
          3 :     0 :   1.0 :     1 : False : False : Binary
          4 :     0 :   0.0 :     1 : False : False : Binary
          5 :     0 :   0.0 :     1 : False : False : Binary
          6 :     0 :   0.0 :     1 : False : False : Binary
          7 :     0 :   0.0 :     1 : False : False : Binary
          8 :     0 :   1.0 :     1 : False : False : Binary
          9 :     0 :   1.0 :     1 : False : False : Binary
         10 :     0 :   1.0 :     1 : False : False : Binary
         11 :     0 :   0.0 :     1 : False : False : Binary