Does the order of the constraint list matter?

I moved a group of constraints within a list to a different position for “aesthetic” reasons.
Instead, I noticed, with surprise, that the resulting solution differed in the values ​​of the x[ …] matrix of the variables.
How should I understand this?

 model = Model(HiGHS.Optimizer)
    JuMP.set_time_limit_sec(model, 600)
    # set_attribute(model, "mip_rel_gap", 0.75) # parametro di HIGHS
    # set_silent(model)
    @variable(model, x[1:nr, 1:ng, turni ], Bin)
    @variable(model, z[1:nr, 1:ng], Bin)
    @constraints(model, begin
        # each day must have exactly one t: (t in turni)
        fer1[k in union(t_feriali,t_card), g in g_feriali], sum(x[:,g,k]) == 1
        fer2[k in t_festivi, g in g_feriali], sum(x[:,g,k]) == 0

        fest1[k in t_festivi, g in g_festivi], sum(x[:,g,k]) == 1
        fest2[k in union(t_feriali, t_card), g in g_festivi], sum(x[:,g,k]) == 0

        # 1 turno di servizio di sabato mattina in CU e CL
        sat[k in t_Cs, g in saturdays(mese,y)], sum(x[:,g,k]) == 1
        # ma solo di sabato e non negli altri giorni
        no_sat[k in t_Cs, g in setdiff(1:ng,saturdays(mese,y))], sum(x[:,g,k]) == 0

        # range of N and NF for row for month
        [r in ops_lim], sum(x[r,j,"N"]+x[r,j,"NF"] for j in 1:ng) <= Nel_Mese["max_notti"] 
        [r in ops_lim], sum(x[r,j,"N"]+x[r,j,"NF"] for j in 1:ng) >= Nel_Mese["min_notti"]
        [r in ops_lim], sum(x[r,j,"N"] for j in 1:ng) <= Nel_Mese["max_notti_feriali"]
        [r in ops_lim], sum(x[r,j,"N"] for j in 1:ng) >= Nel_Mese["min_notti_feriali"]

        # # # max of  NF for row for month
        [ r in 1:nr], sum(x[r,j,"NF"] for j in g_festivi) <=  Nel_Mese["max_notti_festive"]  
        
        # # # NF==2 --> N<=1
        [r in ops_lim], sum(x[r,:,"NF"]) == sum(i * z[r, i] for i in 1:ng)
        [r in ops_lim], z[r,Nel_Mese["NF: NF ->N"]] --> {sum(x[r, :, "N"]) <= Nel_Mese["N: NF ->N"]}
        [r in ops_lim], sum(z[r,:]) <= 1

        # continuità per la cardio urgenza mattutina meno le eccezioni necessarie
        [r in 1:nr, (i,w) in enumerate(wks)], x[r,w[1],"CUM1"]-->{sum(x[r,w,"CUM1"])>=length(w)-wk["continuità_CUM1"][i]}
        [r in 1:nr, (i,w) in enumerate(wks)], x[r,w[1],"CUM2"]-->{sum(x[r,w,"CUM2"])>=length(w)-wk["continuità_CUM2"][i]}
        [r in 1:nr, (i,w) in enumerate(wks)], x[r,w[1],"CLM" ]-->{sum(x[r,w,"CLM" ])>=length(w)-wk[ "continuità_CLM"][i]}


        # # max turni nello stesso giorno per operatore
        # non notte insieme a mattina o pomeriggio, ecc.
        max_turni_fer_N_P[op in 1:nr, g in g_feriali], sum(x[op,g,k]  for k in ["N","P"]) <= 1
        max_turni_fer_N_M[op in 1:nr, g in g_feriali], sum(x[op,g,k]  for k in ["N";t_mattina]) <= 1
        max_turni_fer_P_M[op in 1:nr, g in g_feriali], sum(x[op,g,k]  for k in ["P";t_mattina]) <= 2    # 1
        max_turni_fer[op in 1:nr, g in g_feriali], sum(x[op,g,k]  for k in t_feriali) <= 2
        max_turni_CU[op in 1:nr, g in g_feriali], sum(x[op,g,k]  for k in t_card) <= 1
        max_turni_fest[op in 1:nr, g in g_festivi], sum(x[op,g,k]  for k in [t_festivi;t_Cs]) <= 1

...

end
...

    peso = Dict{String, Int}()
    for g in turni
        if g in [t_festivi; ["N", "CUMs", "CLMs"]]
            peso[g] = peso_festivi_e_notti
        else
            peso[g] = peso_feriali_giorno
        end
    end

    # Carico di lavoro
    @expression(model, workload[r=no_E], sum(x[r, g,k]*peso[k] for g in 1:ng, k in turni))
  
    # Variabili per max/min carico
    @variable(model, M)   # massimo carico
    @variable(model, m)   # minimo carico

    @constraint(model, [r in no_E], workload[r] <= M)
    @constraint(model, [r in no_E], workload[r] >= m)

    # Obiettivo: minimizzare squilibrio massimo
    @objective(model, Min, M - m)


...


########

 model = Model(HiGHS.Optimizer)
    JuMP.set_time_limit_sec(model, 600)
    # set_attribute(model, "mip_rel_gap", 0.75) # parametro di HIGHS
    # set_silent(model)
    @variable(model, x[1:nr, 1:ng, turni ], Bin)
    @variable(model, z[1:nr, 1:ng], Bin)
    @constraints(model, begin
        # each day must have exactly one t: (t in turni)
        fer1[k in union(t_feriali,t_card), g in g_feriali], sum(x[:,g,k]) == 1
        fer2[k in t_festivi, g in g_feriali], sum(x[:,g,k]) == 0

        fest1[k in t_festivi, g in g_festivi], sum(x[:,g,k]) == 1
        fest2[k in union(t_feriali, t_card), g in g_festivi], sum(x[:,g,k]) == 0

        # 1 turno di servizio di sabato mattina in CU e CL
        sat[k in t_Cs, g in saturdays(mese,y)], sum(x[:,g,k]) == 1
        # ma solo di sabato e non negli altri giorni
        no_sat[k in t_Cs, g in setdiff(1:ng,saturdays(mese,y))], sum(x[:,g,k]) == 0

#####

        # # max turni nello stesso giorno per operatore
        # non notte insieme a mattina o pomeriggio, ecc.
        max_turni_fer_N_P[op in 1:nr, g in g_feriali], sum(x[op,g,k]  for k in ["N","P"]) <= 1
        max_turni_fer_N_M[op in 1:nr, g in g_feriali], sum(x[op,g,k]  for k in ["N";t_mattina]) <= 1
        max_turni_fer_P_M[op in 1:nr, g in g_feriali], sum(x[op,g,k]  for k in ["P";t_mattina]) <= 2    # 1
        max_turni_fer[op in 1:nr, g in g_feriali], sum(x[op,g,k]  for k in t_feriali) <= 2
        max_turni_CU[op in 1:nr, g in g_feriali], sum(x[op,g,k]  for k in t_card) <= 1
        max_turni_fest[op in 1:nr, g in g_festivi], sum(x[op,g,k]  for k in [t_festivi;t_Cs]) <= 1

########


        # range of N and NF for row for month
        [r in ops_lim], sum(x[r,j,"N"]+x[r,j,"NF"] for j in 1:ng) <= Nel_Mese["max_notti"] 
        [r in ops_lim], sum(x[r,j,"N"]+x[r,j,"NF"] for j in 1:ng) >= Nel_Mese["min_notti"]
        [r in ops_lim], sum(x[r,j,"N"] for j in 1:ng) <= Nel_Mese["max_notti_feriali"]
        [r in ops_lim], sum(x[r,j,"N"] for j in 1:ng) >= Nel_Mese["min_notti_feriali"]

        # # # max of  NF for row for month
        [ r in 1:nr], sum(x[r,j,"NF"] for j in g_festivi) <=  Nel_Mese["max_notti_festive"]  
        
        # # # NF==2 --> N<=1
        [r in ops_lim], sum(x[r,:,"NF"]) == sum(i * z[r, i] for i in 1:ng)
        [r in ops_lim], z[r,Nel_Mese["NF: NF ->N"]] --> {sum(x[r, :, "N"]) <= Nel_Mese["N: NF ->N"]}
        [r in ops_lim], sum(z[r,:]) <= 1

        # continuità per la cardio urgenza mattutina meno le eccezioni necessarie
        [r in 1:nr, (i,w) in enumerate(wks)], x[r,w[1],"CUM1"]-->{sum(x[r,w,"CUM1"])>=length(w)-wk["continuità_CUM1"][i]}
        [r in 1:nr, (i,w) in enumerate(wks)], x[r,w[1],"CUM2"]-->{sum(x[r,w,"CUM2"])>=length(w)-wk["continuità_CUM2"][i]}
        [r in 1:nr, (i,w) in enumerate(wks)], x[r,w[1],"CLM" ]-->{sum(x[r,w,"CLM" ])>=length(w)-wk[ "continuità_CLM"][i]}



...

end


...

    peso = Dict{String, Int}()
    for g in turni
        if g in [t_festivi; ["N", "CUMs", "CLMs"]]
            peso[g] = peso_festivi_e_notti
        else
            peso[g] = peso_feriali_giorno
        end
    end

    # Carico di lavoro
    @expression(model, workload[r=no_E], sum(x[r, g,k]*peso[k] for g in 1:ng, k in turni))
  
    # Variabili per max/min carico
    @variable(model, M)   # massimo carico
    @variable(model, m)   # minimo carico

    @constraint(model, [r in no_E], workload[r] <= M)
    @constraint(model, [r in no_E], workload[r] >= m)

    # Obiettivo: minimizzare squilibrio massimo
    @objective(model, Min, M - m)

...

That is, I understand that I can have the same optimum (by the way, I don’t know what it is, in my case) in different combinations of the variables, but I can’t explain why a different ordering of the constraints changes the “landing” point.

PS
Out of curiosity, how can I see the value of the optimum at which the algorithm stopped?

1 Like

Does the order of the constraint list matter?

The short answer is yes.

The main reason is that there can be many equivalent solutions to a problem. Changing the order of the variables and constraints can change some of the internal algorithmic decisions that HiGHS makes during the solution algorithm, and this can cause it to find one solution before another.

However, even if it finds different solutions in x, the objective value of each optimal decision will be the same (to within the optimality tolerance).

You can see the final objective value with objective_value(model).

2 Likes