Search code examples
optimizationschedulermathematical-optimizationminizinc

Using forall() predicate in minizinc as assignment statement without 'constraint'


I have a Minizinc program for generating the optimal charge/discharge schedule for a grid-connected battery, given a set of prices by time-interval.

My program works (sort of; subject to some caveats), but my question is about two 'constraint' statements which are really just assignment statements:

constraint forall(t in 2..T)(MW_SETPOINT[t-1] - SALE[t] = MW_SETPOINT[t]);
constraint forall(t in 1..T)(PROFIT[t] = SALE[t] * PRICE[t]);

These just mean Energy SALES is the delta in MW_SETPOINT from t-1 to 1, and PROFIT is SALE * PRICE for each interval. So it seems counterintuitive to me to declare them as 'constraints'. But I've been unable to formulate them as assignment statements without throwing syntax errors.

Question:

  • Is there a more idiomatic way to declare such assignment statements for an array which is a function of other params/variables? Or is making assignments for arrays in constraints the recommended/idiomatic way to do it in Minizinc?

Full program for context:

% PARAMS
int: MW_CAPACITY = 10;
array[int] of float: PRICE; 

% DERIVED PARAMS
int: STARTING_MW = MW_CAPACITY div 2;  % integer division
int: T = length(PRICE);

% DECISION VARIABLE - MW SETPOINT EACH INTERVAL
array[1..T] of var 0..MW_CAPACITY: MW_SETPOINT;

% DERIVED/INTERMEDIATE VARIABLES
array[1..T] of var -1*MW_CAPACITY..MW_CAPACITY: SALE;  
array[1..T] of var float: PROFIT;
var float: NET_PROFIT = sum(PROFIT);

% CONSTRAINTS
%% If start at 5MW, and sell 5 first interval, setpoint for first interval is 0 
constraint MW_SETPOINT[1] = STARTING_MW - SALE[1];  

%% End where you started; opt schedule from arbitrage means no net MW over time
constraint MW_SETPOINT[T] = STARTING_MW;            

%% these are really justassignment statements for SALE & PROFIT
constraint forall(t in 2..T)(MW_SETPOINT[t-1] - SALE[t] = MW_SETPOINT[t]);
constraint forall(t in 1..T)(PROFIT[t] = SALE[t] * PRICE[t]);

% OBJECTIVE: MAXIMIZE REVENUE
solve maximize NET_PROFIT;

output["DAILY_PROFIT: " ++ show(NET_PROFIT) ++ 
      "\nMW SETPOINTS: " ++ show(MW_SETPOINT) ++ 
      "\nMW SALES: " ++ show(SALE) ++
      "\n$/MW PRICES: " ++ show(PRICE)++
      "\nPROFITS: " ++ show(PROFIT)
      ]; 

It can be run with

minizinc opt_sched_hindsight.mzn --solver org.minizinc.mip.coin-bc -D "PRICE = [29.835, 29.310470000000002, 28.575059999999997, 28.02416, 28.800690000000003, 32.41052, 34.38542, 29.512390000000003, 25.66587, 25.0499, 26.555529999999997, 28.149440000000002, 30.216509999999996, 32.32415, 31.406609999999997, 36.77642, 41.94735, 51.235209999999995, 50.68137, 64.54481, 48.235170000000004, 40.27663, 34.93675, 31.10404];"```

Solution

  • You can play with Array Comprehensions: (quote from the docs)

    Array comprehensions have this syntax:

    <array-comp> ::= "[" <expr> "|" <comp-tail> "]"
    

    For example (with the literal equivalents on the right):

    [2*i | i in 1..5]       % [2, 4, 6, 8, 10]
    

    Array comprehensions have more flexible type and inst requirements than set comprehensions (see Set Comprehensions).

    Array comprehensions are allowed over a variable set with finite type, the result is an array of optional type, with length equal to the cardinality of the upper bound of the variable set. For example:

    var set of 1..5: x;
    array[int] of var opt int: y = [ i * i | i in x ];
    

    The length of array will be 5.

    Array comprehensions are allowed where the where-expression is a var bool. Again the resulting array is of optional type, and of length equal to that given by the generator expressions. For example:

    var int x;
    array[int] of var opt int: y = [ i | i in 1..10 where i != x ];
    

    The length of the array will be 10.

    The indices of an evaluated simple array comprehension are implicitly 1..n, where n is the length of the evaluated comprehension.

    Example:

    int: MW_CAPACITY = 10;
    int: STARTING_MW = MW_CAPACITY div 2;
    
    array [int] of float: PRICE = [1.0, 2.0, 3.0, 4.0];
    int: T = length(PRICE);
    
    array [1..T] of var -1*MW_CAPACITY..MW_CAPACITY: SALE;
    
    array [1..T] of var 0..MW_CAPACITY: MW_SETPOINT = let {
            int: min_i = min(index_set(PRICE));
        } in  
            [STARTING_MW - sum([SALE[j] | j in min_i..i])
             | i in index_set(PRICE)];
    
    array [1..T] of var float: PROFIT =
            [SALE[i] * PRICE[i]
             | i in index_set(PRICE)];
    
    solve satisfy;
    

    Output:

    ~$ minizinc test.mzn 
    SALE = array1d(1..4, [-10, -5, 0, 0]);
    ----------
    

    Notice that index_set(PRICE) is nothing else but 1..T and that min(index_set(PRICE)) is nothing else but 1, so one could write the above array comprehensions also as

    array [1..T] of var 0..MW_CAPACITY: MW_SETPOINT =
            [STARTING_MW - sum([SALE[j] | j in 1..i])
             | i in 1..T];
    
    array [1..T] of var float: PROFIT =
            [SALE[i] * PRICE[i]
             | i in 1..T];