or-toolschococp-sat

Minimize error in average of priced resource assignments


Problem definition

I need to group resources, where the average price of each group is closest to a given average price. As an input I get a list of resources with price and quantities of each one, plus the target groups quantities and average prices. The goal here is to assign quantities from resources to each group, whereby the group's final average price is as close as possible to requested price.

I've modeled this as an error minimization problem. My constraints are that all quantities must be utilized and SUM(quantities of resources) = SUM(quantities of target groups).

I managed to use Choco Solver on my problem successfully, however it uses integer to store the variables, and this will not be enough precision for me (as my real use case has decimals so I have to scale them to integers).

I now want to port my model to use Google's OR-Tools that uses longs instead of ints which should be enough for my use case. But I don't know how to define the error functions to minimize in OR-Tool's API. Has anyone done something similar?

My model (which works in Choco) is as per below:

R = input resources,
G = output groups,

Rq = resource quantity,
Rp = resource price,

Gq = group target quantity,
Gp = group target average price

The variables to calculate are the quantity of each resource i assigned to each group j, RiGj.

// Sum of quantities assigned to each group, must be the same as the group's quantity
`Gjq = SUM(RiGj)`

// Sum of each resource's assigned quantities, must be the available resource's quantity
`Riq = SUM(RiGj)`

// The error of each group's assignments would be:
`Ej = ABS(SUM(RiGj * Rip)/Gjq - Gjp)`

The solver should then minimize Ej. Please see below my working Choco solution:

    Model model = new Model();
    IntVar[] vars = new IntVar[resources.length * groups.length];

    ArExpression[] groupQtyConstraint = new ArExpression[groups.length];
    ArExpression[] resourceQtyConstraint = new ArExpression[resources.length];
    ArExpression[] error = new ArExpression[groups.length];

    for (int i = 0; i < groups.length; i++) {
      for (int j = 0; j < resources.length; j++) {
        // Variables
        IntVar curr = model.intVar(0, resources[j].quantity().intValue());
        vars[i * resources.length + j] = curr;

        // Group qty constraint build-up
        if (groupQtyConstraint[i] == null) {
          groupQtyConstraint[i] = curr;
        } else {
          groupQtyConstraint[i] = groupQtyConstraint[i].add(curr);
        }

        // Resource qty constraint build-up
        if (resourceQtyConstraint[j] == null) {
          resourceQtyConstraint[j] = curr;
        } else {
          resourceQtyConstraint[j] = resourceQtyConstraint[j].add(curr);
        }

        // Average price build-up
        // TODO: Problem here is precisions - int will NOT BE enough
        if (error[i] == null) {
          error[i] = curr.mul(resources[j].price()));
        } else {
          error[i] = error[i].add(curr.mul(resources[j].price()));
        }
      }
      model.arithm(groupQtyConstraint[i].intVar(), "=", groups[i].quantity()).post();
    }

    for (int i = 0; i < resources.length; i++) {
      model.arithm(resourceQtyConstraint[i].intVar(), "=", resources[i].quantity()).post();
    }

    ArExpression objective = null;
    for (int i = 0; i < groups.length; i++) {
      error[i] =
          error[i]
              .div(groups[i].quantity())
              .sub(groups[i].price())
              .abs();

      if (objective == null) {
        objective = error[i];
      } else {
        objective = objective.add(error[i]);
      }
    }

    // Find the optimal solution that minimizes the error
    Solver solver = model.getSolver();
    Solution optimalSolution = solver.findOptimalSolution(objective.intVar(), Model.MINIMIZE);

Example:

Resources: [
  ("R1", 12, 103.5),
  ("R2", 6, 102.45),
  ("R3", 2, 100.10)
]

Groups: [
  ("G1", 6, 102.5833),
  ("G2", 14, 102.9571)
]

The optimal solution here should be:

"G1": [
  ("R1", 3),
  ("R2", 2),
  ("R3", 1)
]

"G2": [
  ("R1", 9),
  ("R2", 4),
  ("R3", 1)
]

Note: I am multiplying the prices by 10,000 in this example to normalize them to integer.

I simply cant wrap my head around on how to define the below using OR-Tools:

Ej = ABS(SUM(RiGj * Rip)/Gjq - Gjp)

And then minimize on the solver.


Solution

  • Ej = ABS(SUM(RiGj * Rip)/Gjq - Gjp)

    You need to use multiple temporary variables and use the AddAbsEquality() and AddDivisionEquality() variables.

        ej_tmp1 = sum(RiGj * RiGj)  # with the correct python loop.
        ej_tmp2 = model.NewIntVar(0, some_upper_bound, 'avg_assigned_to_ej')
        model.AddDivisionEquality(ej_tmp2, ej_tmp1, Gjq)
        Ej = model.NewIntVar(0, some_upper_bound, 'deviation_assigned_to_ej')
        model.AddAbsEquality(Ej, ej_tmp2 - Gjp)