routing(swig): Expose SimpleBoundCost related methods (Fix #1815)

* Extract BoundCost from SimpleBoundCost since SWIG do not support
nested class
* Wrap it in all languages
* Expose new API
  * SoftSpanUpperBoundForVehicle
  * QuadraticCostSoftSpanUpperBoundForVehicle
* Add tests
This commit is contained in:
Mizux Seiha
2022-10-20 18:06:52 +02:00
committed by Corentin Le Molgat
parent 13c13048d5
commit 36a29b2ddf
10 changed files with 326 additions and 23 deletions

View File

@@ -12,6 +12,7 @@
// limitations under the License.
using System;
using System.Linq;
using Xunit;
using Google.OrTools.ConstraintSolver;
@@ -213,4 +214,118 @@ public class RoutingSolverTest
Assert.Equal(5, solution.ObjectiveValue());
}
}
public class BoundCostTest
{
[Fact]
public void TestCtor()
{
// Create Routing Index Manager
BoundCost boundCost = new BoundCost();
Assert.NotNull(boundCost);
Assert.Equal(0, boundCost.bound);
Assert.Equal(0, boundCost.cost);
boundCost = new BoundCost(97 /*bound*/, 101 /*cost*/);
Assert.NotNull(boundCost);
Assert.Equal(97, boundCost.bound);
Assert.Equal(101, boundCost.cost);
}
}
public class RoutingDimensionTest
{
[Fact]
public void TestCtor()
{
// Create Routing Index Manager
RoutingIndexManager manager = new RoutingIndexManager(31 /*locations*/, 7 /*vehicle*/, 3 /*depot*/);
Assert.NotNull(manager);
// Create Routing Model.
RoutingModel routing = new RoutingModel(manager);
Assert.NotNull(routing);
// Create a distance callback.
int transitIndex = routing.RegisterTransitCallback((long fromIndex, long toIndex) =>
{
// Convert from routing variable Index to
// distance matrix NodeIndex.
var fromNode = manager.IndexToNode(fromIndex);
var toNode = manager.IndexToNode(toIndex);
return Math.Abs(toNode - fromNode);
});
Assert.True(routing.AddDimension(transitIndex, 100, 100, true, "Dimension"));
RoutingDimension dimension = routing.GetDimensionOrDie("Dimension");
}
[Fact]
public void TestSoftSpanUpperBound()
{
// Create Routing Index Manager
RoutingIndexManager manager = new RoutingIndexManager(31 /*locations*/, 7 /*vehicle*/, 3 /*depot*/);
Assert.NotNull(manager);
// Create Routing Model.
RoutingModel routing = new RoutingModel(manager);
Assert.NotNull(routing);
// Create a distance callback.
int transitIndex = routing.RegisterTransitCallback((long fromIndex, long toIndex) =>
{
// Convert from routing variable Index to
// distance matrix NodeIndex.
var fromNode = manager.IndexToNode(fromIndex);
var toNode = manager.IndexToNode(toIndex);
return Math.Abs(toNode - fromNode);
});
Assert.True(routing.AddDimension(transitIndex, 100, 100, true, "Dimension"));
RoutingDimension dimension = routing.GetDimensionOrDie("Dimension");
BoundCost boundCost = new BoundCost(/*bound=*/97, /*cost=*/43);
Assert.NotNull(boundCost);
Assert.False(dimension.HasSoftSpanUpperBounds());
foreach (int v in Enumerable.Range(0, manager.GetNumberOfVehicles()).ToArray())
{
dimension.SetSoftSpanUpperBoundForVehicle(boundCost, v);
BoundCost bc = dimension.GetSoftSpanUpperBoundForVehicle(v);
Assert.NotNull(bc);
Assert.Equal(97, bc.bound);
Assert.Equal(43, bc.cost);
}
Assert.True(dimension.HasSoftSpanUpperBounds());
}
[Fact]
public void TestQuadraticCostSoftSpanUpperBound()
{
// Create Routing Index Manager
RoutingIndexManager manager = new RoutingIndexManager(31 /*locations*/, 7 /*vehicle*/, 3 /*depot*/);
Assert.NotNull(manager);
// Create Routing Model.
RoutingModel routing = new RoutingModel(manager);
Assert.NotNull(routing);
// Create a distance callback.
int transitIndex = routing.RegisterTransitCallback((long fromIndex, long toIndex) =>
{
// Convert from routing variable Index to
// distance matrix NodeIndex.
var fromNode = manager.IndexToNode(fromIndex);
var toNode = manager.IndexToNode(toIndex);
return Math.Abs(toNode - fromNode);
});
Assert.True(routing.AddDimension(transitIndex, 100, 100, true, "Dimension"));
RoutingDimension dimension = routing.GetDimensionOrDie("Dimension");
BoundCost boundCost = new BoundCost(/*bound=*/97, /*cost=*/43);
Assert.NotNull(boundCost);
Assert.False(dimension.HasQuadraticCostSoftSpanUpperBounds());
foreach (int v in Enumerable.Range(0, manager.GetNumberOfVehicles()).ToArray())
{
dimension.SetQuadraticCostSoftSpanUpperBoundForVehicle(boundCost, v);
BoundCost bc = dimension.GetQuadraticCostSoftSpanUpperBoundForVehicle(v);
Assert.NotNull(bc);
Assert.Equal(97, bc.bound);
Assert.Equal(43, bc.cost);
}
Assert.True(dimension.HasQuadraticCostSoftSpanUpperBounds());
}
}
} // namespace Google.OrTools.Tests

View File

@@ -167,6 +167,12 @@ using System.Collections.Generic;
%unignore TypeRegulationsChecker;
%ignore TypeRegulationsChecker::CheckVehicle;
// SimpleBoundCosts
%unignore BoundCost;
%unignore SimpleBoundCosts;
%rename("GetBoundCost") SimpleBoundCosts::bound_cost;
%rename("GetSize") SimpleBoundCosts::Size;
} // namespace operations_research
%rename("GetStatus") operations_research::RoutingModel::status;

View File

@@ -15,17 +15,17 @@ package com.google.ortools.constraintsolver;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.google.auto.value.AutoValue;
import com.google.ortools.Loader;
import com.google.ortools.constraintsolver.RoutingModelParameters;
import com.google.ortools.constraintsolver.RoutingSearchParameters;
import com.google.protobuf.Duration;
import java.util.ArrayList;
import java.util.function.LongBinaryOperator;
import java.util.function.LongUnaryOperator;
import java.util.stream.IntStream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -690,4 +690,88 @@ public final class RoutingSolverTest {
IntVar[] vehicleVars = model.vehicleVars();
assertThat(vehicleVars).isNotEmpty();
}
@Test
public void testBoundCost_Ctor() {
// Create Routing Index Manager
BoundCost boundCost = new BoundCost();
assertNotNull(boundCost);
assertEquals(0, boundCost.getBound());
assertEquals(0, boundCost.getCost());
boundCost = new BoundCost(97 /*bound*/, 101 /*cost*/);
assertNotNull(boundCost);
assertEquals(97, boundCost.getBound());
assertEquals(101, boundCost.getCost());
}
@Test
public void testRoutingDimension_Ctor() {
final RoutingIndexManager manager = new RoutingIndexManager(31, 7, 3);
assertNotNull(manager);
final RoutingModel model = new RoutingModel(manager);
assertNotNull(model);
final int transitIndex = model.registerTransitCallback((long fromIndex, long toIndex) -> {
final int fromNode = manager.indexToNode(fromIndex);
final int toNode = manager.indexToNode(toIndex);
return (long) Math.abs(toNode - fromNode);
});
assertTrue(model.addDimension(transitIndex, 100, 100, true, "Dimension"));
final RoutingDimension dimension = model.getMutableDimension("Dimension");
}
@Test
public void testRoutingDimension_SoftSpanUpperBound() {
final RoutingIndexManager manager = new RoutingIndexManager(31, 7, 3);
assertNotNull(manager);
final RoutingModel model = new RoutingModel(manager);
assertNotNull(model);
final int transitIndex = model.registerTransitCallback((long fromIndex, long toIndex) -> {
final int fromNode = manager.indexToNode(fromIndex);
final int toNode = manager.indexToNode(toIndex);
return (long) Math.abs(toNode - fromNode);
});
assertTrue(model.addDimension(transitIndex, 100, 100, true, "Dimension"));
final RoutingDimension dimension = model.getMutableDimension("Dimension");
final BoundCost boundCost = new BoundCost(97 /*bound*/, 101 /*cost*/);
assertNotNull(boundCost);
assertFalse(dimension.hasSoftSpanUpperBounds());
for (int v : IntStream.range(0, manager.getNumberOfVehicles()).toArray()) {
dimension.setSoftSpanUpperBoundForVehicle(boundCost, v);
final BoundCost bc = dimension.getSoftSpanUpperBoundForVehicle(v);
assertNotNull(bc);
assertEquals(97, bc.getBound());
assertEquals(101, bc.getCost());
}
assertTrue(dimension.hasSoftSpanUpperBounds());
}
@Test
public void testRoutingDimension_QuadraticCostSoftSpanUpperBound() {
final RoutingIndexManager manager = new RoutingIndexManager(31, 7, 3);
assertNotNull(manager);
final RoutingModel model = new RoutingModel(manager);
assertNotNull(model);
final int transitIndex = model.registerTransitCallback((long fromIndex, long toIndex) -> {
final int fromNode = manager.indexToNode(fromIndex);
final int toNode = manager.indexToNode(toIndex);
return (long) Math.abs(toNode - fromNode);
});
assertTrue(model.addDimension(transitIndex, 100, 100, true, "Dimension"));
final RoutingDimension dimension = model.getMutableDimension("Dimension");
final BoundCost boundCost = new BoundCost(97 /*bound*/, 101 /*cost*/);
assertNotNull(boundCost);
assertFalse(dimension.hasQuadraticCostSoftSpanUpperBounds());
for (int v : IntStream.range(0, manager.getNumberOfVehicles()).toArray()) {
dimension.setQuadraticCostSoftSpanUpperBoundForVehicle(boundCost, v);
final BoundCost bc = dimension.getQuadraticCostSoftSpanUpperBoundForVehicle(v);
assertNotNull(bc);
assertEquals(97, bc.getBound());
assertEquals(101, bc.getCost());
}
assertTrue(dimension.hasQuadraticCostSoftSpanUpperBounds());
}
}

View File

@@ -251,6 +251,16 @@ import java.util.function.LongBinaryOperator;
%rename (getCumulVarSoftUpperBound) RoutingDimension::GetCumulVarSoftUpperBound;
%rename (getCumulVarSoftUpperBoundCoefficient) RoutingDimension::GetCumulVarSoftUpperBoundCoefficient;
%rename (getGlobalSpanCostCoefficient) RoutingDimension::global_span_cost_coefficient;
%rename (getLocalOptimizerOffsetForVehicle) RoutingDimension::GetLocalOptimizerOffsetForVehicle;
%rename (setSoftSpanUpperBoundForVehicle) RoutingDimension::SetSoftSpanUpperBoundForVehicle;
%rename (hasSoftSpanUpperBounds) RoutingDimension::HasSoftSpanUpperBounds;
%rename (getSoftSpanUpperBoundForVehicle) RoutingDimension::GetSoftSpanUpperBoundForVehicle;
%rename (setQuadraticCostSoftSpanUpperBoundForVehicle) RoutingDimension::SetQuadraticCostSoftSpanUpperBoundForVehicle;
%rename (hasQuadraticCostSoftSpanUpperBounds) RoutingDimension::HasQuadraticCostSoftSpanUpperBounds;
%rename (getQuadraticCostSoftSpanUpperBoundForVehicle) RoutingDimension::GetQuadraticCostSoftSpanUpperBoundForVehicle;
%rename (getGroupDelay) RoutingDimension::GetGroupDelay;
%rename (getNodeVisitTransitsOfVehicle) RoutingDimension::GetNodeVisitTransitsOfVehicle;
%rename (getSpanCostCoefficientForVehicle) RoutingDimension::GetSpanCostCoefficientForVehicle;
@@ -286,6 +296,12 @@ import java.util.function.LongBinaryOperator;
%unignore TypeRegulationsChecker;
%ignore TypeRegulationsChecker::CheckVehicle;
// SimpleBoundCosts
%unignore BoundCost;
%unignore SimpleBoundCosts;
%rename (getBoundCost) SimpleBoundCosts::bound_cost;
%rename (getSize) SimpleBoundCosts::Size;
} // namespace operations_research
// Generic rename rules.

View File

@@ -722,5 +722,78 @@ class TestRoutingModel(unittest.TestCase):
count += 1
class TestBoundCost(unittest.TestCase):
def testCtor(self):
bound_cost = pywrapcp.BoundCost()
self.assertIsNotNone(bound_cost)
self.assertEqual(0, bound_cost.bound)
self.assertEqual(0, bound_cost.cost)
bound_cost = pywrapcp.BoundCost(bound=97, cost=43)
self.assertIsNotNone(bound_cost)
self.assertEqual(97, bound_cost.bound)
self.assertEqual(43, bound_cost.cost)
class TestRoutingDimension(unittest.TestCase):
def testCtor(self):
manager = pywrapcp.RoutingIndexManager(31, 7, 3)
self.assertIsNotNone(manager)
model = pywrapcp.RoutingModel(manager)
self.assertIsNotNone(model)
transit_idx = model.RegisterTransitCallback(
partial(TransitDistance, manager))
self.assertTrue(
model.AddDimension(transit_idx, 90, 90, True, 'distance'))
model.GetDimensionOrDie('distance')
def testSoftSpanUpperBound(self):
manager = pywrapcp.RoutingIndexManager(31, 7, 3)
self.assertIsNotNone(manager)
model = pywrapcp.RoutingModel(manager)
self.assertIsNotNone(model)
transit_idx = model.RegisterTransitCallback(
partial(TransitDistance, manager))
self.assertTrue(
model.AddDimension(transit_idx, 100, 100, True, 'distance'))
dimension = model.GetDimensionOrDie('distance')
bound_cost = pywrapcp.BoundCost(bound=97, cost=43)
self.assertIsNotNone(bound_cost)
self.assertFalse(dimension.HasSoftSpanUpperBounds())
for v in range(manager.GetNumberOfVehicles()):
dimension.SetSoftSpanUpperBoundForVehicle(bound_cost, v)
bc = dimension.GetSoftSpanUpperBoundForVehicle(v)
self.assertIsNotNone(bc)
self.assertEqual(97, bc.bound)
self.assertEqual(43, bc.cost)
self.assertTrue(dimension.HasSoftSpanUpperBounds())
def testQuadraticCostSoftSpanUpperBound(self):
manager = pywrapcp.RoutingIndexManager(31, 7, 3)
self.assertIsNotNone(manager)
model = pywrapcp.RoutingModel(manager)
self.assertIsNotNone(model)
transit_idx = model.RegisterTransitCallback(
partial(TransitDistance, manager))
self.assertTrue(
model.AddDimension(transit_idx, 100, 100, True, 'distance'))
dimension = model.GetDimensionOrDie('distance')
bound_cost = pywrapcp.BoundCost(bound=97, cost=43)
self.assertIsNotNone(bound_cost)
self.assertFalse(dimension.HasQuadraticCostSoftSpanUpperBounds())
for v in range(manager.GetNumberOfVehicles()):
dimension.SetQuadraticCostSoftSpanUpperBoundForVehicle(
bound_cost, v)
bc = dimension.GetQuadraticCostSoftSpanUpperBoundForVehicle(v)
self.assertIsNotNone(bc)
self.assertEqual(97, bc.bound)
self.assertEqual(43, bc.cost)
self.assertTrue(dimension.HasQuadraticCostSoftSpanUpperBounds())
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@@ -88,6 +88,14 @@ enum OptionalBoolean {
BOOL_FALSE = 2,
BOOL_TRUE = 3,
};
// SimpleBoundCosts
%unignore BoundCost;
%unignore SimpleBoundCosts;
%unignore SimpleBoundCosts::bound_cost;
%rename("size") SimpleBoundCosts::Size;
} // namespace operations_research
// TODO(user): Use ignoreall/unignoreall for this one. A lot of work.

View File

@@ -2810,7 +2810,7 @@ void RoutingModel::CloseModelWithParameters(
if (dimension->HasSoftSpanUpperBounds()) {
for (int vehicle = 0; vehicle < vehicles(); ++vehicle) {
if (spans[vehicle]) continue;
const SimpleBoundCosts::BoundCost bound_cost =
const BoundCost bound_cost =
dimension->GetSoftSpanUpperBoundForVehicle(vehicle);
if (bound_cost.cost == 0) continue;
spans[vehicle] = solver_->MakeIntVar(0, span_ubs[vehicle]);
@@ -2819,7 +2819,7 @@ void RoutingModel::CloseModelWithParameters(
if (dimension->HasQuadraticCostSoftSpanUpperBounds()) {
for (int vehicle = 0; vehicle < vehicles(); ++vehicle) {
if (spans[vehicle]) continue;
const SimpleBoundCosts::BoundCost bound_cost =
const BoundCost bound_cost =
dimension->GetQuadraticCostSoftSpanUpperBoundForVehicle(vehicle);
if (bound_cost.cost == 0) continue;
spans[vehicle] = solver_->MakeIntVar(0, span_ubs[vehicle]);

View File

@@ -2655,7 +2655,7 @@ class TypeRegulationsConstraint : public Constraint {
TypeRequirementChecker requirement_checker_;
std::vector<Demon*> vehicle_demons_;
};
#if !defined SWIG
/// A structure meant to store soft bounds and associated violation constants.
/// It is 'Simple' because it has one BoundCost per element,
/// in contrast to 'Multiple'. Design notes:
@@ -2668,12 +2668,15 @@ class TypeRegulationsConstraint : public Constraint {
/// because the structure will be accessed through pointers, moreover having
/// to type bound_cost reminds the user of the order if they do a copy
/// assignment of the element.
// Can't be nested in SimpleBoundCosts since SWIG doesn't support nested class...
struct BoundCost {
int64_t bound;
int64_t cost;
BoundCost(int64_t bound = 0, int64_t cost = 0): bound(bound), cost(cost) {}
};
class SimpleBoundCosts {
public:
struct BoundCost {
int64_t bound;
int64_t cost;
};
SimpleBoundCosts(int num_bounds, BoundCost default_bound_cost)
: bound_costs_(num_bounds, default_bound_cost) {}
BoundCost& bound_cost(int element) { return bound_costs_[element]; }
@@ -2685,7 +2688,6 @@ class SimpleBoundCosts {
private:
std::vector<BoundCost> bound_costs_;
};
#endif // !defined SWIG
/// Dimensions represent quantities accumulated at nodes along the routes. They
/// represent quantities such as weights or volumes carried along the route, or
@@ -3057,21 +3059,21 @@ class RoutingDimension {
DCHECK_GE(local_optimizer_offset_for_vehicle_[vehicle], 0);
return local_optimizer_offset_for_vehicle_[vehicle];
}
#if !defined SWIG
/// If the span of vehicle on this dimension is larger than bound,
/// the cost will be increased by cost * (span - bound).
void SetSoftSpanUpperBoundForVehicle(SimpleBoundCosts::BoundCost bound_cost,
void SetSoftSpanUpperBoundForVehicle(BoundCost bound_cost,
int vehicle) {
if (!HasSoftSpanUpperBounds()) {
vehicle_soft_span_upper_bound_ = std::make_unique<SimpleBoundCosts>(
model_->vehicles(), SimpleBoundCosts::BoundCost{kint64max, 0});
model_->vehicles(), BoundCost{kint64max, 0});
}
vehicle_soft_span_upper_bound_->bound_cost(vehicle) = bound_cost;
}
bool HasSoftSpanUpperBounds() const {
return vehicle_soft_span_upper_bound_ != nullptr;
}
SimpleBoundCosts::BoundCost GetSoftSpanUpperBoundForVehicle(
BoundCost GetSoftSpanUpperBoundForVehicle(
int vehicle) const {
DCHECK(HasSoftSpanUpperBounds());
return vehicle_soft_span_upper_bound_->bound_cost(vehicle);
@@ -3079,11 +3081,11 @@ class RoutingDimension {
/// If the span of vehicle on this dimension is larger than bound,
/// the cost will be increased by cost * (span - bound)^2.
void SetQuadraticCostSoftSpanUpperBoundForVehicle(
SimpleBoundCosts::BoundCost bound_cost, int vehicle) {
BoundCost bound_cost, int vehicle) {
if (!HasQuadraticCostSoftSpanUpperBounds()) {
vehicle_quadratic_cost_soft_span_upper_bound_ =
std::make_unique<SimpleBoundCosts>(
model_->vehicles(), SimpleBoundCosts::BoundCost{kint64max, 0});
model_->vehicles(), BoundCost{kint64max, 0});
}
vehicle_quadratic_cost_soft_span_upper_bound_->bound_cost(vehicle) =
bound_cost;
@@ -3091,12 +3093,11 @@ class RoutingDimension {
bool HasQuadraticCostSoftSpanUpperBounds() const {
return vehicle_quadratic_cost_soft_span_upper_bound_ != nullptr;
}
SimpleBoundCosts::BoundCost GetQuadraticCostSoftSpanUpperBoundForVehicle(
BoundCost GetQuadraticCostSoftSpanUpperBoundForVehicle(
int vehicle) const {
DCHECK(HasQuadraticCostSoftSpanUpperBounds());
return vehicle_quadratic_cost_soft_span_upper_bound_->bound_cost(vehicle);
}
#endif /// !defined SWIG
private:
struct SoftBound {

View File

@@ -1473,7 +1473,7 @@ void PathCumulFilter::OnBeforeSynchronizePaths() {
CapSub(span_lower_bound, total_transit)));
}
if (FilterSoftSpanCost()) {
const SimpleBoundCosts::BoundCost bound_cost =
const BoundCost bound_cost =
dimension_.GetSoftSpanUpperBoundForVehicle(vehicle);
if (bound_cost.bound < span_lower_bound) {
const int64_t violation =
@@ -1483,7 +1483,7 @@ void PathCumulFilter::OnBeforeSynchronizePaths() {
}
}
if (FilterSoftSpanQuadraticCost()) {
const SimpleBoundCosts::BoundCost bound_cost =
const BoundCost bound_cost =
dimension_.GetQuadraticCostSoftSpanUpperBoundForVehicle(vehicle);
if (bound_cost.bound < span_lower_bound) {
const int64_t violation =
@@ -1676,7 +1676,7 @@ bool PathCumulFilter::AcceptPath(int64_t path_start, int64_t /*chain_start*/,
CapProd(vehicle_span_cost_coefficients_[vehicle], min_total_slack));
const int64_t span_lower_bound = CapAdd(total_transit, min_total_slack);
if (FilterSoftSpanCost()) {
const SimpleBoundCosts::BoundCost bound_cost =
const BoundCost bound_cost =
dimension_.GetSoftSpanUpperBoundForVehicle(vehicle);
if (bound_cost.bound < span_lower_bound) {
const int64_t violation = CapSub(span_lower_bound, bound_cost.bound);
@@ -1685,7 +1685,7 @@ bool PathCumulFilter::AcceptPath(int64_t path_start, int64_t /*chain_start*/,
}
}
if (FilterSoftSpanQuadraticCost()) {
const SimpleBoundCosts::BoundCost bound_cost =
const BoundCost bound_cost =
dimension_.GetQuadraticCostSoftSpanUpperBoundForVehicle(vehicle);
if (bound_cost.bound < span_lower_bound) {
const int64_t violation = CapSub(span_lower_bound, bound_cost.bound);

View File

@@ -1846,7 +1846,7 @@ bool DimensionCumulOptimizerCore::SetRouteCumulConstraints(
}
// Add soft span cost.
if (optimize_costs && dimension_->HasSoftSpanUpperBounds()) {
SimpleBoundCosts::BoundCost bound_cost =
BoundCost bound_cost =
dimension_->GetSoftSpanUpperBoundForVehicle(vehicle);
if (bound_cost.bound < std::numeric_limits<int64_t>::max() &&
bound_cost.cost > 0) {