Skip to content

Commit ae46694

Browse files
committed
Add start vertex support to topological sort
Allow Get-GraphTopologicalSort to limit sorting to the subgraph reachable from a provided StartVertex. The cmdlet now canonicalizes the start vertex against the graph, builds a reachable subgraph by following outgoing edges, and applies the existing forward/reverse topological ordering to that subgraph only. This makes reverse topological traversal usable for computation graphs where callers want to backpropagate from a selected output vertex while ignoring independent graph components. Unreachable cycles no longer block StartVertex-scoped sorting, while cycles reachable from the start vertex still fail as before. Add tests for reachable ordering, reverse ordering, missing start vertices, and unreachable cycles. Update the Get-GraphTopologicalSort documentation with the new parameter and example.
1 parent 5aaa6bb commit ae46694

3 files changed

Lines changed: 193 additions & 4 deletions

File tree

PSGraph.Tests/GetGraphTopologicalSortTests.cs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,88 @@ public void GetGraphTopologicalSort_NonUniqueLabels_ReturnsDistinctVertices()
114114
AssertTopologicalOrder(graph, vertices!);
115115
}
116116

117+
[Fact]
118+
public void GetGraphTopologicalSort_StartVertex_ReturnsReachableVerticesInTopologicalOrder()
119+
{
120+
var graph = CreateDiamondGraph();
121+
var x = new PSVertex("X");
122+
var y = new PSVertex("Y");
123+
graph.AddVerticesAndEdge(new PSEdge(x, y));
124+
125+
var startVertex = graph.Vertices.Single(vertex => vertex.Label == "B");
126+
127+
_powershell.AddCommand("Get-GraphTopologicalSort")
128+
.AddParameter("Graph", graph)
129+
.AddParameter("StartVertex", startVertex);
130+
131+
var results = _powershell.Invoke();
132+
133+
var vertices = results.Select(result => result.BaseObject as PSVertex).ToList();
134+
vertices.Should().Equal(
135+
startVertex,
136+
graph.Vertices.Single(vertex => vertex.Label == "D"));
137+
AssertTopologicalOrder(graph, vertices!);
138+
}
139+
140+
[Fact]
141+
public void GetGraphTopologicalSort_StartVertexReverse_ReturnsReachableVerticesInReverseTopologicalOrder()
142+
{
143+
var graph = CreateDiamondGraph();
144+
var startVertex = graph.Vertices.Single(vertex => vertex.Label == "A");
145+
146+
_powershell.AddCommand("Get-GraphTopologicalSort")
147+
.AddParameter("Graph", graph)
148+
.AddParameter("StartVertex", startVertex)
149+
.AddParameter("Reverse");
150+
151+
var results = _powershell.Invoke();
152+
153+
var vertices = results.Select(result => result.BaseObject as PSVertex).ToList();
154+
vertices.Should().HaveCount(4);
155+
vertices.Last().Should().Be(startVertex);
156+
AssertReverseTopologicalOrder(graph, vertices!);
157+
}
158+
159+
[Fact]
160+
public void GetGraphTopologicalSort_StartVertex_ThrowsWhenVertexIsNotInGraph()
161+
{
162+
var graph = CreateDiamondGraph();
163+
var startVertex = new PSVertex("X");
164+
165+
_powershell.AddCommand("Get-GraphTopologicalSort")
166+
.AddParameter("Graph", graph)
167+
.AddParameter("StartVertex", startVertex);
168+
169+
Action act = () => _powershell.Invoke();
170+
171+
act.Should().Throw<CmdletInvocationException>()
172+
.WithMessage("*The graph does not contain the provided start vertex*");
173+
}
174+
175+
[Fact]
176+
public void GetGraphTopologicalSort_StartVertex_IgnoresUnreachableCycle()
177+
{
178+
var graph = CreateDiamondGraph();
179+
var x = new PSVertex("X");
180+
var y = new PSVertex("Y");
181+
graph.AddVerticesAndEdge(new PSEdge(x, y));
182+
graph.AddVerticesAndEdge(new PSEdge(y, x));
183+
184+
var startVertex = graph.Vertices.Single(vertex => vertex.Label == "A");
185+
186+
_powershell.AddCommand("Get-GraphTopologicalSort")
187+
.AddParameter("Graph", graph)
188+
.AddParameter("StartVertex", startVertex);
189+
190+
var results = _powershell.Invoke();
191+
192+
var vertices = results.Select(result => result.BaseObject as PSVertex).ToList();
193+
vertices.Should().HaveCount(4);
194+
vertices.Should().NotContain(vertex => vertex!.Label == "X");
195+
vertices.Should().NotContain(vertex => vertex!.Label == "Y");
196+
AssertTopologicalOrder(graph, vertices!);
197+
}
198+
117199
private static PsBidirectionalGraph CreateDiamondGraph()
118200
{
119201
var graph = new PsBidirectionalGraph();
@@ -137,6 +219,11 @@ private static void AssertTopologicalOrder(PsBidirectionalGraph graph, IList<PSV
137219

138220
foreach (var edge in graph.Edges)
139221
{
222+
if (!index.ContainsKey(edge.Source) || !index.ContainsKey(edge.Target))
223+
{
224+
continue;
225+
}
226+
140227
index[edge.Source].Should().BeLessThan(index[edge.Target]);
141228
}
142229
}
@@ -148,6 +235,11 @@ private static void AssertReverseTopologicalOrder(PsBidirectionalGraph graph, IL
148235

149236
foreach (var edge in graph.Edges)
150237
{
238+
if (!index.ContainsKey(edge.Source) || !index.ContainsKey(edge.Target))
239+
{
240+
continue;
241+
}
242+
151243
index[edge.Target].Should().BeLessThan(index[edge.Source]);
152244
}
153245
}

PSGraph/cmdlets/graph/GetGraphTopologicalSortCmdlet.cs

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
24
using System.Management.Automation;
35
using PSGraph.Model;
46
using QuikGraph.Algorithms;
@@ -16,23 +18,82 @@ public class GetGraphTopologicalSortCmdlet : PSCmdlet
1618
[Parameter(Mandatory = false)]
1719
public SwitchParameter Reverse { get; set; }
1820

21+
[Parameter(Mandatory = false)]
22+
[ValidateNotNull]
23+
public PSVertex? StartVertex { get; set; }
24+
1925
protected override void ProcessRecord()
2026
{
21-
if (!Graph.IsDag)
27+
var graphToSort = StartVertex is null
28+
? Graph
29+
: CreateReachableSubgraph(StartVertex);
30+
31+
if (!graphToSort.IsDag)
2232
{
2333
ThrowTerminatingError(new ErrorRecord(
2434
new InvalidOperationException("Topological sort requires a directed acyclic graph."),
2535
"GraphContainsCycle",
2636
ErrorCategory.InvalidData,
27-
Graph));
37+
graphToSort));
2838
return;
2939
}
3040

3141
var direction = Reverse.IsPresent
3242
? TopologicalSortDirection.Backward
3343
: TopologicalSortDirection.Forward;
3444

35-
var sortedVertices = Graph.SourceFirstBidirectionalTopologicalSort(direction);
45+
var sortedVertices = graphToSort.SourceFirstBidirectionalTopologicalSort(direction);
3646
WriteObject(sortedVertices, enumerateCollection: true);
3747
}
48+
49+
private PsBidirectionalGraph CreateReachableSubgraph(PSVertex startVertex)
50+
{
51+
var canonicalStartVertex = Graph.Vertices.FirstOrDefault(v => v.Equals(startVertex));
52+
if (canonicalStartVertex is null)
53+
{
54+
ThrowTerminatingError(new ErrorRecord(
55+
new InvalidOperationException("The graph does not contain the provided start vertex."),
56+
"StartVertexNotFound",
57+
ErrorCategory.ObjectNotFound,
58+
startVertex));
59+
return new PsBidirectionalGraph(Graph.AllowParallelEdges, Graph.UseNonUniqueLabels);
60+
}
61+
62+
var reachableVertices = GetReachableVertices(canonicalStartVertex);
63+
var subgraph = new PsBidirectionalGraph(Graph.AllowParallelEdges, Graph.UseNonUniqueLabels);
64+
subgraph.AddVertexRange(reachableVertices);
65+
subgraph.AddEdgeRange(Graph.Edges.Where(edge =>
66+
reachableVertices.Contains(edge.Source) &&
67+
reachableVertices.Contains(edge.Target)));
68+
69+
return subgraph;
70+
}
71+
72+
private HashSet<PSVertex> GetReachableVertices(PSVertex startVertex)
73+
{
74+
var reachableVertices = new HashSet<PSVertex>();
75+
var pendingVertices = new Stack<PSVertex>();
76+
pendingVertices.Push(startVertex);
77+
78+
while (pendingVertices.Count > 0)
79+
{
80+
var currentVertex = pendingVertices.Pop();
81+
if (!reachableVertices.Add(currentVertex))
82+
{
83+
continue;
84+
}
85+
86+
if (!Graph.TryGetOutEdges(currentVertex, out var outEdges))
87+
{
88+
continue;
89+
}
90+
91+
foreach (var edge in outEdges)
92+
{
93+
pendingVertices.Push(edge.Target);
94+
}
95+
}
96+
97+
return reachableVertices;
98+
}
3899
}

docs/Get-GraphTopologicalSort.md

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Return vertices from a directed acyclic graph in topological order.
1313
## SYNTAX
1414

1515
```
16-
Get-GraphTopologicalSort -Graph <PsBidirectionalGraph> [-Reverse] [-ProgressAction <ActionPreference>] [<CommonParameters>]
16+
Get-GraphTopologicalSort -Graph <PsBidirectionalGraph> [-Reverse] [-StartVertex <PSVertex>] [-ProgressAction <ActionPreference>] [<CommonParameters>]
1717
```
1818

1919
## DESCRIPTION
@@ -27,6 +27,9 @@ positions as long as every edge preserves the source-before-target constraint.
2727
If the graph contains a cycle, the cmdlet writes a terminating error. Use DSM sequencing or
2828
condensation algorithms when cyclic components need to be grouped before ordering.
2929

30+
When `-StartVertex` is specified, the cmdlet sorts only the vertices reachable from that vertex
31+
by outgoing edges. Cycles outside the reachable subgraph do not affect the result.
32+
3033
## EXAMPLES
3134

3235
### Example 1
@@ -56,6 +59,24 @@ Get-GraphTopologicalSort -Graph $g -Reverse | ForEach-Object Name
5659
```
5760

5861
### Example 3
62+
Sort only the reachable subgraph from a start vertex.
63+
64+
```powershell
65+
$g = New-Graph
66+
$a = Add-Vertex -Vertex A -Graph $g
67+
$b = Add-Vertex -Vertex B -Graph $g
68+
$c = Add-Vertex -Vertex C -Graph $g
69+
$x = Add-Vertex -Vertex X -Graph $g
70+
71+
Add-Edge -From $a -To $b -Graph $g | Out-Null
72+
Add-Edge -From $b -To $c -Graph $g | Out-Null
73+
74+
Get-GraphTopologicalSort -Graph $g -StartVertex $b | ForEach-Object Name
75+
```
76+
77+
The command returns `B`, then `C`. The independent vertex `X` is not emitted.
78+
79+
### Example 4
5980
Cycles are rejected.
6081

6182
```powershell
@@ -85,6 +106,21 @@ Accept pipeline input: False
85106
Accept wildcard characters: False
86107
```
87108
109+
### -StartVertex
110+
Optional vertex that limits the sort to the subgraph reachable from that vertex by outgoing edges.
111+
112+
```yaml
113+
Type: PSVertex
114+
Parameter Sets: (All)
115+
Aliases:
116+
117+
Required: False
118+
Position: Named
119+
Default value: None
120+
Accept pipeline input: False
121+
Accept wildcard characters: False
122+
```
123+
88124
### -Reverse
89125
Return a valid reverse topological ordering, where targets appear before sources.
90126

0 commit comments

Comments
 (0)