diff --git a/examples/dotnet/Google.OrTools.Examples.sln b/examples/dotnet/Google.OrTools.Examples.sln index 3db75a47b6..c9a63cb800 100644 --- a/examples/dotnet/Google.OrTools.Examples.sln +++ b/examples/dotnet/Google.OrTools.Examples.sln @@ -29,6 +29,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JobshopFt06Sat", "JobshopFt EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JobshopSat", "JobshopSat.csproj", "{ED4158FF-7E32-4C4A-A3AB-20DC162CAB6E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetworkRoutingSat", "NetworkRoutingSat.csproj", "{132ECD45-2448-4084-B253-D49F0FAB2EB3}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NursesSat", "NursesSat.csproj", "{8525EA38-48C0-4D64-A1DE-FE54F5287B4F}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ShiftSchedulingSat", "ShiftSchedulingSat.csproj", "{D309FF10-3D42-446A-8402-62A941C0783A}" @@ -266,6 +268,18 @@ Global {921FA31F-F200-4144-8925-7F93F5BC7062}.Release|x64.Build.0 = Release|Any CPU {921FA31F-F200-4144-8925-7F93F5BC7062}.Release|x86.ActiveCfg = Release|Any CPU {921FA31F-F200-4144-8925-7F93F5BC7062}.Release|x86.Build.0 = Release|Any CPU + {132ECD45-2448-4084-B253-D49F0FAB2EB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {132ECD45-2448-4084-B253-D49F0FAB2EB3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {132ECD45-2448-4084-B253-D49F0FAB2EB3}.Debug|x64.ActiveCfg = Debug|Any CPU + {132ECD45-2448-4084-B253-D49F0FAB2EB3}.Debug|x64.Build.0 = Debug|Any CPU + {132ECD45-2448-4084-B253-D49F0FAB2EB3}.Debug|x86.ActiveCfg = Debug|Any CPU + {132ECD45-2448-4084-B253-D49F0FAB2EB3}.Debug|x86.Build.0 = Debug|Any CPU + {132ECD45-2448-4084-B253-D49F0FAB2EB3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {132ECD45-2448-4084-B253-D49F0FAB2EB3}.Release|Any CPU.Build.0 = Release|Any CPU + {132ECD45-2448-4084-B253-D49F0FAB2EB3}.Release|x64.ActiveCfg = Release|Any CPU + {132ECD45-2448-4084-B253-D49F0FAB2EB3}.Release|x64.Build.0 = Release|Any CPU + {132ECD45-2448-4084-B253-D49F0FAB2EB3}.Release|x86.ActiveCfg = Release|Any CPU + {132ECD45-2448-4084-B253-D49F0FAB2EB3}.Release|x86.Build.0 = Release|Any CPU {D309FF10-3D42-446A-8402-62A941C0783A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D309FF10-3D42-446A-8402-62A941C0783A}.Debug|Any CPU.Build.0 = Debug|Any CPU {D309FF10-3D42-446A-8402-62A941C0783A}.Debug|x64.ActiveCfg = Debug|Any CPU diff --git a/examples/dotnet/NetworkRoutingSat.cs b/examples/dotnet/NetworkRoutingSat.cs new file mode 100644 index 0000000000..90a68e856d --- /dev/null +++ b/examples/dotnet/NetworkRoutingSat.cs @@ -0,0 +1,1149 @@ +// Copyright 2010-2019 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +using Google.OrTools.Sat; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + + +/// +/// This model solves a multicommodity mono-routing problem with +/// capacity constraints and a max usage cost structure. This means +/// that given a graph with capacity on edges, and a set of demands +/// (source, destination, traffic), the goal is to assign one unique +/// path for each demand such that the cost is minimized. The cost is +/// defined by the maximum ratio utilization (traffic/capacity) for all +/// arcs. There is also a penalty associated with an traffic of an arc +/// being above the comfort zone, 85% of the capacity by default. +/// Please note that constraint programming is well suited here because +/// we cannot have multiple active paths for a single demand. +/// Otherwise, a approach based on a linear solver is a better match. +/// A random problem generator is also included. +/// +public class NetworkRoutingSat +{ + private static int clients = 0; // Number of network clients nodes. If equal to zero, then all backbones nodes are also client nodes. + private static int backbones = 0; // "Number of backbone nodes" + private static int demands = 0; // "Number of network demands." + private static int trafficMin = 0; // "Min traffic of a demand." + private static int trafficMax = 0; // "Max traffic of a demand." + private static int minClientDegree = 0; //"Min number of connections from a client to the backbone." + private static int maxClientDegree = 0; //"Max number of connections from a client to the backbone." + private static int minBackboneDegree = 0; //"Min number of connections from a backbone node to the rest of the backbone nodes." + private static int maxBackboneDegree = 0; // "Max number of connections from a backbone node to the rest of the backbone nodes." + private static int maxCapacity = 0; //"Max traffic on any arc." + private static int fixedChargeCost = 0; //"Fixed charged cost when using an arc." + private static int seed = 0; //"Random seed" + private static double comfortZone = 0.85; // "Above this limit in 1/1000th, the link is said to be congested." + private static int extraHops = 6; // "When creating all paths for a demand, we look at paths with maximum length 'shortest path + extra_hops'" + private static int maxPaths = 1200; //"Max number of possible paths for a demand." + private static bool printModel = false; //"Print details of the model." + private static string parameters = ""; // "Sat parameters." + + private const long kDisconnectedDistance = -1L; + + static void Main(string[] args) + { + readArgs(args); + var builder = new NetworkRoutingDataBuilder(); + var data = builder.BuildModelFromParameters(clients, backbones, demands, trafficMin, trafficMax, + minClientDegree, maxClientDegree, minBackboneDegree, maxBackboneDegree, maxCapacity, fixedChargeCost, seed); + var solver = new NetworkRoutingSolver(); + solver.Init(data, extraHops, maxPaths); + var cost = solver.Solve(); + Console.WriteLine($"Final cost = {cost}"); + } + + private static void readArgs(string[] args) + { + readInt(args, ref clients, nameof(clients)); + readInt(args, ref backbones, nameof(backbones)); + readInt(args, ref demands, nameof(demands)); + readInt(args, ref trafficMin, nameof(trafficMin)); + readInt(args, ref trafficMax, nameof(trafficMax)); + readInt(args, ref minClientDegree, nameof(minClientDegree)); + readInt(args, ref maxClientDegree, nameof(maxClientDegree)); + readInt(args, ref minBackboneDegree, nameof(minBackboneDegree)); + readInt(args, ref maxBackboneDegree, nameof(maxBackboneDegree)); + readInt(args, ref maxCapacity, nameof(maxCapacity)); + readInt(args, ref fixedChargeCost, nameof(fixedChargeCost)); + readInt(args, ref seed, nameof(seed)); + readDouble(args, ref comfortZone, nameof(comfortZone)); + readInt(args, ref extraHops, nameof(extraHops)); + readInt(args, ref maxPaths, nameof(maxPaths)); + readBoolean(args, ref printModel, nameof(printModel)); + readString(args, ref parameters, nameof(parameters)); + } + + private static void readDouble(string[] args, ref double setting, string arg) + { + var v = getArgValue(args, arg); + if (v.IsSet) + { + setting = Convert.ToDouble(v.Value); + } + } + + private static void readInt(string[] args, ref int setting, string arg) + { + var v = getArgValue(args, arg); + if (v.IsSet) + { + setting = Convert.ToInt32(v.Value); + } + } + + private static void readBoolean(string[] args, ref bool setting, string arg) + { + var v = getArgValue(args, arg); + if (v.IsSet) + { + setting = Convert.ToBoolean(v.Value); + } + } + + private static void readString(string[] args, ref string setting, string arg) + { + var v = getArgValue(args, arg); + if (v.IsSet) + { + setting = v.Value; + } + } + + private static (bool IsSet, string Value) getArgValue(string[] args, string arg) + { + string lookup = $"--{arg}="; + + var item = args.FirstOrDefault(x => x.StartsWith(lookup)); + + if (string.IsNullOrEmpty(item)) + { + return (false, string.Empty); + } + + return (true, item.Replace(lookup, string.Empty)); + } + + /// + /// Contains problem data. It assumes capacities are symmetrical: + /// (capacity(i->j) == capacity(j->i)). + /// Demands are not symmetrical. + /// + public class NetworkRoutingData + { + private Dictionary<(int source, int destination), int> _arcs = new Dictionary<(int source, int destination), int>(); + private Dictionary<(int node1, int node2), int> _demands = new Dictionary<(int node1, int node2), int>(); + + public int NumberOfNodes { get; set; } = -1; + + public int NumberOfArcs + { + get { return _arcs.Count(); } + } + + public int NumberOfDemands + { + get { return _demands.Count(); } + } + + public int MaximumCapacity { get; set; } = -1; + public int FixedChargeCost { get; set; } = -1; + public string Name { get; set; } = string.Empty; + + public void AddDemand(int source, int destination, int traffic) + { + var pair = (source, destination); + if(!_demands.ContainsKey(pair)) + _demands.Add(pair, traffic); + } + + public void AddArc(int node1, int node2, int capacity) + { + _arcs.Add((Math.Min(node1, node2), Math.Max(node1, node2)), capacity); + } + + public int Demand(int source, int destination) + { + var pair = (source, destination); + if (_demands.TryGetValue(pair, out var demand)) + return demand; + + return 0; + } + + public int Capacity(int node1, int node2) + { + var pair = (Math.Min(node1, node2), Math.Max(node1, node2)); + if (_arcs.TryGetValue(pair, out var capacity)) + return capacity; + + return 0; + } + } + + /// + /// Random generator of problem. This generator creates a random + /// problem. This problem uses a special topology. There are + /// 'numBackbones' nodes and 'numClients' nodes. if 'numClients' is + /// null, then all backbones nodes are also client nodes. All traffic + /// originates and terminates in client nodes. Each client node is + /// connected to 'minClientDegree' - 'maxClientDegree' backbone + /// nodes. Each backbone node is connected to 'minBackboneDegree' - + /// 'maxBackboneDegree' other backbone nodes. There are 'numDemands' + /// demands, with a traffic between 'trafficMin' and 'trafficMax'. + /// Each arc has a capacity of 'maxCapacity'. Using an arc incurs a + /// fixed cost of 'fixedChargeCost'. + /// + public class NetworkRoutingDataBuilder + { + private List> _network; + private List _degrees; + private Random _random; + + public NetworkRoutingData BuildModelFromParameters(int numClients, int numBackbones, + int numDemands, int trafficMin, + int trafficMax, int minClientDegree, + int maxClientDegree, int minBackboneDegree, + int maxBackboneDegree, int maxCapacity, + int fixedChargeCost, int seed) + { + Debug.Assert(numBackbones >= 1); + Debug.Assert(numClients >= 0); + Debug.Assert(numDemands >= 1); + Debug.Assert(numDemands <= (numClients == 0 ? numBackbones * numBackbones : numClients * numBackbones)); + Debug.Assert(maxClientDegree >= minClientDegree); + Debug.Assert(maxBackboneDegree >= minBackboneDegree); + Debug.Assert(trafficMax >= 1); + Debug.Assert(trafficMax >= trafficMin); + Debug.Assert(trafficMin >= 1); + Debug.Assert(maxBackboneDegree >= 2); + Debug.Assert(maxClientDegree >= 2); + Debug.Assert(maxClientDegree <= numBackbones); + Debug.Assert(maxBackboneDegree <= numBackbones); + Debug.Assert(maxCapacity >= 1); + + int size = numBackbones + numClients; + initData(size, seed); + buildGraph(numClients, numBackbones, minClientDegree, maxClientDegree, minBackboneDegree, maxBackboneDegree); + NetworkRoutingData data = new NetworkRoutingData(); + createDemands(numClients, numBackbones, numDemands, trafficMin, trafficMax, data); + fillData(numClients, numBackbones, numDemands, trafficMin, trafficMax, minClientDegree, maxClientDegree, + minBackboneDegree, maxBackboneDegree, maxCapacity, fixedChargeCost, seed, data); + + return data; + } + + private void initData(int size, int seed) + { + _network = new List>(size); + for (int i = 0; i < size; i++) + { + _network.Add(new List(size)); + for (int j = 0; j < size; j++) + { + _network[i].Add(false); + } + } + + _degrees = new List(size); + for (int i = 0; i < size; i++) + { + _degrees.Add(0); + } + + _random = new Random(seed); + } + + private void buildGraph(int numClients, int numBackbones, int minClientDegree, + int maxClientDegree, int minBackboneDegree, int maxBackboneDegree) + { + int size = numBackbones + numClients; + for (int i = 1; i < numBackbones; i++) + { + int j = randomUniform(i); + addEdge(i, j); + } + + List notFull = new List(); + HashSet toComplete = new HashSet(); + + for (int i = 0; i < numBackbones; i++) + { + if (_degrees[i] < minBackboneDegree) + { + toComplete.Add(i); + } + + if (_degrees[i] < maxBackboneDegree) + { + notFull.Add(i); + } + } + + while (toComplete.Any() && notFull.Count > 1) + { + int node1 = getNextToComplete(toComplete); + int node2 = node1; + while (node2 == node1 || _degrees[node2] >= maxBackboneDegree) + { + node2 = randomUniform(numBackbones); + } + + addEdge(node1, node2); + + if (_degrees[node1] >= minBackboneDegree) + { + toComplete.Remove(node1); + } + + if (_degrees[node2] >= minBackboneDegree) + { + toComplete.Remove(node2); + } + + if (_degrees[node1] >= maxBackboneDegree) + { + notFull.Remove(node1); + } + + if (_degrees[node2] >= maxBackboneDegree) + { + notFull.Remove(node2); + } + } + + // Then create the client nodes connected to the backbone nodes. + // If numClient is 0, then backbone nodes are also client nodes. + + for (int i = numBackbones; i < size; i++) + { + int degree = randomInInterval(minClientDegree, maxClientDegree); + + while (_degrees[i] < degree) + { + int j = randomUniform(numBackbones); + if (!_network[i][j]) + { + addEdge(i, j); + } + } + } + } + + private int getNextToComplete(HashSet toComplete) + { + return toComplete.Last(); + } + + private void createDemands(int numClients, int numBackbones, int numDemands, + int trafficMin, int trafficMax, NetworkRoutingData data) + { + while (data.NumberOfDemands < numDemands) + { + int source = randomClient(numClients, numBackbones); + int dest = source; + while (dest == source) + { + dest = randomClient(numClients, numBackbones); + } + + int traffic = randomInInterval(trafficMin, trafficMax); + data.AddDemand(source, dest, traffic); + } + } + + private void fillData(int numClients, int numBackbones, int numDemands, + int trafficMin, int trafficMax, int minClientDegree, + int maxClientDegree, int minBackboneDegree, + int maxBackboneDegree, int maxCapacity, + int fixedChargeCost, int seed, + NetworkRoutingData data) + { + int size = numBackbones + numClients; + string name = $"mp_c{numClients}_b{numBackbones}_d{numDemands}.t{trafficMin}-{trafficMax}.cd{minClientDegree}-{maxClientDegree}.bd{minBackboneDegree}-{maxBackboneDegree}.mc{maxCapacity}.fc{fixedChargeCost}.s{seed}"; + data.Name = name; + + data.NumberOfNodes = size; + int numArcs = 0; + for (int i = 0; i < size - 1; i++) + { + for (int j = i + 1; j < size; j++) + { + if (_network[i][j]) + { + data.AddArc(i, j, maxCapacity); + numArcs++; + } + } + } + + data.MaximumCapacity = maxCapacity; + data.FixedChargeCost = fixedChargeCost; + } + + private void addEdge(int i, int j) + { + _degrees[i]++; + _degrees[j]++; + _network[i][j] = true; + _network[j][i] = true; + } + + private int randomInInterval(int intervalMin, int intervalMax) + { + var p = randomUniform(intervalMax - intervalMin + 1) + intervalMin; + return p; + } + + private int randomClient(int numClients, int numBackbones) + { + var p = (numClients == 0) + ? randomUniform(numBackbones) + : randomUniform(numClients) + numBackbones; + return p; + } + + private int randomUniform(int max) + { + var r = _random.Next(max); + return r; + } + } + + [DebuggerDisplay("Source {Source} Destination {Destination} Traffic {Traffic}")] + public struct Demand + { + public Demand(int source, int destination, int traffic) + { + Source = source; + Destination = destination; + Traffic = traffic; + } + + public int Source { get; } + public int Destination { get; } + public int Traffic { get; } + } + + public class NetworkRoutingSolver + { + private List<(long source, long destination, int arcId)> _arcsData = new List<(long source, long destination, int arcId)>(); + private List _arcCapacity = new List(); + private List _demands = new List(); + private List _allMinPathLengths = new List(); + private List> _capacity; + private List>> _allPaths; + + public int NumberOfNodes { get; private set; } = -1; + + private int countArcs + { + get { return _arcsData.Count / 2; } + } + + public void ComputeAllPathsForOneDemandAndOnePathLength(int demandIndex, int maxLength, int maxPaths) + { + // We search for paths of length exactly 'maxLength'. + CpModel cpModel = new CpModel(); + var arcVars = new List(); + var nodeVars = new List(); + + for (int i = 0; i < maxLength; i++) + { + nodeVars.Add(cpModel.NewIntVar(0, NumberOfNodes - 1, string.Empty)); + } + + for (int i = 0; i < maxLength - 1; i++) + { + arcVars.Add(cpModel.NewIntVar(-1, countArcs - 1, string.Empty)); + } + + var arcs = getArcsData(); + + for (int i = 0; i < maxLength - 1; i++) + { + var tmpVars = new List(); + tmpVars.Add(nodeVars[i]); + tmpVars.Add(nodeVars[i + 1]); + tmpVars.Add(arcVars[i]); + var table = cpModel.AddAllowedAssignments(tmpVars, arcs); + } + + var demand = _demands[demandIndex]; + cpModel.Add(nodeVars[0] == demand.Source); + cpModel.Add(nodeVars[maxLength - 1] == demand.Destination); + cpModel.AddAllDifferent(arcVars); + cpModel.AddAllDifferent(nodeVars); + + var solver = new CpSolver(); + + var solutionPrinter = new FeasibleSolutionChecker(demandIndex, ref _allPaths, maxLength, arcVars, maxPaths, nodeVars); + var status = solver.SearchAllSolutions(cpModel, solutionPrinter); + } + + private long[,] getArcsData() + { + long[,] arcs = new long[_arcsData.Count, 3]; + + for (int i = 0; i < _arcsData.Count; i++) + { + var data = _arcsData[i]; + arcs[i, 0] = data.source; + arcs[i, 1] = data.destination; + arcs[i, 2] = data.arcId; + } + + return arcs; + } + + public int ComputeAllPaths(int extraHops, int maxPaths) + { + int numPaths = 0; + for (int demandIndex = 0; demandIndex < _demands.Count; demandIndex++) + { + int minPathLength = _allMinPathLengths[demandIndex]; + + for (int maxLength = minPathLength + 1; maxLength <= minPathLength + extraHops+1; maxLength++) + { + ComputeAllPathsForOneDemandAndOnePathLength(demandIndex, maxLength, maxPaths); + + if (_allPaths[demandIndex].Count >= maxPaths) + break; + } + + numPaths += _allPaths[demandIndex].Count; + } + + return numPaths; + } + + public void AddArcData(long source, long destination, int arcId) + { + _arcsData.Add((source, destination, arcId)); + } + + public void InitArcInfo(NetworkRoutingData data) + { + int numArcs = data.NumberOfArcs; + _capacity = new List>(NumberOfNodes); + + for (int nodeIndex = 0; nodeIndex < NumberOfNodes; nodeIndex++) + { + _capacity.Add(new List(NumberOfNodes)); + for (int i = 0; i < NumberOfNodes; i++) + { + _capacity[nodeIndex].Add(0); + } + } + + int arcId = 0; + for (int i = 0; i < NumberOfNodes - 1; i++) + { + for (int j = i + 1; j < NumberOfNodes; j++) + { + int capacity = data.Capacity(i, j); + if (capacity > 0) + { + AddArcData(i, j, arcId); + AddArcData(j, i, arcId); + arcId++; + _arcCapacity.Add(capacity); + _capacity[i][j] = capacity; + _capacity[j][i] = capacity; + + if (printModel) + { + Console.WriteLine($"Arc {i} <-> {j} with capacity {capacity}"); + } + } + } + } + + Debug.Assert(arcId == numArcs); + } + + public int InitDemandInfo(NetworkRoutingData data) + { + int numDemands = data.NumberOfDemands; + int totalDemand = 0; + for (int i = 0; i < NumberOfNodes; i++) + { + for (int j = 0; j < NumberOfNodes; j++) + { + int traffic = data.Demand(i, j); + if (traffic > 0) + { + _demands.Add(new Demand(i, j, traffic)); + totalDemand += traffic; + } + } + } + + Debug.Assert(numDemands == _demands.Count); + + return totalDemand; + } + + public long InitShortestPaths(NetworkRoutingData data) + { + int numDemands = data.NumberOfDemands; + long totalCumulatedTraffic = 0L; + _allMinPathLengths.Clear(); + var paths = new List(); + + for (int demandIndex = 0; demandIndex < numDemands; demandIndex++) + { + paths.Clear(); + var demand = _demands[demandIndex]; + var r = DijkstraShortestPath(NumberOfNodes, demand.Source, demand.Destination, + ((int x, int y) p) => hasArc(p.x, p.y), kDisconnectedDistance, paths); + + _allMinPathLengths.Add(paths.Count - 1); + var minPathLength = _allMinPathLengths[demandIndex]; + totalCumulatedTraffic += minPathLength * demand.Traffic; + } + + return totalCumulatedTraffic; + } + + public int InitPaths(NetworkRoutingData data, int extraHops, int maxPaths) + { + var numDemands = data.NumberOfDemands; + Console.WriteLine("Computing all possible paths "); + Console.WriteLine($" - extra hops = {extraHops}"); + Console.WriteLine($" - max paths per demand = {maxPaths}"); + + _allPaths =new List>>(numDemands); + + var numPaths = ComputeAllPaths(extraHops, maxPaths); + + for (int demandIndex = 0; demandIndex < numDemands; demandIndex++) + { + var demand = _demands[demandIndex]; + Console.WriteLine($"Demand from {demand.Source} to {demand.Destination} with traffic {demand.Traffic}, amd {_allPaths[demandIndex].Count} possible paths."); + } + + return numPaths; + } + + public void Init(NetworkRoutingData data, int extraHops, int maxPaths) + { + Console.WriteLine($"Model {data.Name}"); + NumberOfNodes = data.NumberOfNodes; + var numArcs = data.NumberOfArcs; + var numDemands = data.NumberOfDemands; + + InitArcInfo(data); + var totalDemand = InitDemandInfo(data); + var totalAccumulatedTraffic = InitShortestPaths(data); + var numPaths = InitPaths(data, extraHops, maxPaths); + + Console.WriteLine("Model created:"); + Console.WriteLine($" - {NumberOfNodes} nodes"); + Console.WriteLine($" - {numArcs} arcs"); + Console.WriteLine($" - {numDemands} demands"); + Console.WriteLine($" - a total traffic of {totalDemand}"); + Console.WriteLine($" - a minimum cumulated traffic of {totalAccumulatedTraffic}"); + Console.WriteLine($" - {numPaths} possible paths for all demands"); + } + + private long hasArc(int i, int j) + { + if (_capacity[i][j] > 0) + return 1; + else + return kDisconnectedDistance; + } + + public long Solve() + { + Console.WriteLine("Solving model"); + var numDemands = _demands.Count; + var numArcs = countArcs; + + CpModel cpModel = new CpModel(); + + var pathVars = new List>(numDemands); + + for (int demandIndex = 0; demandIndex < numDemands; demandIndex++) + { + pathVars.Add(new List()); + + for (int arc = 0; arc < numArcs; arc++) + { + pathVars[demandIndex].Add(cpModel.NewBoolVar("")); + } + + long[,] tuples = new long[_allPaths[demandIndex].Count, numArcs]; + + int pathCount = 0; + foreach (var set in _allPaths[demandIndex]) + { + foreach (var arc in set) + { + tuples[pathCount, arc] = 1; + } + + pathCount++; + } + + var pathCt = cpModel.AddAllowedAssignments(pathVars[demandIndex].ToArray(), tuples); + } + + var trafficVars = new List(numArcs); + var normalizedTrafficVars = new List(numArcs); + var comfortableTrafficVars = new List(numArcs); + + long maxNormalizedTraffic = 0; + + for (int arcIndex = 0; arcIndex < numArcs; arcIndex++) + { + long sumOfTraffic = 0; + + var vars = new List(); + var traffics = new List(); + + for (int i = 0; i < pathVars.Count; i++) + { + sumOfTraffic += _demands[i].Traffic; + vars.Add(pathVars[i][arcIndex]); + traffics.Add(_demands[i].Traffic); + } + + var sum = vars.ToArray().ScalProd(traffics.ToArray()); + var trafficVar = cpModel.NewIntVar(0, sumOfTraffic, $"trafficVar{arcIndex}"); + trafficVars.Add(trafficVar); + cpModel.Add(sum == trafficVar); + + var capacity = _arcCapacity[arcIndex]; + var scaledTraffic = cpModel.NewIntVar(0, sumOfTraffic * 1000, $"scaledTrafficVar{arcIndex}"); + var scaledTrafficVar = new[] {trafficVar}.ScalProd(new[] {1000}); + cpModel.Add(scaledTrafficVar == scaledTraffic); + + var normalizedTraffic = + cpModel.NewIntVar(0, sumOfTraffic * 1000 / capacity, $"normalizedTraffic{arcIndex}"); + + maxNormalizedTraffic = Math.Max(maxNormalizedTraffic, sumOfTraffic * 1000 / capacity); + cpModel.AddDivisionEquality(normalizedTraffic, scaledTraffic, cpModel.NewConstant(capacity)); + normalizedTrafficVars.Add(normalizedTraffic); + var comfort = cpModel.NewBoolVar($"comfort{arcIndex}"); + var safeCapacity = (long)(capacity * comfortZone); + cpModel.Add(trafficVar > safeCapacity).OnlyEnforceIf(comfort); + cpModel.Add(trafficVar <=safeCapacity).OnlyEnforceIf(comfort.Not()); + comfortableTrafficVars.Add(comfort); + } + + var maxUsageCost = cpModel.NewIntVar(0, maxNormalizedTraffic, "maxUsageCost"); + cpModel.AddMaxEquality(maxUsageCost, normalizedTrafficVars); + + var obj = new List() {maxUsageCost}; + obj.AddRange(comfortableTrafficVars); + cpModel.Minimize(obj.ToArray().Sum()); + + CpSolver solver = new CpSolver(); + solver.StringParameters = parameters; + + int numSolutions = 0; + CpSolverStatus status = solver.SearchAllSolutions(cpModel, + new FeasibleSolutionChecker2(maxUsageCost, comfortableTrafficVars, trafficVars)); + + return (long)solver.ObjectiveValue; + + } + } + + private class DijkstraSP + { + private const long kInfinity = long.MaxValue / 2; + + private readonly Func<(int, int), long> _graph; + private readonly int[] _predecessor; + private readonly List _elements; + private readonly AdjustablePriorityQueue _frontier; + private readonly List _notVisited = new List(); + private readonly List _addedToFrontier = new List(); + + public DijkstraSP(int nodeCount, int startNode, Func<(int,int), long> graph, long disconnectedDistance) + { + NodeCount = nodeCount; + StartNode = startNode; + this._graph = graph; + DisconnectedDistance = disconnectedDistance; + _predecessor = new int[nodeCount]; + _elements = new List(nodeCount); + _frontier = new AdjustablePriorityQueue(); + } + + public int NodeCount { get; } + public int StartNode { get; } + public long DisconnectedDistance { get; } + + public bool ShortestPath(int endNode, List nodes) + { + initialize(); + bool found = false; + while (!_frontier.IsEmpty) + { + long distance; + int node = selectClosestNode(out distance); + if (distance == kInfinity) + { + found = false; + break; + } + else if (node == endNode) + { + found = true; + break; + } + update(node); + } + + if (found) + { + findPath(endNode, nodes); + } + + return found; + + } + + private void initialize() + { + for (int i = 0; i < NodeCount; i++) + { + _elements.Add(new Element {Node = i}); + + if (i == StartNode) + { + _predecessor[i] = -1; + _elements[i].Distance = 0; + _frontier.Add(_elements[i]); + } + else + { + _elements[i].Distance = kInfinity; + _predecessor[i] = StartNode; + _notVisited.Add(i); + } + } + } + + private int selectClosestNode(out long distance) + { + var node = _frontier.Top().Node; + distance = _frontier.Top().Distance; + _frontier.Pop(); + _notVisited.Remove(node); + _addedToFrontier.Remove(node); + return node; + } + + private void update(int node) + { + foreach (var otherNode in _notVisited) + { + var graphNode = _graph((node, otherNode)); + + if (graphNode != DisconnectedDistance) + { + if (!_addedToFrontier.Contains(otherNode)) + { + _frontier.Add(_elements[otherNode]); + _addedToFrontier.Add(otherNode); + } + + var otherDistance = _elements[node].Distance + graphNode; + + if (_elements[otherNode].Distance > otherDistance) + { + _elements[otherNode].Distance = otherDistance; + _frontier.NoteChangedPriority(_elements[otherNode]); + _predecessor[otherNode] = node; + } + } + } + } + + private void findPath(int dest, List nodes) + { + var j = dest; + nodes.Add(j); + while (_predecessor[j] != -1) + { + nodes.Add(_predecessor[j]); + j = _predecessor[j]; + } + } + } + + public static bool DijkstraShortestPath(int nodeCount, int startNode, int endNode, Func<(int, int), long> graph, + long disconnectedDistance, List nodes) + { + DijkstraSP bf = new DijkstraSP(nodeCount, startNode, graph, disconnectedDistance); + return bf.ShortestPath(endNode, nodes); + } + + [DebuggerDisplay("Node = {Node}, HeapIndex = {HeapIndex}, Distance = {Distance}")] + private class Element : IHasHeapIndex, IComparable + { + public int HeapIndex { get; set; } = -1; + public long Distance { get; set; } = 0; + public int Node { get; set; } = -1; + + public int CompareTo(Element other) + { + if (this.Distance > other.Distance) + return -1; + + if (this.Distance < other.Distance) + return 1; + + return 0; + } + } + + private class AdjustablePriorityQueue where T: class, IHasHeapIndex, IComparable + { + private readonly List _elems = new List(); + + public void Add(T val) + { + _elems.Add(val); + adjustUpwards(_elems.Count - 1); + } + + public void Remove(T val) + { + var i = val.HeapIndex; + if (i == _elems.Count - 1) + { + _elems.RemoveAt(_elems.Count - 1); + return; + } + + _elems[i] = _elems.Last(); + _elems[i].HeapIndex = i; + _elems.RemoveAt(_elems.Count - 1); + NoteChangedPriority(_elems[i]); + } + + public bool Contains(T val) + { + var i = val.HeapIndex; + if (i < 0 || i >= _elems.Count || _elems[i].CompareTo(val) != 0) + return false; + + return true; + } + + public T Top() + { + return _elems[0]; + } + + public void Pop() + { + Remove(Top()); + } + + public int Size() + { + return _elems.Count; + } + + public bool IsEmpty + { + get { return !_elems.Any(); } + } + + public void Clear() + { + _elems.Clear(); + } + + public void CheckValid() + { + for (int i = 0; i < _elems.Count; i++) + { + var leftChild = 1 + 2 * i; + if (leftChild < _elems.Count) + { + var compare = _elems[i].CompareTo(_elems[leftChild]); + Debug.Assert(compare >=0); + } + + int rightChild = leftChild + 1; + if (rightChild < _elems.Count) + { + var compare = _elems[i].CompareTo(_elems[rightChild]); + Debug.Assert(compare >= 0); + } + } + } + + public void NoteChangedPriority(T val) + { + if (_elems.Count == 0) + return; + + var i = val.HeapIndex; + var parent = (i - 1) / 2; + if (_elems[parent].CompareTo(val) == -1) + { + adjustUpwards(i); + } + else + { + adjustDownwards(i); + } + } + + private void adjustUpwards(int i) + { + var t = _elems[i]; + while (i > 0) + { + var parent = (i - 1) / 2; + if (_elems[parent].CompareTo(t) != -1) + { + break; + } + + _elems[i] = _elems[parent]; + _elems[i].HeapIndex = i; + i = parent; + } + + _elems[i] = t; + t.HeapIndex = i; + } + + private void adjustDownwards(int i) + { + var t = _elems[i]; + while (true) + { + var leftChild = 1 + 2 * i; + if (leftChild >= _elems.Count) + { + break; + } + + var rightChild = leftChild + 1; + var next = (rightChild < _elems.Count && _elems[leftChild].CompareTo(_elems[rightChild]) == -1) + ? rightChild + : leftChild; + + if (t.CompareTo(_elems[next]) != -1) + { + break; + } + + _elems[i] = _elems[next]; + _elems[i].HeapIndex = i; + i = next; + } + + _elems[i] = t; + t.HeapIndex = i; + } + } + + public interface IHasHeapIndex + { + int HeapIndex { get; set; } + } + + private class FeasibleSolutionChecker : CpSolverSolutionCallback + { + public FeasibleSolutionChecker(int demandIndex, ref List>> allPaths, int maxLength, List arcVars, int maxPaths, List nodeVars) + { + DemandIndex = demandIndex; + AllPaths = allPaths; + MaxLength = maxLength; + ArcVars = arcVars; + MaxPaths = maxPaths; + NodeVars = nodeVars; + } + + public int DemandIndex { get; } + public List>> AllPaths { get; } + public int MaxLength { get; } + public List ArcVars { get; } + public int MaxPaths { get; } + public List NodeVars { get; } + + public override void OnSolutionCallback() + { + if(AllPaths.Count < DemandIndex + 1) + AllPaths.Add(new List>()); + + int pathId = AllPaths[DemandIndex].Count; + AllPaths[DemandIndex].Add(new HashSet()); + + for (int i = 0; i < MaxLength - 1; i++) + { + int arc = (int) this.SolutionIntegerValue(ArcVars[i].GetIndex()); + AllPaths[DemandIndex][pathId].Add(arc); + } + + if (AllPaths[DemandIndex].Count() >= MaxPaths) + { + StopSearch(); + } + } + } + + private class FeasibleSolutionChecker2 : CpSolverSolutionCallback + { + public IntVar MaxUsageCost { get; } + public List ComfortableTrafficVars { get; } + public List TrafficVars { get; } + private int _numSolutions = 0; + + public FeasibleSolutionChecker2(IntVar maxUsageCost, List comfortableTrafficVars, List trafficVars) + { + MaxUsageCost = maxUsageCost; + ComfortableTrafficVars = comfortableTrafficVars; + TrafficVars = trafficVars; + } + + public override void OnSolutionCallback() + { + Console.WriteLine($"Solution {_numSolutions}"); + var percent = SolutionIntegerValue(MaxUsageCost.GetIndex()) / 10.0; + int numNonComfortableArcs = 0; + + foreach (var comfort in ComfortableTrafficVars) + { + numNonComfortableArcs += SolutionBooleanValue(comfort.GetIndex()) ? 1 : 0; + } + + if (numNonComfortableArcs > 0) + { + Console.WriteLine($"*** Found a solution with a max usage of {percent}%, and {numNonComfortableArcs} links above the comfort zone"); + } + else + { + Console.WriteLine($"*** Found a solution with a max usage of {percent}%"); + } + + _numSolutions++; + } + } +} diff --git a/examples/dotnet/NetworkRoutingSat.csproj b/examples/dotnet/NetworkRoutingSat.csproj new file mode 100644 index 0000000000..bf16af6454 --- /dev/null +++ b/examples/dotnet/NetworkRoutingSat.csproj @@ -0,0 +1,22 @@ + + + Exe + 7.2 + netcoreapp2.1 + false + ../../packages;$(RestoreSources);https://api.nuget.org/v3/index.json + Google.OrTools.NetworkRoutingSat + true + + + + full + true + true + + + + + + +