OpenRaoMPSolverTest.java
/*
* Copyright (c) 2024, RTE (http://www.rte-france.com)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.powsybl.openrao.searchtreerao.linearoptimisation.algorithms.linearproblem;
import com.google.ortools.linearsolver.MPConstraint;
import com.google.ortools.linearsolver.MPSolver;
import com.google.ortools.linearsolver.MPVariable;
import com.powsybl.openrao.commons.OpenRaoException;
import com.powsybl.openrao.raoapi.parameters.extensions.SearchTreeRaoRangeActionsOptimizationParameters;
import com.powsybl.openrao.searchtreerao.result.api.LinearProblemStatus;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* @author Peter Mitri {@literal <peter.mitri at rte-international.com>}
*/
class OpenRaoMPSolverTest {
static final double DOUBLE_TOLERANCE = 1e-4;
private OpenRaoMPSolver openRaoMPSolver;
private MPSolver mpSolver;
@BeforeEach
void setUp() {
openRaoMPSolver = new OpenRaoMPSolver("test", SearchTreeRaoRangeActionsOptimizationParameters.Solver.SCIP);
mpSolver = openRaoMPSolver.getMpSolver();
}
@Test
void basicTest() {
assertNotNull(openRaoMPSolver.getObjective());
assertEquals(SearchTreeRaoRangeActionsOptimizationParameters.Solver.SCIP, openRaoMPSolver.getSolver());
assertEquals(MPSolver.OptimizationProblemType.SCIP_MIXED_INTEGER_PROGRAMMING, openRaoMPSolver.getMpSolver().problemType());
openRaoMPSolver = new OpenRaoMPSolver("rao_test_prob", SearchTreeRaoRangeActionsOptimizationParameters.Solver.CBC);
assertEquals(SearchTreeRaoRangeActionsOptimizationParameters.Solver.CBC, openRaoMPSolver.getSolver());
assertEquals(MPSolver.OptimizationProblemType.CBC_MIXED_INTEGER_PROGRAMMING, openRaoMPSolver.getMpSolver().problemType());
}
@Test
void testAddAndRemoveVariable() {
String varName = "var1";
assertEquals(0, openRaoMPSolver.numVariables());
// Check exception on get before adding variable
Exception e = assertThrows(OpenRaoException.class, () -> openRaoMPSolver.getVariable(varName));
assertEquals("Variable var1 has not been created yet", e.getMessage());
// Add variable
OpenRaoMPVariable var1 = openRaoMPSolver.makeNumVar(-5, 3.6, varName);
// Check exception when re-adding
e = assertThrows(OpenRaoException.class, () -> openRaoMPSolver.makeNumVar(-5, 3.6, varName));
assertEquals("Variable var1 already exists", e.getMessage());
// Check OpenRaoMPVariable
assertEquals(varName, var1.name());
assertTrue(openRaoMPSolver.hasVariable(varName));
assertEquals(var1, openRaoMPSolver.getVariable(varName));
assertEquals(1, openRaoMPSolver.numVariables());
// Check OR-Tools object
MPVariable orToolsVar1 = mpSolver.lookupVariableOrNull(varName);
assertNotNull(orToolsVar1);
checkVarBounds(var1, orToolsVar1, -5, 3.6);
// Change lb/ub & check
var1.setLb(-100.);
var1.setUb(150.7);
checkVarBounds(var1, orToolsVar1, -100., 150.7);
var1.setBounds(-98.5, -97.6);
checkVarBounds(var1, orToolsVar1, -98.5, -97.6);
}
private void checkVarBounds(OpenRaoMPVariable raoVar, MPVariable ortoolsVar, double expectedLb, double expectedUb) {
// OpenRAO object
assertEquals(expectedLb, raoVar.lb(), DOUBLE_TOLERANCE);
assertEquals(expectedUb, raoVar.ub(), DOUBLE_TOLERANCE);
// OR-Tools object
assertEquals(expectedLb, ortoolsVar.lb(), DOUBLE_TOLERANCE);
assertEquals(expectedUb, ortoolsVar.ub(), DOUBLE_TOLERANCE);
}
@Test
void testAddIntVar() {
openRaoMPSolver.makeIntVar(5, 10, "var1");
MPVariable orToolsVar = mpSolver.lookupVariableOrNull("var1");
assertNotNull(orToolsVar);
assertEquals(5., orToolsVar.lb(), DOUBLE_TOLERANCE);
assertEquals(10., orToolsVar.ub(), DOUBLE_TOLERANCE);
}
@Test
void testAddBoolVar() {
openRaoMPSolver.makeBoolVar("var1");
MPVariable orToolsVar = mpSolver.lookupVariableOrNull("var1");
assertNotNull(orToolsVar);
assertEquals(0., orToolsVar.lb(), DOUBLE_TOLERANCE);
assertEquals(1., orToolsVar.ub(), DOUBLE_TOLERANCE);
}
@Test
void testAddAndRemoveConstraint() {
String varName = "var1";
String constName = "const1";
assertEquals(0, openRaoMPSolver.numConstraints());
// Add variable
OpenRaoMPVariable var1 = openRaoMPSolver.makeNumVar(-5, 3.6, varName);
// Check exception on get before adding constraint
Exception e = assertThrows(OpenRaoException.class, () -> openRaoMPSolver.getConstraint(constName));
assertEquals("Constraint const1 has not been created yet", e.getMessage());
// Add constraint & coefficient
OpenRaoMPConstraint const1 = openRaoMPSolver.makeConstraint(-121.6, 65.956, constName);
const1.setCoefficient(var1, 648.9);
// Check exception when re-adding
e = assertThrows(OpenRaoException.class, () -> openRaoMPSolver.makeConstraint(-121.6, 65.956, constName));
assertEquals("Constraint const1 already exists", e.getMessage());
// Check OpenRaoMPConstraint
assertTrue(openRaoMPSolver.hasConstraint(constName));
assertEquals(const1, openRaoMPSolver.getConstraint(constName));
assertEquals(648.9, const1.getCoefficient(var1), DOUBLE_TOLERANCE);
assertEquals(1, openRaoMPSolver.numConstraints());
assertEquals(constName, const1.name());
// Check OR-Tools object
MPConstraint orToolsConst1 = mpSolver.lookupConstraintOrNull(constName);
assertNotNull(orToolsConst1);
MPVariable orToolsVar1 = mpSolver.lookupVariableOrNull(varName);
assertEquals(648.9, orToolsConst1.getCoefficient(orToolsVar1), DOUBLE_TOLERANCE);
checkConstBounds(const1, orToolsConst1, -121.6, 65.956);
// Change lb/ub & check
const1.setLb(-100.);
const1.setUb(150.7);
checkConstBounds(const1, orToolsConst1, -100., 150.7);
const1.setBounds(-98.5, -97.6);
checkConstBounds(const1, orToolsConst1, -98.5, -97.6);
// Change coef & check
const1.setCoefficient(var1, 465.9);
assertEquals(465.9, const1.getCoefficient(var1), DOUBLE_TOLERANCE);
assertEquals(465.9, orToolsConst1.getCoefficient(orToolsVar1), DOUBLE_TOLERANCE);
}
private void checkConstBounds(OpenRaoMPConstraint raoConst, MPConstraint ortoolsConst, double expectedLb, double expectedUb) {
// OpenRAO object
assertEquals(expectedLb, raoConst.lb(), DOUBLE_TOLERANCE);
assertEquals(expectedUb, raoConst.ub(), DOUBLE_TOLERANCE);
// OR-Tools object
assertEquals(expectedLb, ortoolsConst.lb(), DOUBLE_TOLERANCE);
assertEquals(expectedUb, ortoolsConst.ub(), DOUBLE_TOLERANCE);
}
@Test
void testAddConstraintWithNoBounds() {
String constName = "const1";
// Add constraint
OpenRaoMPConstraint const1 = openRaoMPSolver.makeConstraint(constName);
// Check OpenRaoMPConstraint
assertEquals(-openRaoMPSolver.infinity(), const1.lb(), openRaoMPSolver.infinity() * 1e-3);
assertEquals(openRaoMPSolver.infinity(), const1.ub(), openRaoMPSolver.infinity() * 1e-3);
assertTrue(openRaoMPSolver.hasConstraint(constName));
assertEquals(const1, openRaoMPSolver.getConstraint(constName));
// Check OR-Tools object
MPConstraint orToolsConst1 = mpSolver.lookupConstraintOrNull(constName);
assertNotNull(orToolsConst1);
assertEquals(-openRaoMPSolver.infinity(), orToolsConst1.lb(), openRaoMPSolver.infinity() * 1e-3);
assertEquals(openRaoMPSolver.infinity(), orToolsConst1.ub(), openRaoMPSolver.infinity() * 1e-3);
}
@Test
void testRounding() {
double d1 = 1.;
// big enough deltas are not rounded out by the rounding method
double eps = 1e-6;
double d2 = d1 + eps;
assertNotEquals(OpenRaoMPSolver.roundDouble(d1), OpenRaoMPSolver.roundDouble(d2), 1e-20);
// small deltas are rounded out as long as we round enough bits
eps = 1e-15;
d2 = d1 + eps;
assertEquals(OpenRaoMPSolver.roundDouble(d1), OpenRaoMPSolver.roundDouble(d2), 1e-20);
// infinity
assertEquals(Double.POSITIVE_INFINITY, OpenRaoMPSolver.roundDouble(Double.POSITIVE_INFINITY));
}
@Test
void testRoundingFailsOnNan() {
Exception e = assertThrows(OpenRaoException.class, () -> OpenRaoMPSolver.roundDouble(Double.NaN));
assertEquals("Trying to add a NaN value in MIP!", e.getMessage());
}
@Test
void testSetSolverSpecificParametersAsString() {
assertTrue(openRaoMPSolver.setSolverSpecificParametersAsString(null)); // acceptable
assertTrue(openRaoMPSolver.setSolverSpecificParametersAsString("parallel/maxnthreads 1, lp/presolving TRUE")); // acceptable SCIP parameters
assertFalse(openRaoMPSolver.setSolverSpecificParametersAsString("parallel/maxnthreads 1, lp/pre_solving TRUE")); // not acceptable SCIP parameters
}
@Test
void testConvertResultStatus() {
assertEquals(LinearProblemStatus.OPTIMAL, OpenRaoMPSolver.convertResultStatus(MPSolver.ResultStatus.OPTIMAL));
assertEquals(LinearProblemStatus.ABNORMAL, OpenRaoMPSolver.convertResultStatus(MPSolver.ResultStatus.ABNORMAL));
assertEquals(LinearProblemStatus.FEASIBLE, OpenRaoMPSolver.convertResultStatus(MPSolver.ResultStatus.FEASIBLE));
assertEquals(LinearProblemStatus.UNBOUNDED, OpenRaoMPSolver.convertResultStatus(MPSolver.ResultStatus.UNBOUNDED));
assertEquals(LinearProblemStatus.INFEASIBLE, OpenRaoMPSolver.convertResultStatus(MPSolver.ResultStatus.INFEASIBLE));
assertEquals(LinearProblemStatus.NOT_SOLVED, OpenRaoMPSolver.convertResultStatus(MPSolver.ResultStatus.NOT_SOLVED));
}
@Test
void testObjective() {
checkObjectiveSense(true); // minimization by default
openRaoMPSolver.setMaximization();
checkObjectiveSense(false);
openRaoMPSolver.setMinimization();
checkObjectiveSense(true);
String varName = "var1";
OpenRaoMPVariable var1 = openRaoMPSolver.makeNumVar(-5, 3.6, varName);
openRaoMPSolver.getObjective().setCoefficient(var1, 3.5);
assertEquals(3.5, openRaoMPSolver.getObjective().getCoefficient(var1));
assertEquals(3.5, mpSolver.objective().getCoefficient(mpSolver.lookupVariableOrNull(varName)));
openRaoMPSolver.getObjective().setCoefficient(var1, -963.5);
assertEquals(-963.5, openRaoMPSolver.getObjective().getCoefficient(var1));
assertEquals(-963.5, mpSolver.objective().getCoefficient(mpSolver.lookupVariableOrNull(varName)));
}
private void checkObjectiveSense(boolean minim) {
// OpenRAO object
assertEquals(minim, openRaoMPSolver.isMinimization());
assertEquals(!minim, openRaoMPSolver.isMaximization());
// OR-Tools object
assertEquals(minim, mpSolver.objective().minimization());
assertEquals(!minim, mpSolver.objective().maximization());
}
@Test
void testSolve() {
// Maximize 2 * x + y
// such that: x + y <= 10
// 0 <= x <= 4
// 0 <= y <= 10
// Should result in: x = 4, y = 6, obj = 14
OpenRaoMPVariable x = openRaoMPSolver.makeNumVar(0, 4, "x");
OpenRaoMPVariable y = openRaoMPSolver.makeNumVar(0, 10, "y");
OpenRaoMPConstraint constraint = openRaoMPSolver.makeConstraint(-openRaoMPSolver.infinity(), 10, "constraint");
constraint.setCoefficient(x, 1);
constraint.setCoefficient(y, 1);
openRaoMPSolver.getObjective().setCoefficient(x, 2);
openRaoMPSolver.getObjective().setCoefficient(y, 1);
openRaoMPSolver.setMaximization();
LinearProblemStatus result = openRaoMPSolver.solve();
assertTrue(mpSolver.objective().maximization());
assertFalse(mpSolver.objective().minimization());
assertEquals(LinearProblemStatus.OPTIMAL, result);
assertEquals(4., x.solutionValue(), DOUBLE_TOLERANCE);
assertEquals(6., y.solutionValue(), DOUBLE_TOLERANCE);
// Test that after resetting, solver & obj sense is the same
openRaoMPSolver.resetModel();
assertNotNull(openRaoMPSolver.getObjective());
checkObjectiveSense(false);
assertEquals(SearchTreeRaoRangeActionsOptimizationParameters.Solver.SCIP, openRaoMPSolver.getSolver());
assertEquals(MPSolver.OptimizationProblemType.SCIP_MIXED_INTEGER_PROGRAMMING, openRaoMPSolver.getMpSolver().problemType());
}
@Test
void testInfinity() {
OpenRaoMPSolver solver = new OpenRaoMPSolver("solver", SearchTreeRaoRangeActionsOptimizationParameters.Solver.CBC);
assertEquals(Double.POSITIVE_INFINITY, solver.infinity());
solver = new OpenRaoMPSolver("solver", SearchTreeRaoRangeActionsOptimizationParameters.Solver.SCIP);
assertEquals(1e23, solver.infinity());
// can't test XPRESS because we need the link to the library
}
@Test
void testRoundSmallValues() {
assertEquals(1e-5, OpenRaoMPSolver.roundDouble(1e-5), 1e-12);
assertEquals(1e-6, OpenRaoMPSolver.roundDouble(1e-6), 1e-12);
assertEquals(0., OpenRaoMPSolver.roundDouble(1e-6 * 0.999), 1e-12);
assertEquals(0., OpenRaoMPSolver.roundDouble(1e-7), 1e-12);
assertEquals(0., OpenRaoMPSolver.roundDouble(1e-11), 1e-12);
}
}