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
+
+
+
+
+
+
+