Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions Routers/Routers.Cli/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using Routers;

if (args.Length < 2)
{
Console.WriteLine("""
Network topology optimizer
Usage:
dotnet run -- {inputFile} {outputFile} [-f|--force]

Arguments:
inputFile file path to read network topology from
outputFile file path to write optimized network topology to
-f or --force overwrite outputFile if it already exists
""");
return 0;
}

var inputFilePath = args[0];
var outputFilePath = args[1];

if (!File.Exists(inputFilePath))
{
Console.Error.WriteLine($"error: input file '{inputFilePath}' does not exist");
return 1;
}

var force = args.Length >= 3 && args[2] is "-f" or "--force";

if (!force && File.Exists(outputFilePath))
{
Console.Write($"File '{outputFilePath}' already exists, overwrite? (y/n): ");
if (Console.ReadLine()?.Trim() != "y")
{
Console.WriteLine("Cancelled");
return 0;
}
}

Graph graph;
using (var inputFile = File.OpenText(inputFilePath))
{
try
{
graph = Graph.ReadGraph(inputFile);
}
catch (InvalidDataException)
{
Console.Error.WriteLine($"error: invalid input file format");
return 1;
}
}

if (!GraphOptimizer.Optimize(graph, out var optimized))
{
Console.Error.WriteLine($"error: graph is disconected");
return 1;
}

using var outputFile = File.CreateText(outputFilePath);
optimized.Write(outputFile);

return 0;
14 changes: 14 additions & 0 deletions Routers/Routers.Cli/Routers.Cli.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="../Routers/Routers.csproj"/>
</ItemGroup>

</Project>
28 changes: 28 additions & 0 deletions Routers/Routers.Tests/Routers.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="NUnit" Version="4.2.2" />
<PackageReference Include="NUnit.Analyzers" Version="4.4.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
</ItemGroup>

<ItemGroup>
<Using Include="NUnit.Framework" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="../Routers/Routers.csproj"/>
</ItemGroup>

</Project>
62 changes: 62 additions & 0 deletions Routers/Routers.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Routers", "Routers\Routers.csproj", "{19D4C610-E237-45B9-A8D2-A52A9FCD9986}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Routers.Tests", "Routers.Tests\Routers.Tests.csproj", "{91AF46DD-D4BB-4CAF-A8CB-E06715C013FA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Routers.Cli", "Routers.Cli\Routers.Cli.csproj", "{1A5C197A-399A-4808-BC0C-C033CE664E19}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{19D4C610-E237-45B9-A8D2-A52A9FCD9986}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{19D4C610-E237-45B9-A8D2-A52A9FCD9986}.Debug|Any CPU.Build.0 = Debug|Any CPU
{19D4C610-E237-45B9-A8D2-A52A9FCD9986}.Debug|x64.ActiveCfg = Debug|Any CPU
{19D4C610-E237-45B9-A8D2-A52A9FCD9986}.Debug|x64.Build.0 = Debug|Any CPU
{19D4C610-E237-45B9-A8D2-A52A9FCD9986}.Debug|x86.ActiveCfg = Debug|Any CPU
{19D4C610-E237-45B9-A8D2-A52A9FCD9986}.Debug|x86.Build.0 = Debug|Any CPU
{19D4C610-E237-45B9-A8D2-A52A9FCD9986}.Release|Any CPU.ActiveCfg = Release|Any CPU
{19D4C610-E237-45B9-A8D2-A52A9FCD9986}.Release|Any CPU.Build.0 = Release|Any CPU
{19D4C610-E237-45B9-A8D2-A52A9FCD9986}.Release|x64.ActiveCfg = Release|Any CPU
{19D4C610-E237-45B9-A8D2-A52A9FCD9986}.Release|x64.Build.0 = Release|Any CPU
{19D4C610-E237-45B9-A8D2-A52A9FCD9986}.Release|x86.ActiveCfg = Release|Any CPU
{19D4C610-E237-45B9-A8D2-A52A9FCD9986}.Release|x86.Build.0 = Release|Any CPU
{91AF46DD-D4BB-4CAF-A8CB-E06715C013FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{91AF46DD-D4BB-4CAF-A8CB-E06715C013FA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{91AF46DD-D4BB-4CAF-A8CB-E06715C013FA}.Debug|x64.ActiveCfg = Debug|Any CPU
{91AF46DD-D4BB-4CAF-A8CB-E06715C013FA}.Debug|x64.Build.0 = Debug|Any CPU
{91AF46DD-D4BB-4CAF-A8CB-E06715C013FA}.Debug|x86.ActiveCfg = Debug|Any CPU
{91AF46DD-D4BB-4CAF-A8CB-E06715C013FA}.Debug|x86.Build.0 = Debug|Any CPU
{91AF46DD-D4BB-4CAF-A8CB-E06715C013FA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{91AF46DD-D4BB-4CAF-A8CB-E06715C013FA}.Release|Any CPU.Build.0 = Release|Any CPU
{91AF46DD-D4BB-4CAF-A8CB-E06715C013FA}.Release|x64.ActiveCfg = Release|Any CPU
{91AF46DD-D4BB-4CAF-A8CB-E06715C013FA}.Release|x64.Build.0 = Release|Any CPU
{91AF46DD-D4BB-4CAF-A8CB-E06715C013FA}.Release|x86.ActiveCfg = Release|Any CPU
{91AF46DD-D4BB-4CAF-A8CB-E06715C013FA}.Release|x86.Build.0 = Release|Any CPU
{1A5C197A-399A-4808-BC0C-C033CE664E19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1A5C197A-399A-4808-BC0C-C033CE664E19}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1A5C197A-399A-4808-BC0C-C033CE664E19}.Debug|x64.ActiveCfg = Debug|Any CPU
{1A5C197A-399A-4808-BC0C-C033CE664E19}.Debug|x64.Build.0 = Debug|Any CPU
{1A5C197A-399A-4808-BC0C-C033CE664E19}.Debug|x86.ActiveCfg = Debug|Any CPU
{1A5C197A-399A-4808-BC0C-C033CE664E19}.Debug|x86.Build.0 = Debug|Any CPU
{1A5C197A-399A-4808-BC0C-C033CE664E19}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1A5C197A-399A-4808-BC0C-C033CE664E19}.Release|Any CPU.Build.0 = Release|Any CPU
{1A5C197A-399A-4808-BC0C-C033CE664E19}.Release|x64.ActiveCfg = Release|Any CPU
{1A5C197A-399A-4808-BC0C-C033CE664E19}.Release|x64.Build.0 = Release|Any CPU
{1A5C197A-399A-4808-BC0C-C033CE664E19}.Release|x86.ActiveCfg = Release|Any CPU
{1A5C197A-399A-4808-BC0C-C033CE664E19}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
160 changes: 160 additions & 0 deletions Routers/Routers/Graph.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
namespace Routers;

using System.Text.RegularExpressions;

/// <summary>
/// Network graph.
/// </summary>
public partial class Graph
{
/// <summary>
/// Initializes a new instance of the <see cref="Graph"/> class.
/// </summary>
/// <param name="nodes">Nodes of the graph.</param>
internal Graph(IEnumerable<Node> nodes)
{
Nodes = [.. nodes];
}

/// <summary>
/// Gets all nodes in graph.
/// </summary>
internal List<Node> Nodes { get; }

/// <summary>
/// Reads graph from <paramref name="reader"/>.
/// </summary>
/// <param name="reader"><see cref="TextReader"/> to read graph from.</param>
/// <returns>Read graph.</returns>
public static Graph ReadGraph(TextReader reader)
{
var graph = new Graph([]);
var nodes = new Dictionary<int, Node>();

while (true)
{
var line = reader.ReadLine();
if (line == null)
{
break;
}

var nodeMatch = NodeRegex().Match(line);
if (!nodeMatch.Success || nodeMatch.Groups.Count != 3 ||
!int.TryParse(nodeMatch.Groups[1].ValueSpan, out int nodeIndex))
{
throw new InvalidDataException();
}

if (!nodes.TryGetValue(nodeIndex, out var node))
{
node = new([]);
nodes[nodeIndex] = node;
graph.Nodes.Add(node);
}

var rawConnections = nodeMatch.Groups[2].Value.Split(',');
var connections = new Dictionary<int, int>();
foreach (var connection in rawConnections)
{
var connectionMatch = ConnectionRegex().Match(connection);
if (!connectionMatch.Success || connectionMatch.Groups.Count != 3)
{
throw new InvalidDataException();
}

if (!int.TryParse(connectionMatch.Groups[1].ValueSpan, out int neighborIndex) ||
!int.TryParse(connectionMatch.Groups[2].ValueSpan, out int bandwidth) ||
connections.ContainsKey(neighborIndex))
{
throw new InvalidDataException();
}

connections[neighborIndex] = bandwidth;
}

foreach (var (neighborIndex, bandwidth) in connections)
{
if (!nodes.TryGetValue(neighborIndex, out var neighbor))
{
neighbor = new([]);
nodes[neighborIndex] = neighbor;
graph.Nodes.Add(neighbor);
}

node.Neighbors[neighbor] = bandwidth;
neighbor.Neighbors[node] = bandwidth;
}
}

return graph;
}

/// <summary>
/// Writes graph to <paramref name="writer"/>.
/// </summary>
/// <param name="writer"><see cref="TextWriter"/> to write graph to.</param>
public void Write(TextWriter writer)
{
var wroteConnections = new HashSet<(Node, Node)>();
var nodeIndexLookup = new Dictionary<Node, int>();
int lastIndex = 0;

int GetIndex(Node node)
{
if (!nodeIndexLookup.TryGetValue(node, out int index))
{
nodeIndexLookup[node] = lastIndex;
index = lastIndex;
lastIndex++;
}

return index;
}

foreach (var node in Nodes)
{
int neighborCount = 0;
foreach (var (neighbor, bandwidth) in node.Neighbors)
{
if (wroteConnections.Contains((node, neighbor)) ||
wroteConnections.Contains((neighbor, node)))
{
continue;
}

wroteConnections.Add((node, neighbor));
wroteConnections.Add((neighbor, node));

if (neighborCount == 0)
{
writer.Write($"{GetIndex(node)}: ");
}
else if (neighborCount == 1)
{
writer.Write(", ");
}

writer.Write($"{GetIndex(neighbor)} ({bandwidth})");
neighborCount++;
}

if (neighborCount != 0)
{
writer.WriteLine();
}
}
}

[GeneratedRegex(@"^\s*(\d+)\s*:(.*)$")]
private static partial Regex NodeRegex();

[GeneratedRegex(@"\s*(\d+)\s*\(\s*(\d+)\s*\)\s*")]
private static partial Regex ConnectionRegex();

/// <summary>
/// Graph node.
/// </summary>
/// <param name="Neighbors">Neighbors of node, stored as pairs of <see cref="Node"/> and bandwidth.</param>
internal record Node(Dictionary<Node, int> Neighbors);
}
68 changes: 68 additions & 0 deletions Routers/Routers/GraphOptimizer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
namespace Routers;

using System.Diagnostics.CodeAnalysis;

/// <summary>
/// Utility class for optimizing network graphs.
/// </summary>
public static class GraphOptimizer
{
/// <summary>
/// Optimizes network graph.
/// </summary>
/// <param name="graph">Graph to optimize.</param>
/// <param name="optimized">When this method returns, contains optimized graph, if read successfully, <see langword="null"/> otherwise.</param>
/// <returns><see langword="true"/> if <paramref name="graph"/> is connected, <see langword="false"/> otherwise.</returns>
public static bool Optimize(Graph graph, [MaybeNullWhen(false)] out Graph optimized)
{
// use negative weights as workaround, so +1 is greater than any broadband
var broadbands = graph.Nodes.ToDictionary(x => x, x => 1);
var visited = new HashSet<Graph.Node>();

var queue = new PriorityQueue<Edge, int>();
queue.Enqueue(new(graph.Nodes[0], graph.Nodes[0]), 0);

broadbands[graph.Nodes[0]] = 0;

var newNodes = new List<Graph.Node>();
var oldToNewMap = new Dictionary<Graph.Node, Graph.Node>();
while (queue.TryDequeue(out Edge edge, out int broadband))
{
var node = edge.To;
visited.Add(node);

var newNode = new Graph.Node([]);
newNodes.Add(newNode);
oldToNewMap[node] = newNode;
if (edge.From != edge.To)
{
oldToNewMap[edge.From].Neighbors[edge.To] = -broadband;
}

foreach (var (neighbor, neighborBroadband) in node.Neighbors)
{
if (visited.Contains(neighbor))
{
continue;
}

if (neighborBroadband > broadbands[neighbor])
{
broadbands[neighbor] = neighborBroadband;
queue.Enqueue(new(node, neighbor), -neighborBroadband);
}
}
}

if (visited.Count != graph.Nodes.Count)
{
optimized = null;
return false;
}

optimized = new(newNodes);
return true;
}

private readonly record struct Edge(Graph.Node From, Graph.Node To);
}
Loading